From b90c2072f1ef8f55ef495bfa3920af4bb482f0cd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 22:13:08 -0300 Subject: data: valdiateArrayItems: use same index formatting as other errors Specifically, the same as decorateErrorWithIndex. --- src/data/things/validators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index fc953c2a..5748eacf 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -174,7 +174,7 @@ function validateArrayItemsHelper(itemValidator) { throw new Error(`Expected validator to return true`); } } catch (error) { - error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${error.message}`; + error.message = `(index: ${color.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; throw error; } }; -- cgit 1.3.0-6-gf8a5 From 3adf33b567c53e79ce45a36031b29da719fb2377 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 22:14:04 -0300 Subject: sugar: showAggregate: display top-level non-AggregateErrors w/ more detail --- src/util/sugar.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/util/sugar.js b/src/util/sugar.js index 487c093c..5b1f3193 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -574,7 +574,12 @@ export function showAggregate(topError, { } }; - const message = recursive(topError, {level: 0}); + const message = + (topError instanceof AggregateError + ? recursive(topError, {level: 0}) + : (showTraces + ? topError.stack + : topError.toString())); if (print) { console.error(message); -- cgit 1.3.0-6-gf8a5 From 98133431c801a823f3430b0e178c56350e12622c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 22:14:34 -0300 Subject: write: static-build: gently log failed content functions & continue --- src/util/cli.js | 4 +++- src/write/build-modes/static-build.js | 38 +++++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/util/cli.js b/src/util/cli.js index f83c8061..e8c8c79f 100644 --- a/src/util/cli.js +++ b/src/util/cli.js @@ -334,7 +334,9 @@ export function progressCallAll(msgOrMsgFn, array) { export function fileIssue({ topMessage = `This shouldn't happen.`, } = {}) { - console.error(color.red(`${topMessage} Please let the HSMusic developers know:`)); + if (topMessage) { + console.error(color.red(`${topMessage} Please let the HSMusic developers know:`)); + } console.error(color.red(`- https://hsmusic.wiki/feedback/`)); console.error(color.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`)); } diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 2210dfe7..192b7966 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -17,6 +17,7 @@ import {serializeThings} from '#serialize'; import {empty, queue, withEntries} from '#sugar'; import { + fileIssue, logError, logInfo, logWarn, @@ -98,6 +99,7 @@ export async function go({ developersComment, getSizeOfAdditionalFile, getSizeOfImageFile, + niceShowAggregate, }) { const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT; const appendIndexHTML = cliOptions['append-index-html'] ?? false; @@ -253,6 +255,8 @@ export async function go({ )); */ + let errored = false; + const contentDependencies = await quickLoadContentDependencies(); const perLanguageFn = async (language, i, entries) => { @@ -303,14 +307,22 @@ export async function go({ wikiData, }); - const topLevelResult = - quickEvaluate({ - contentDependencies, - extraDependencies: {...bound, appendIndexHTML}, - - name: page.contentFunction.name, - args: page.contentFunction.args ?? [], - }); + let topLevelResult; + try { + topLevelResult = + quickEvaluate({ + contentDependencies, + extraDependencies: {...bound, appendIndexHTML}, + + name: page.contentFunction.name, + args: page.contentFunction.args ?? [], + }); + } catch (error) { + logError`\rError generating page: ${pathname}`; + niceShowAggregate(error); + errored = true; + return; + } const {pageHTML, oEmbedJSON} = html.resolve(topLevelResult); @@ -358,6 +370,16 @@ export async function go({ // The single most important step. logInfo`Written!`; + + if (errored) { + logWarn`The code generating content for some pages ended up erroring.`; + logWarn`These pages were skipped, so if you ran a build previously and`; + logWarn`they didn't error that time, then the old version is still`; + logWarn`available - albeit possibly outdated! Please scroll up and send`; + logWarn`the HSMusic developers a copy of the errors:`; + fileIssue({topMessage: null}); + } + return true; } -- cgit 1.3.0-6-gf8a5 From 5c2aca67db34301e4fb11c28b1b737e9e47c88ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 23 Aug 2023 18:35:33 -0300 Subject: write: live-dev-server: fix bad error response for data.json --- src/write/build-modes/live-dev-server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 2767a02f..28cf7a48 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -163,7 +163,7 @@ export async function go({ if (!quietResponses) console.log(`${requestHead} [200] /data.json`); } catch (error) { response.writeHead(500, contentTypeJSON); - response.end({error: `Internal error serializing wiki JSON`}); + response.end(`Internal error serializing wiki JSON`); console.error(`${requestHead} [500] /data.json`); showError(error); } -- cgit 1.3.0-6-gf8a5 From 1a359fd6f0a4f672db3e50ee7d5398f223124edb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 27 May 2023 11:13:18 -0300 Subject: thumbs: get identify binary in addition to convert --- src/gen-thumbs.js | 55 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index e9932822..0846dd6f 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -122,8 +122,8 @@ function readFileMD5(filePath) { }); } -async function getImageMagickVersion(spawnConvert) { - const proc = spawnConvert(['--version'], false); +async function getImageMagickVersion(binary) { + const proc = spawn(binary, ['--version']); let allData = ''; proc.stdout.on('data', (data) => { @@ -144,23 +144,33 @@ async function getImageMagickVersion(spawnConvert) { return match[1]; } -async function getSpawnConvert() { - let fn, description, version; - if (await commandExists('convert')) { - fn = (args) => spawn('convert', args); - description = 'convert'; - } else if (await commandExists('magick')) { - fn = (args, prefix = true) => - spawn('magick', prefix ? ['convert', ...args] : args); - description = 'magick convert'; - } else { - return [`no convert or magick binary`, null]; +async function getSpawnMagick(tool) { + if (tool !== 'identify' && tool !== 'convert') { + throw new Error(`Expected identify or convert`); } - version = await getImageMagickVersion(fn); + let fn = null; + let description = null; + let version = null; - if (version === null) { - return [`binary --version output didn't indicate it's ImageMagick`]; + if (await commandExists(tool)) { + version = await getImageMagickVersion(tool); + if (version !== null) { + fn = (args) => spawn(tool, args); + description = tool; + } + } + + if (fn === null && await commandExists('magick')) { + version = await getImageMagickVersion(fn); + if (version !== null) { + fn = (args) => spawn('magick', [tool, ...args]); + description = `magick ${tool}`; + } + } + + if (fn === null) { + return [`no ${tool} or magick binary`, null]; } return [`${description} (${version})`, fn]; @@ -290,18 +300,23 @@ export default async function genThumbs(mediaPath, { const quietInfo = quiet ? () => null : logInfo; - const [convertInfo, spawnConvert] = (await getSpawnConvert()) ?? []; - if (!spawnConvert) { + const [convertInfo, spawnConvert] = await getSpawnMagick('convert'); + const [identifyInfo, spawnIdentify] = await getSpawnMagick('convert'); + + if (!spawnConvert || !spawnIdentify) { logError`${`It looks like you don't have ImageMagick installed.`}`; logError`ImageMagick is required to generate thumbnails for display on the wiki.`; - logError`(Error message: ${convertInfo})`; + for (const error of [convertInfo, identifyInfo].filter(Boolean)) { + logError`(Error message: ${error})`; + } logInfo`You can find info to help install ImageMagick on Linux, Windows, or macOS`; logInfo`from its official website: ${`https://imagemagick.org/script/download.php`}`; logInfo`If you have trouble working ImageMagick and would like some help, feel free`; logInfo`to drop a message in the HSMusic Discord server! ${'https://hsmusic.wiki/discord/'}`; return false; } else { - logInfo`Found ImageMagick binary: ${convertInfo}`; + logInfo`Found ImageMagick convert binary: ${convertInfo}`; + logInfo`Found ImageMagick identify binary: ${identifyInfo}`; } quietInfo`Running up to ${magickThreads + ' magick threads'} simultaneously.`; -- cgit 1.3.0-6-gf8a5 From 0944e67a92b2ac7203af1e7152a33395b32923a2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 27 May 2023 12:08:26 -0300 Subject: thumbs: imagemagick is fricking killing me --- src/gen-thumbs.js | 111 +++++++++++++++++++++++++++++++++++++++++++----------- src/upd8.js | 13 ++++--- 2 files changed, 97 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 0846dd6f..2f5304ea 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -122,6 +122,48 @@ function readFileMD5(filePath) { }); } +async function identifyImageDimensions(filePath, {spawnIdentify}) { + const maxTries = 5; + + const recursive = async n => { + if (n > maxTries) { + throw new Error(`Didn't get any output after ${maxTries} tries`); + } + + if (n > 1) { + logInfo`Attempt #${n} for ${filePath}`; + } + + const stdoutText = await new Promise((resolve, reject) => { + let stdout = ''; + let stderr = ''; + + const proc = spawnIdentify(['-format', '%w %h', filePath]); + proc.stdout.on('data', data => stdout += data); + proc.stderr.on('data', data => stderr += data); + + proc.on('exit', code => { + if (code === 0) { + resolve(stdout); + } else { + reject(stderr); + } + }); + }); + + if (stdoutText === '') { + return recursive(n + 1); + } + + const words = stdoutText.split(' '); + const width = parseInt(words[0]); + const height = parseInt(words[1]); + return [width, height]; + }; + + return recursive(1); +} + async function getImageMagickVersion(binary) { const proc = spawn(binary, ['--version']); @@ -301,7 +343,7 @@ export default async function genThumbs(mediaPath, { const quietInfo = quiet ? () => null : logInfo; const [convertInfo, spawnConvert] = await getSpawnMagick('convert'); - const [identifyInfo, spawnIdentify] = await getSpawnMagick('convert'); + const [identifyInfo, spawnIdentify] = await getSpawnMagick('identify'); if (!spawnConvert || !spawnIdentify) { logError`${`It looks like you don't have ImageMagick installed.`}`; @@ -313,7 +355,7 @@ export default async function genThumbs(mediaPath, { logInfo`from its official website: ${`https://imagemagick.org/script/download.php`}`; logInfo`If you have trouble working ImageMagick and would like some help, feel free`; logInfo`to drop a message in the HSMusic Discord server! ${'https://hsmusic.wiki/discord/'}`; - return false; + return {success: false}; } else { logInfo`Found ImageMagick convert binary: ${convertInfo}`; logInfo`Found ImageMagick identify binary: ${identifyInfo}`; @@ -356,19 +398,16 @@ export default async function genThumbs(mediaPath, { const imagePaths = await traverseSourceImagePaths(mediaPath, {target: 'generate'}); - const imageToMD5Entries = await progressPromiseAll( - `Generating MD5s of image files`, - queue( - imagePaths.map( - (imagePath) => () => - readFileMD5(path.join(mediaPath, imagePath)).then( - (md5) => [imagePath, md5], - (error) => [imagePath, {error}] - ) - ), - queueSize - ) - ); + const imageToMD5Entries = + await progressPromiseAll( + `Generating MD5s of image files`, + queue( + imagePaths.map(imagePath => () => + readFileMD5(path.join(mediaPath, imagePath)) + .then( + md5 => [imagePath, md5], + error => [imagePath, {error}])), + queueSize)); { let error = false; @@ -382,23 +421,53 @@ export default async function genThumbs(mediaPath, { logError`Failed to read at least one image file!`; logError`This implies a thumbnail probably won't be generatable.`; logError`So, exiting early.`; - return false; + return {success: false}; } else { quietInfo`All image files successfully read.`; } } + const imageToDimensionsEntries = + await progressPromiseAll( + `Identifying dimensions of image files`, + queue( + imagePaths.map(imagePath => () => + identifyImageDimensions(path.join(mediaPath, imagePath), {spawnIdentify}) + .then( + dimensions => [imagePath, dimensions], + error => [imagePath, {error}])), + queueSize)); + + { + let error = false; + for (const entry of imageToDimensionsEntries) { + if (entry[1].error) { + logError`Failed to identify dimensions ${entry[0]}: ${entry[1].error}`; + error = true; + } + } + if (error) { + logError`Failed to identify dimensions of at least one image file!`; + logError`This implies a thumbnail probably won't be generatable.`; + logError`So, exiting early.`; + return {success: false}; + } else { + quietInfo`All image files successfully had dimensions identified.`; + } + } + + const imageToDimensions = Object.fromEntries(imageToDimensionsEntries); + // Technically we could pro8a8ly mut8te the cache varia8le in-place? // 8ut that seems kinda iffy. const updatedCache = Object.assign({}, cache); const entriesToGenerate = imageToMD5Entries.filter( - ([filePath, md5]) => md5 !== cache[filePath] - ); + ([filePath, md5]) => md5 !== cache[filePath]?.[0]); if (empty(entriesToGenerate)) { logInfo`All image thumbnails are already up-to-date - nice!`; - return true; + return {success: true, cache}; } logInfo`Generating thumbnails for ${entriesToGenerate.length} media files.`; @@ -416,7 +485,7 @@ export default async function genThumbs(mediaPath, { entriesToGenerate.map(([filePath, md5]) => () => generateImageThumbnails(path.join(mediaPath, filePath), {spawnConvert}).then( () => { - updatedCache[filePath] = md5; + updatedCache[filePath] = [md5, ...imageToDimensions[filePath]]; succeeded.push(filePath); }, error => { @@ -446,7 +515,7 @@ export default async function genThumbs(mediaPath, { logWarn`Sorry about that!`; } - return true; + return {success: true, cache: updatedCache}; } export function getExpectedImagePaths(mediaPath, {urls, wikiData}) { diff --git a/src/upd8.js b/src/upd8.js index bfdd1c2a..2051517b 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -418,11 +418,12 @@ async function main() { } if (clearThumbsFlag) { - await clearThumbs(mediaPath, {queueSize}); - - logInfo`All done! Remove ${'--clear-thumbs'} to run the next build.`; - if (skipThumbs) { - logInfo`And don't forget to remove ${'--skip-thumbs'} too, eh?`; + const result = await clearThumbs(mediaPath, {queueSize}); + if (result.success) { + logInfo`All done! Remove ${'--clear-thumbs'} to run the next build.`; + if (skipThumbs) { + logInfo`And don't forget to remove ${'--skip-thumbs'} too, eh?`; + } } return; } @@ -437,7 +438,7 @@ async function main() { quiet: !thumbsOnly, }); logInfo`Done thumbnail generation! --------+`; - if (!result) return; + if (!result.success) return; if (thumbsOnly) return; } -- cgit 1.3.0-6-gf8a5 From d68ead9ff27a166ccf90492cd900ef4d8d6b8e3e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 27 May 2023 12:15:41 -0300 Subject: thumbs: use image-size module instead of magick identify --- src/gen-thumbs.js | 57 +++++++++++-------------------------------------------- 1 file changed, 11 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 2f5304ea..4f5c0fec 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -91,6 +91,8 @@ import {createReadStream} from 'node:fs'; import {readFile, stat, unlink, writeFile} from 'node:fs/promises'; import * as path from 'node:path'; +import dimensionsOf from 'image-size'; + import { color, fileIssue, @@ -122,46 +124,11 @@ function readFileMD5(filePath) { }); } -async function identifyImageDimensions(filePath, {spawnIdentify}) { - const maxTries = 5; - - const recursive = async n => { - if (n > maxTries) { - throw new Error(`Didn't get any output after ${maxTries} tries`); - } - - if (n > 1) { - logInfo`Attempt #${n} for ${filePath}`; - } - - const stdoutText = await new Promise((resolve, reject) => { - let stdout = ''; - let stderr = ''; - - const proc = spawnIdentify(['-format', '%w %h', filePath]); - proc.stdout.on('data', data => stdout += data); - proc.stderr.on('data', data => stderr += data); - - proc.on('exit', code => { - if (code === 0) { - resolve(stdout); - } else { - reject(stderr); - } - }); - }); - - if (stdoutText === '') { - return recursive(n + 1); - } - - const words = stdoutText.split(' '); - const width = parseInt(words[0]); - const height = parseInt(words[1]); - return [width, height]; - }; - - return recursive(1); +async function identifyImageDimensions(filePath) { + // See: https://github.com/image-size/image-size/issues/96 + const buffer = await readFile(filePath); + const dimensions = dimensionsOf(buffer); + return [dimensions.width, dimensions.height]; } async function getImageMagickVersion(binary) { @@ -343,12 +310,11 @@ export default async function genThumbs(mediaPath, { const quietInfo = quiet ? () => null : logInfo; const [convertInfo, spawnConvert] = await getSpawnMagick('convert'); - const [identifyInfo, spawnIdentify] = await getSpawnMagick('identify'); - if (!spawnConvert || !spawnIdentify) { + if (!spawnConvert) { logError`${`It looks like you don't have ImageMagick installed.`}`; logError`ImageMagick is required to generate thumbnails for display on the wiki.`; - for (const error of [convertInfo, identifyInfo].filter(Boolean)) { + for (const error of [convertInfo].filter(Boolean)) { logError`(Error message: ${error})`; } logInfo`You can find info to help install ImageMagick on Linux, Windows, or macOS`; @@ -357,8 +323,7 @@ export default async function genThumbs(mediaPath, { logInfo`to drop a message in the HSMusic Discord server! ${'https://hsmusic.wiki/discord/'}`; return {success: false}; } else { - logInfo`Found ImageMagick convert binary: ${convertInfo}`; - logInfo`Found ImageMagick identify binary: ${identifyInfo}`; + logInfo`Found ImageMagick binary: ${convertInfo}`; } quietInfo`Running up to ${magickThreads + ' magick threads'} simultaneously.`; @@ -432,7 +397,7 @@ export default async function genThumbs(mediaPath, { `Identifying dimensions of image files`, queue( imagePaths.map(imagePath => () => - identifyImageDimensions(path.join(mediaPath, imagePath), {spawnIdentify}) + identifyImageDimensions(path.join(mediaPath, imagePath)) .then( dimensions => [imagePath, dimensions], error => [imagePath, {error}])), -- cgit 1.3.0-6-gf8a5 From 641d821d03ad96bca28b8b09d2408457443a9f7f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 30 May 2023 09:50:21 -0300 Subject: thumbs: only generate thumbs of appropriate sizes --- src/gen-thumbs.js | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 4f5c0fec..bf6de286 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -114,6 +114,60 @@ import {delay, empty, queue} from '#sugar'; export const defaultMagickThreads = 8; +export function getThumbnailsAvailableForDimensions([width, height]) { + // This function is intended to be portable, so it can be used both for + // calculating which thumbnails to generate, and which ones will be ready + // to reference in generated code. Sizes are in array [name, size] form + // with larger sizes earlier in return. Keep in mind this isn't a direct + // 1:1 mapping with the sizes listed in the thumbnail spec, because the + // largest thumbnail (first in return) will be adjusted to the provided + // dimensions. + + const {all} = getThumbnailsAvailableForDimensions; + + // Find the largest size which is beneath the passed dimensions. We use the + // longer edge here (of width and height) so that each resulting thumbnail is + // fully constrained within the size*size square defined by its spec. + const longerEdge = Math.max(width, height); + const index = all.findIndex(([name, size]) => size <= longerEdge); + + // Literal edge cases are handled specially. For dimensions which are bigger + // than the biggest thumbnail in the spec, return all possible results. + // These don't need any adjustments since the largest is already smaller than + // the provided dimensions. + if (index === 0) { + return [ + ...all, + ]; + } + + // For dimensions which are smaller than the smallest thumbnail, return only + // the smallest, adjusted to the provided dimensions. + if (index === -1) { + const smallest = all[all.length - 1]; + return [ + [smallest[0], longerEdge], + ]; + } + + // For non-edge cases, we return the largest size below the dimensions + // as well as everything smaller, but also the next size larger - that way + // there's a size which is as big as the original, but still JPEG compressed. + // The size larger is adjusted to the provided dimensions to represent the + // actual dimensions it'll provide. + const larger = all[index - 1]; + const rest = all.slice(index); + return [ + [larger[0], longerEdge], + ...rest, + ]; +} + +getThumbnailsAvailableForDimensions.all = + Object.entries(thumbnailSpec) + .map(([name, {size}]) => [name, size]) + .sort((a, b) => b[1] - a[1]); + function readFileMD5(filePath) { return new Promise((resolve, reject) => { const md5 = createHash('md5'); @@ -185,7 +239,11 @@ async function getSpawnMagick(tool) { return [`${description} (${version})`, fn]; } -function generateImageThumbnails(filePath, {spawnConvert}) { +function generateImageThumbnails({ + filePath, + dimensions, + spawnConvert, +}) { const dirname = path.dirname(filePath); const extname = path.extname(filePath); const basename = path.basename(filePath, extname); @@ -205,9 +263,10 @@ function generateImageThumbnails(filePath, {spawnConvert}) { ]); return Promise.all( - Object.entries(thumbnailSpec) - .map(([ext, details]) => - promisifyProcess(convert('.' + ext, details), false))); + getThumbnailsAvailableForDimensions(dimensions) + .map(([name]) => [name, thumbnailSpec[name]]) + .map(([name, details]) => + promisifyProcess(convert('.' + name, details), false))); } export async function clearThumbs(mediaPath, { @@ -448,7 +507,11 @@ export default async function genThumbs(mediaPath, { await progressPromiseAll(writeMessageFn, queue( entriesToGenerate.map(([filePath, md5]) => () => - generateImageThumbnails(path.join(mediaPath, filePath), {spawnConvert}).then( + generateImageThumbnails({ + filePath: path.join(mediaPath, filePath), + dimensions: imageToDimensions[filePath], + spawnConvert, + }).then( () => { updatedCache[filePath] = [md5, ...imageToDimensions[filePath]]; succeeded.push(filePath); -- cgit 1.3.0-6-gf8a5 From 029210cc329a015a939472a688209d3f3423242b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 30 May 2023 09:51:26 -0300 Subject: thumbs, content: integrate cached thumb sizes into content --- src/content/dependencies/image.js | 85 +++++++++++++++++++++++--------- src/gen-thumbs.js | 31 +++++++++++- src/upd8.js | 36 ++++++++++++-- src/write/bind-utilities.js | 20 +++++++- src/write/build-modes/live-dev-server.js | 10 ++-- src/write/build-modes/static-build.js | 6 ++- 6 files changed, 151 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 71b905f7..905b3c2e 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -2,10 +2,12 @@ import {empty} from '#sugar'; export default { extraDependencies: [ - 'getSizeOfImageFile', + 'getDimensionsOfImagePath', + 'getSizeOfImagePath', + 'getThumbnailEqualOrSmaller', + 'getThumbnailsAvailableForDimensions', 'html', 'language', - 'thumb', 'to', ], @@ -52,10 +54,12 @@ export default { }, generate(data, slots, { - getSizeOfImageFile, + getDimensionsOfImagePath, + getSizeOfImagePath, + getThumbnailEqualOrSmaller, + getThumbnailsAvailableForDimensions, html, language, - thumb, to, }) { let originalSrc; @@ -68,12 +72,6 @@ export default { originalSrc = ''; } - const thumbSrc = - originalSrc && - (slots.thumb - ? thumb[slots.thumb](originalSrc) - : originalSrc); - const willLink = typeof slots.link === 'string' || slots.link; const customLink = typeof slots.link === 'string'; @@ -95,18 +93,6 @@ export default { slots.missingSourceContent)); } - let fileSize = null; - if (willLink) { - const mediaRoot = to('media.root'); - if (originalSrc.startsWith(mediaRoot)) { - fileSize = - getSizeOfImageFile( - originalSrc - .slice(mediaRoot.length) - .replace(/^\//, '')); - } - } - let reveal = null; if (willReveal) { reveal = [ @@ -119,16 +105,67 @@ export default { ]; } + let mediaSrc = null; + if (originalSrc.startsWith(to('media.root'))) { + mediaSrc = + originalSrc + .slice(to('media.root').length) + .replace(/^\//, ''); + } + + let thumbSrc = null; + if (mediaSrc) { + // Note: This provides mediaSrc to getThumbnailEqualOrSmaller, since + // it's the identifier which thumbnail utilities use to query from the + // thumbnail cache. But we use the result to operate on originalSrc, + // which is the HTML output-appropriate path including `../../` or + // another alternate base path. + const selectedSize = getThumbnailEqualOrSmaller(slots.thumb, mediaSrc); + thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${selectedSize}.jpg`); + } + + let originalWidth = null; + let originalHeight = null; + let availableThumbs = null; + if (mediaSrc) { + [originalWidth, originalHeight] = + getDimensionsOfImagePath(mediaSrc); + availableThumbs = + getThumbnailsAvailableForDimensions([originalWidth, originalHeight]); + } + + let fileSize = null; + if (willLink && mediaSrc) { + fileSize = getSizeOfImagePath(mediaSrc); + } + const imgAttributes = { id: idOnImg, class: classOnImg, alt: slots.alt, width: slots.width, height: slots.height, - 'data-original-size': fileSize, - 'data-no-image-preview': customLink, }; + if (customLink) { + imgAttributes['data-no-image-preview'] = true; + } + + if (fileSize) { + imgAttributes['data-original-size'] = fileSize; + } + + if (originalWidth && originalHeight) { + imgAttributes['data-original-length'] = Math.max(originalWidth, originalHeight); + } + + if (!empty(availableThumbs)) { + imgAttributes['data-thumbs'] = + availableThumbs + .map(([name, size]) => `${name}:${size}`) + .join(' '); + } + const nonlazyHTML = originalSrc && prepare( diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index bf6de286..f6a8eaaf 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -74,7 +74,7 @@ 'use strict'; -const CACHE_FILE = 'thumbnail-cache.json'; +export const CACHE_FILE = 'thumbnail-cache.json'; const WARNING_DELAY_TIME = 10000; const thumbnailSpec = { @@ -168,6 +168,30 @@ getThumbnailsAvailableForDimensions.all = .map(([name, {size}]) => [name, size]) .sort((a, b) => b[1] - a[1]); +export function getDimensionsOfImagePath(imagePath, cache) { + // This function is really generic. It takes the gen-thumbs image cache and + // returns the dimensions in that cache, so that other functions don't need + // to hard-code the cache format. + + const [width, height] = cache[imagePath].slice(1); + return [width, height]; +} + +export function getThumbnailEqualOrSmaller(preferred, imagePath, cache) { + // This function is totally exclusive to page generation. It's a shorthand + // for accessing dimensions from the thumbnail cache, calculating all the + // thumbnails available, and selecting the one which is equal to or smaller + // than the provided size. Since the path provided might not be the actual + // one which is being thumbnail-ified, this just returns the name of the + // selected thumbnail size. + + const {size: preferredSize} = thumbnailSpec[preferred]; + const [width, height] = getDimensionsOfImagePath(imagePath, cache); + const available = getThumbnailsAvailableForDimensions([width, height]); + const [selected] = available.find(([name, size]) => size <= preferredSize); + return selected; +} + function readFileMD5(filePath) { return new Promise((resolve, reject) => { const md5 = createHash('md5'); @@ -302,7 +326,7 @@ export async function clearThumbs(mediaPath, { console.error(file); } fileIssue(); - return; + return {success: false}; } logInfo`Clearing out ${thumbFiles.length} thumbs.`; @@ -327,6 +351,7 @@ export async function clearThumbs(mediaPath, { console.error(file); } logError`Check for permission errors?`; + return {success: false}; } else { logInfo`Successfully deleted all ${thumbFiles.length} thumbnail files!`; } @@ -355,6 +380,8 @@ export async function clearThumbs(mediaPath, { logWarn`Failed to remove cache file. Check its permissions?`; } } + + return {success: true}; } export default async function genThumbs(mediaPath, { diff --git a/src/upd8.js b/src/upd8.js index 2051517b..2f08204a 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -32,6 +32,7 @@ // node.js and you'll 8e fine. ...Within the project root. O8viously. import {execSync} from 'node:child_process'; +import {readFile} from 'node:fs/promises'; import * as path from 'node:path'; import {fileURLToPath} from 'node:url'; @@ -57,6 +58,7 @@ import { } from '#cli'; import genThumbs, { + CACHE_FILE as thumbsCacheFile, clearThumbs, defaultMagickThreads, isThumb, @@ -428,7 +430,33 @@ async function main() { return; } + let thumbsCache; + if (skipThumbs) { + const thumbsCachePath = path.join(mediaPath, thumbsCacheFile); + try { + thumbsCache = JSON.parse(await readFile(thumbsCachePath)); + logInfo`Thumbnail cache file successfully read.`; + } catch (error) { + if (error.code === 'ENOENT') { + logError`The thumbnail cache doesn't exist, and it's necessary to build` + logError`the website. Please run once without ${'--skip-thumbs'} - after` + logError`that you'll be good to go and don't need to process thumbnails` + logError`again!`; + return; + } else { + logError`Malformed or unreadable thumbnail cache file: ${error}`; + logError`Path: ${thumbsCachePath}`; + logError`The thumbbnail cache is necessary to build the site, so you'll`; + logError`have to investigate this to get the build working. Try running`; + logError`again without ${'--skip-thumbs'}. If you can't get it working,`; + logError`you're welcome to message in the HSMusic Discord and we'll try`; + logError`to help you out with troubleshooting!`; + logError`${'https://hsmusic.wiki/discord/'}`; + return; + } + } + logInfo`Skipping thumbnail generation.`; } else { logInfo`Begin thumbnail generation... -----+`; @@ -440,6 +468,7 @@ async function main() { logInfo`Done thumbnail generation! --------+`; if (!result.success) return; if (thumbsOnly) return; + thumbsCache = result.cache; } if (noBuild) { @@ -705,7 +734,7 @@ async function main() { }; const getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths); - const getSizeOfImageFile = getSizeOfMediaFileHelper(imageFilePaths); + const getSizeOfImagePath = getSizeOfMediaFileHelper(imageFilePaths); logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`; @@ -760,14 +789,15 @@ async function main() { defaultLanguage: finalDefaultLanguage, languages, - wikiData, + thumbsCache, urls, urlSpec, + wikiData, cachebust: '?' + CACHEBUST, developersComment, getSizeOfAdditionalFile, - getSizeOfImageFile, + getSizeOfImagePath, niceShowAggregate, }); } diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 8e2adea7..c32035f1 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -10,15 +10,22 @@ import * as html from '#html'; import {bindOpts} from '#sugar'; import {thumb} from '#urls'; +import { + getDimensionsOfImagePath, + getThumbnailEqualOrSmaller, + getThumbnailsAvailableForDimensions, +} from '#thumbs'; + export function bindUtilities({ absoluteTo, cachebust, defaultLanguage, getSizeOfAdditionalFile, - getSizeOfImageFile, + getSizeOfImagePath, language, languages, pagePath, + thumbsCache, to, urls, wikiData, @@ -30,7 +37,8 @@ export function bindUtilities({ cachebust, defaultLanguage, getSizeOfAdditionalFile, - getSizeOfImageFile, + getSizeOfImagePath, + getThumbnailsAvailableForDimensions, html, language, languages, @@ -46,5 +54,13 @@ export function bindUtilities({ bound.find = bindFind(wikiData, {mode: 'warn'}); + bound.getDimensionsOfImagePath = + (imagePath) => + getDimensionsOfImagePath(imagePath, thumbsCache); + + bound.getThumbnailEqualOrSmaller = + (preferred, imagePath) => + getThumbnailEqualOrSmaller(preferred, imagePath, thumbsCache); + return bound; } diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 28cf7a48..9889b3f0 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -59,13 +59,14 @@ export async function go({ defaultLanguage, languages, srcRootPath, + thumbsCache, urls, wikiData, cachebust, developersComment, getSizeOfAdditionalFile, - getSizeOfImageFile, + getSizeOfImagePath, niceShowAggregate, }) { const showError = (error) => { @@ -343,10 +344,11 @@ export async function go({ cachebust, defaultLanguage, getSizeOfAdditionalFile, - getSizeOfImageFile, + getSizeOfImagePath, language, languages, pagePath: servePath, + thumbsCache, to, urls, wikiData, @@ -367,10 +369,10 @@ export async function go({ response.writeHead(200, contentTypeHTML); response.end(pageHTML); } catch (error) { - response.writeHead(500, contentTypePlain); - response.end(`Error generating page, view server log for details\n`); console.error(`${requestHead} [500] ${pathname}`); showError(error); + response.writeHead(500, contentTypePlain); + response.end(`Error generating page, view server log for details\n`); } }); diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 192b7966..82a947c7 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -91,6 +91,7 @@ export async function go({ defaultLanguage, languages, srcRootPath, + thumbsCache, urls, urlSpec, wikiData, @@ -98,7 +99,7 @@ export async function go({ cachebust, developersComment, getSizeOfAdditionalFile, - getSizeOfImageFile, + getSizeOfImagePath, niceShowAggregate, }) { const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT; @@ -298,10 +299,11 @@ export async function go({ cachebust, defaultLanguage, getSizeOfAdditionalFile, - getSizeOfImageFile, + getSizeOfImagePath, language, languages, pagePath, + thumbsCache, to, urls, wikiData, -- cgit 1.3.0-6-gf8a5 From f25b377e0f06390e33835b5f3f0ea0cc31915173 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 1 Sep 2023 14:02:39 -0300 Subject: client: update image overlay for available thumb sizes --- src/static/client2.js | 71 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/static/client2.js b/src/static/client2.js index 0cdb8b0e..0a8eb860 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -709,24 +709,34 @@ function handleImageLinkClicked(evt) { const mainImage = document.getElementById('image-overlay-image'); const thumbImage = document.getElementById('image-overlay-image-thumb'); - const mainThumbSize = getPreferredThumbSize(); + const {href: originalSrc} = evt.target.closest('a'); + const {dataset: { + originalSize: originalFileSize, + thumbs: availableThumbList, + }} = evt.target.closest('a').querySelector('img'); - const source = evt.target.closest('a').href; + updateFileSizeInformation(originalFileSize); - const mainSrc = source.replace(/\.(jpg|png)$/, `.${mainThumbSize}.jpg`); - const thumbSrc = source.replace(/\.(jpg|png)$/, '.small.jpg'); + const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(availableThumbList); + const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(availableThumbList); + + const mainSrc = originalSrc.replace(/\.(jpg|png)$/, `.${mainThumb}.jpg`); + const thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${smallThumb}.jpg`); thumbImage.src = thumbSrc; + + // Show the thumbnail size on each <img> element's data attributes. + // Y'know, just for debugging convenience. + mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`; + thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`; + for (const viewOriginal of allViewOriginal) { - viewOriginal.href = source; + viewOriginal.href = originalSrc; } mainImage.addEventListener('load', handleMainImageLoaded); mainImage.addEventListener('error', handleMainImageErrored); - const fileSize = evt.target.closest('a').querySelector('img').dataset.originalSize; - updateFileSizeInformation(fileSize); - container.style.setProperty('--download-progress', '0%'); loadImage(mainSrc, progress => { container.style.setProperty('--download-progress', (20 + 0.8 * progress) + '%'); @@ -750,7 +760,21 @@ function handleImageLinkClicked(evt) { } } -function getPreferredThumbSize() { +function parseThumbList(availableThumbList) { + // Parse all the available thumbnail sizes! These are provided by the actual + // content generation on each image. + const defaultThumbList = 'huge:1400 semihuge:1200 large:800 medium:400 small:250' + const availableSizes = + (availableThumbList || defaultThumbList) + .split(' ') + .map(part => part.split(':')) + .map(([thumb, length]) => ({thumb, length: parseInt(length)})) + .sort((a, b) => a.length - b.length); + + return availableSizes; +} + +function getPreferredThumbSize(availableThumbList) { // Assuming a square, the image will be constrained to the lesser window // dimension. Coefficient here matches CSS dimensions for image overlay. const constrainedLength = Math.floor(Math.min( @@ -761,17 +785,30 @@ function getPreferredThumbSize() { // device configurations. const visualLength = window.devicePixelRatio * constrainedLength; - const largeLength = 800; - const semihugeLength = 1200; + const availableSizes = parseThumbList(availableThumbList); + + // Starting from the smallest dimensions, find (and return) the first + // available length which hits a "good enough" threshold - it's got to be + // at least that percent of the way to the actual displayed dimensions. const goodEnoughThreshold = 0.90; - if (Math.floor(visualLength * goodEnoughThreshold) <= largeLength) { - return 'large'; - } else if (Math.floor(visualLength * goodEnoughThreshold) <= semihugeLength) { - return 'semihuge'; - } else { - return 'huge'; + // (The last item is skipped since we'd be falling back to it anyway.) + for (const {thumb, length} of availableSizes.slice(0, -1)) { + if (Math.floor(visualLength * goodEnoughThreshold) <= length) { + return {thumb, length}; + } } + + // If none of the items in the list were big enough to hit the "good enough" + // threshold, just use the largest size available. + return availableSizes[availableSizes.length - 1]; +} + +function getSmallestThumbSize(availableThumbList) { + // Just snag the smallest size. This'll be used for displaying the "preview" + // as the bigger one is loading. + const availableSizes = parseThumbList(availableThumbList); + return availableSizes[0]; } function updateFileSizeInformation(fileSize) { -- cgit 1.3.0-6-gf8a5 From 2d1eaf5ea4c46527406df527f1b4d2fc36d8566e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 28 Aug 2023 14:45:38 -0300 Subject: thumbs: fix how magickThreads controlls queue --- src/gen-thumbs.js | 54 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index f6a8eaaf..51b2c72d 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -263,6 +263,8 @@ async function getSpawnMagick(tool) { return [`${description} (${version})`, fn]; } +// Note: This returns an array of no-argument functions, suitable for passing +// to queue(). function generateImageThumbnails({ filePath, dimensions, @@ -286,10 +288,10 @@ function generateImageThumbnails({ output(name), ]); - return Promise.all( + return ( getThumbnailsAvailableForDimensions(dimensions) .map(([name]) => [name, thumbnailSpec[name]]) - .map(([name, details]) => + .map(([name, details]) => () => promisifyProcess(convert('.' + name, details), false))); } @@ -527,35 +529,45 @@ export default async function genThumbs(mediaPath, { } const failed = []; - const succeeded = []; + const writeMessageFn = () => `Writing image thumbnails. [failed: ${failed.length}]`; + const generateCalls = + entriesToGenerate.flatMap(([filePath, md5]) => + generateImageThumbnails({ + filePath: path.join(mediaPath, filePath), + dimensions: imageToDimensions[filePath], + spawnConvert, + }).map(call => async () => { + try { + await call(); + } catch (error) { + failed.push([filePath, error]); + } + })); + await progressPromiseAll(writeMessageFn, - queue( - entriesToGenerate.map(([filePath, md5]) => () => - generateImageThumbnails({ - filePath: path.join(mediaPath, filePath), - dimensions: imageToDimensions[filePath], - spawnConvert, - }).then( - () => { - updatedCache[filePath] = [md5, ...imageToDimensions[filePath]]; - succeeded.push(filePath); - }, - error => { - failed.push([filePath, error]); - })), - magickThreads)); + queue(generateCalls, magickThreads)); + + // Sort by file path. + failed.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0); + + const failedFilePaths = new Set(failed.map(([filePath]) => filePath)); + + for (const [filePath, md5] of entriesToGenerate) { + if (failedFilePaths.has(filePath)) continue; + updatedCache[filePath] = [md5, ...imageToDimensions[filePath]]; + } if (empty(failed)) { logInfo`Generated all (updated) thumbnails successfully!`; } else { for (const [path, error] of failed) { - logError`Thumbnails failed to generate for ${path} - ${error}`; + logError`Thumbnail failed to generate for ${path} - ${error}`; } - logWarn`Result is incomplete - the above ${failed.length} thumbnails should be checked for errors.`; - logWarn`${succeeded.length} successfully generated images won't be regenerated next run, though!`; + logWarn`Result is incomplete - the above thumbnails should be checked for errors.`; + logWarn`Successfully generated images won't be regenerated next run, though!`; } try { -- cgit 1.3.0-6-gf8a5 From 75a7b56d3616d384b31757c9537a6e27f4e9b350 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 28 Aug 2023 14:46:06 -0300 Subject: upd8: accept and pass --magick-threads through properly --- src/upd8.js | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/upd8.js b/src/upd8.js index 2f08204a..2ec231c9 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -233,6 +233,12 @@ async function main() { 'magick-threads': { help: `Process more or fewer thumbnail files at once with ImageMagick when generating thumbnails. (Each ImageMagick thread may also make use of multi-core processing at its own utility.)`, + type: 'value', + validate(threads) { + if (parseInt(threads) !== parseFloat(threads)) return 'an integer'; + if (parseInt(threads) < 0) return 'a counting number or zero'; + return true; + } }, magick: {alias: 'magick-threads'}, -- cgit 1.3.0-6-gf8a5 From 7069268db096ab0aa7a8839a3594efb2d1be8f86 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 4 Sep 2023 20:46:58 -0300 Subject: thumbs: new check-has-thumbs util, others throw for missing info --- src/gen-thumbs.js | 16 ++++++++++++++++ src/write/bind-utilities.js | 5 +++++ 2 files changed, 21 insertions(+) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 51b2c72d..741cdff3 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -168,11 +168,23 @@ getThumbnailsAvailableForDimensions.all = .map(([name, {size}]) => [name, size]) .sort((a, b) => b[1] - a[1]); +export function checkIfImagePathHasCachedThumbnails(imagePath, cache) { + // Generic utility for checking if the thumbnail cache includes any info for + // the provided image path, so that the other functions don't hard-code the + // cache format. + + return !!cache[imagePath]; +} + export function getDimensionsOfImagePath(imagePath, cache) { // This function is really generic. It takes the gen-thumbs image cache and // returns the dimensions in that cache, so that other functions don't need // to hard-code the cache format. + if (!cache[imagePath]) { + throw new Error(`Expected imagePath to be included in cache, got ${imagePath}`); + } + const [width, height] = cache[imagePath].slice(1); return [width, height]; } @@ -185,6 +197,10 @@ export function getThumbnailEqualOrSmaller(preferred, imagePath, cache) { // one which is being thumbnail-ified, this just returns the name of the // selected thumbnail size. + if (!cache[imagePath]) { + throw new Error(`Expected imagePath to be included in cache, got ${imagePath}`); + } + const {size: preferredSize} = thumbnailSpec[preferred]; const [width, height] = getDimensionsOfImagePath(imagePath, cache); const available = getThumbnailsAvailableForDimensions([width, height]); diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index c32035f1..942cce89 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -11,6 +11,7 @@ import {bindOpts} from '#sugar'; import {thumb} from '#urls'; import { + checkIfImagePathHasCachedThumbnails, getDimensionsOfImagePath, getThumbnailEqualOrSmaller, getThumbnailsAvailableForDimensions, @@ -54,6 +55,10 @@ export function bindUtilities({ bound.find = bindFind(wikiData, {mode: 'warn'}); + bound.checkIfImagePathHasCachedThumbnails = + (imagePath) => + checkIfImagePathHasCachedThumbnails(imagePath, thumbsCache); + bound.getDimensionsOfImagePath = (imagePath) => getDimensionsOfImagePath(imagePath, thumbsCache); -- cgit 1.3.0-6-gf8a5 From 669f435f2b86133a8e096b22371ff9fbb1b703b7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 4 Sep 2023 20:51:49 -0300 Subject: content: image: defend against unavailable thumbnail info --- src/content/dependencies/image.js | 68 ++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 905b3c2e..b5591e6d 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -1,7 +1,9 @@ +import {logWarn} from '#cli'; import {empty} from '#sugar'; export default { extraDependencies: [ + 'checkIfImagePathHasCachedThumbnails', 'getDimensionsOfImagePath', 'getSizeOfImagePath', 'getThumbnailEqualOrSmaller', @@ -54,6 +56,7 @@ export default { }, generate(data, slots, { + checkIfImagePathHasCachedThumbnails, getDimensionsOfImagePath, getSizeOfImagePath, getThumbnailEqualOrSmaller, @@ -113,8 +116,27 @@ export default { .replace(/^\//, ''); } + const hasThumbnails = + mediaSrc && + checkIfImagePathHasCachedThumbnails(mediaSrc); + + // Warn for images that *should* have cached thumbnail information but are + // missing from the thumbs cache. + if ( + slots.thumb && + !hasThumbnails && + !mediaSrc.endsWith('.gif') + ) { + logWarn`No thumbnail info cached: ${mediaSrc} - displaying original image here (instead of ${slots.thumb})`; + } + + // Important to note that these might not be set at all, even if + // slots.thumb was provided. let thumbSrc = null; - if (mediaSrc) { + let availableThumbs = null; + let originalLength = null; + + if (hasThumbnails && slots.thumb) { // Note: This provides mediaSrc to getThumbnailEqualOrSmaller, since // it's the identifier which thumbnail utilities use to query from the // thumbnail cache. But we use the result to operate on originalSrc, @@ -122,16 +144,12 @@ export default { // another alternate base path. const selectedSize = getThumbnailEqualOrSmaller(slots.thumb, mediaSrc); thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${selectedSize}.jpg`); - } - let originalWidth = null; - let originalHeight = null; - let availableThumbs = null; - if (mediaSrc) { - [originalWidth, originalHeight] = - getDimensionsOfImagePath(mediaSrc); - availableThumbs = - getThumbnailsAvailableForDimensions([originalWidth, originalHeight]); + const dimensions = getDimensionsOfImagePath(mediaSrc); + availableThumbs = getThumbnailsAvailableForDimensions(dimensions); + + const [width, height] = dimensions; + originalLength = Math.max(width, height) } let fileSize = null; @@ -151,19 +169,23 @@ export default { imgAttributes['data-no-image-preview'] = true; } - if (fileSize) { - imgAttributes['data-original-size'] = fileSize; - } + // These attributes are only relevant when a thumbnail are available *and* + // being used. + if (hasThumbnails && slots.thumb) { + if (fileSize) { + imgAttributes['data-original-size'] = fileSize; + } - if (originalWidth && originalHeight) { - imgAttributes['data-original-length'] = Math.max(originalWidth, originalHeight); - } + if (originalLength) { + imgAttributes['data-original-length'] = originalLength; + } - if (!empty(availableThumbs)) { - imgAttributes['data-thumbs'] = - availableThumbs - .map(([name, size]) => `${name}:${size}`) - .join(' '); + if (!empty(availableThumbs)) { + imgAttributes['data-thumbs'] = + availableThumbs + .map(([name, size]) => `${name}:${size}`) + .join(' '); + } } const nonlazyHTML = @@ -171,7 +193,7 @@ export default { prepare( html.tag('img', { ...imgAttributes, - src: thumbSrc, + src: thumbSrc ?? originalSrc, })); if (slots.lazy) { @@ -182,7 +204,7 @@ export default { { ...imgAttributes, class: 'lazy', - 'data-original': thumbSrc, + 'data-original': thumbSrc ?? originalSrc, }), true), ]); -- cgit 1.3.0-6-gf8a5 From 63d9b8c2d455e2f74a837d8772fcfcf8e38e3a3e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 4 Sep 2023 20:52:28 -0300 Subject: client: defend client-side code against images without thumbs --- src/static/client2.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/static/client2.js b/src/static/client2.js index 0a8eb860..8ae9876e 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -717,18 +717,32 @@ function handleImageLinkClicked(evt) { updateFileSizeInformation(originalFileSize); - const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(availableThumbList); - const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(availableThumbList); - - const mainSrc = originalSrc.replace(/\.(jpg|png)$/, `.${mainThumb}.jpg`); - const thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${smallThumb}.jpg`); - - thumbImage.src = thumbSrc; + let mainSrc = null; + let thumbSrc = null; + + if (availableThumbList) { + const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(availableThumbList); + const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(availableThumbList); + mainSrc = originalSrc.replace(/\.(jpg|png)$/, `.${mainThumb}.jpg`); + thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${smallThumb}.jpg`); + // Show the thumbnail size on each <img> element's data attributes. + // Y'know, just for debugging convenience. + mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`; + thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`; + } else { + mainSrc = originalSrc; + thumbSrc = null; + mainImage.dataset.displayingThumb = ''; + thumbImage.dataset.displayingThumb = ''; + } - // Show the thumbnail size on each <img> element's data attributes. - // Y'know, just for debugging convenience. - mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`; - thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`; + if (thumbSrc) { + thumbImage.src = thumbSrc; + thumbImage.style.display = null; + } else { + thumbImage.src = ''; + thumbImage.style.display = 'none'; + } for (const viewOriginal of allViewOriginal) { viewOriginal.href = originalSrc; -- cgit 1.3.0-6-gf8a5 From 0ff743b1350b1d42ba23d9701a0b7acfb7501254 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 5 Sep 2023 20:32:13 -0300 Subject: content: linkTemplate: handle null href w/ hash cleanly --- src/content/dependencies/linkTemplate.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index 1cf64c59..ba7c7cda 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -29,14 +29,16 @@ export default { language, to, }) { - let href = slots.href; + let href; let style; let title; - if (href) { - href = encodeURI(href); + if (slots.href) { + href = encodeURI(slots.href); } else if (!empty(slots.path)) { href = to(...slots.path); + } else { + href = ''; } if (appendIndexHTML) { -- cgit 1.3.0-6-gf8a5 From 218a99a3164e8ae6967335190b72fd36275d1892 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 10:58:55 -0300 Subject: data, test: track: inherit album props more declaratively --- src/data/things/track.js | 225 ++++++++++++++++++++--------------------------- src/data/yaml.js | 6 +- 2 files changed, 98 insertions(+), 133 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index e176acb4..39c2930f 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -44,59 +44,58 @@ export class Track extends Thing { sampledTracksByRef: Thing.common.referenceList(Track), artTagsByRef: Thing.common.referenceList(ArtTag), - hasCoverArt: { + // 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: Thing.common.flag(), + + // File extension for track's corresponding media file. This represents the + // track's unique cover artwork, if any, and does not inherit the cover's + // main artwork. (It does inherit `trackCoverArtFileExtension` if present + // on the album.) + coverArtFileExtension: { flags: {update: true, expose: true}, - update: { - validate(value) { - if (value !== false) { - throw new TypeError(`Expected false or null`); - } + update: {validate: isFileExtension}, - return true; - }, - }, + expose: Track.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension'], { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - expose: { - dependencies: ['albumData', 'coverArtistContribsByRef'], - transform: (hasCoverArt, { - albumData, + transform(coverArtFileExtension, { coverArtistContribsByRef, - [Track.instance]: track, - }) => - Track.hasCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ), - }, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension}, + }) { + if (disableUniqueCoverArt) return null; + if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; + return coverArtFileExtension ?? trackCoverArtFileExtension ?? 'jpg'; + }, + }), }, - coverArtFileExtension: { + // Date of cover art release. Like coverArtFileExtension, this represents + // only the track's own unique cover artwork, if any. This exposes only as + // the track's own coverArtDate or its album's trackArtDate, so if neither + // is specified, this value is null. + coverArtDate: { flags: {update: true, expose: true}, - update: {validate: isFileExtension}, + update: {validate: isDate}, - expose: { - dependencies: ['albumData', 'coverArtistContribsByRef'], - transform: (coverArtFileExtension, { - albumData, + expose: Track.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef'], { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + + transform(coverArtDate, { coverArtistContribsByRef, - hasCoverArt, - [Track.instance]: track, - }) => - coverArtFileExtension ?? - (Track.hasCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ) - ? Track.findAlbum(track, albumData)?.trackCoverArtFileExtension - : Track.findAlbum(track, albumData)?.coverArtFileExtension) ?? - 'jpg', - }, + disableUniqueCoverArt, + album: {trackArtDate, trackCoverArtistContribsByRef}, + }) { + if (disableUniqueCoverArt) return null; + if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; + return coverArtDate ?? trackArtDate; + }, + }), }, originalReleaseTrackByRef: Thing.common.singleReference(Track), @@ -170,53 +169,29 @@ export class Track extends Thing { }, }, - coverArtDate: { - flags: {update: true, expose: true}, - - update: {validate: isDate}, - - expose: { - dependencies: [ - 'albumData', - 'coverArtistContribsByRef', - 'dateFirstReleased', - 'hasCoverArt', - ], - transform: (coverArtDate, { - albumData, - coverArtistContribsByRef, - dateFirstReleased, - hasCoverArt, - [Track.instance]: track, - }) => - (Track.hasCoverArt(track, albumData, coverArtistContribsByRef, hasCoverArt) - ? coverArtDate ?? - dateFirstReleased ?? - Track.findAlbum(track, albumData)?.trackArtDate ?? - Track.findAlbum(track, albumData)?.date ?? - null - : null), - }, - }, - + // Whether or not the track has "unique" cover artwork - a cover which is + // specifically associated with this track in particular, rather than with + // the track's album as a whole. This is typically used to select between + // displaying the track artwork and a fallback, such as the album artwork + // or a placeholder. (This property is named hasUniqueCoverArt instead of + // the usual hasCoverArt to emphasize that it does not inherit from the + // album.) hasUniqueCoverArt: { flags: {expose: true}, - expose: { - dependencies: ['albumData', 'coverArtistContribsByRef', 'hasCoverArt'], - compute: ({ - albumData, + expose: Track.withAlbumProperties(['trackCoverArtistContribsByRef'], { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + compute({ coverArtistContribsByRef, - hasCoverArt, - [Track.instance]: track, - }) => - Track.hasUniqueCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ), - }, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef}, + }) { + if (disableUniqueCoverArt) return false; + if (!empty(coverArtistContribsByRef)) true; + if (!empty(trackCoverArtistContribsByRef)) return true; + return false; + }, + }), }, originalReleaseTrack: Thing.common.dynamicThingFromSingleReference( @@ -342,53 +317,6 @@ export class Track extends Thing { ), }); - // This is a quick utility function for now, since the same code is reused in - // several places. Ideally it wouldn't be - we'd just reuse the `album` - // property - but support for that hasn't been coded yet :P - static findAlbum = (track, albumData) => - albumData?.find((album) => album.tracks.includes(track)); - - // Another reused utility function. This one's logic is a bit more complicated. - static hasCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ) { - if (!empty(coverArtistContribsByRef)) { - return true; - } - - const album = Track.findAlbum(track, albumData); - if (album && !empty(album.trackCoverArtistContribsByRef)) { - return true; - } - - return false; - } - - static hasUniqueCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ) { - if (!empty(coverArtistContribsByRef)) { - return true; - } - - if (hasCoverArt === false) { - return false; - } - - const album = Track.findAlbum(track, albumData); - if (album && !empty(album.trackCoverArtistContribsByRef)) { - return true; - } - - return false; - } - static inheritFromOriginalRelease( originalProperty, originalMissingValue, @@ -423,6 +351,39 @@ export class Track extends Thing { }; } + static withAlbumProperties(albumProperties, oldExpose) { + const applyAlbumDependency = dependencies => { + const track = dependencies[Track.instance]; + const album = + dependencies.albumData + ?.find((album) => album.tracks.includes(track)); + + const filteredAlbum = Object.create(null); + for (const property of albumProperties) { + filteredAlbum[property] = + (album + ? album[property] + : null); + } + + return {...dependencies, album: filteredAlbum}; + }; + + const newExpose = {dependencies: [...oldExpose.dependencies, 'albumData']}; + + if (oldExpose.compute) { + newExpose.compute = dependencies => + oldExpose.compute(applyAlbumDependency(dependencies)); + } + + if (oldExpose.transform) { + newExpose.transform = (value, dependencies) => + oldExpose.transform(value, applyAlbumDependency(dependencies)); + } + + return newExpose; + } + [inspect.custom]() { const base = Thing.prototype[inspect.custom].apply(this); diff --git a/src/data/yaml.js b/src/data/yaml.js index 35943199..13412f17 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -316,6 +316,10 @@ export const processTrackDocument = makeProcessDocument(T.Track, { 'Date First Released': (value) => new Date(value), 'Cover Art Date': (value) => new Date(value), + 'Has Cover Art': (value) => + (value === true ? false : + value === false ? true : + value), 'Artists': parseContributors, 'Contributors': parseContributors, @@ -336,7 +340,7 @@ export const processTrackDocument = makeProcessDocument(T.Track, { dateFirstReleased: 'Date First Released', coverArtDate: 'Cover Art Date', coverArtFileExtension: 'Cover Art File Extension', - hasCoverArt: 'Has Cover Art', + disableCoverArt: 'Has Cover Art', // This gets transformed to flip true/false. lyrics: 'Lyrics', commentary: 'Commentary', -- cgit 1.3.0-6-gf8a5 From 55e4afead38bc541cba4ae1cef183527c254f99a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 17:28:15 -0300 Subject: data: track: experimental Thing.compose.from() processing style --- src/data/things/thing.js | 142 ++++++++++++++++++++++- src/data/things/track.js | 290 ++++++++++++++++++++++++----------------------- 2 files changed, 289 insertions(+), 143 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c2876f56..143c1515 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty} from '#sugar'; +import {empty, openAggregate} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { @@ -418,4 +418,144 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } + + static findArtistsFromContribs(contribsByRef, artistData) { + return ( + contribsByRef + .map(({who, what}) => ({ + who: find.artist(who, artistData), + what, + })) + .filter(({who}) => who)); + } + + static composite = { + from(composition) { + const base = composition.at(-1); + const steps = composition.slice(0, -1); + + const aggregate = openAggregate({message: `Errors preparing Thing.composite.from() composition`}); + + if (base.flags.compose) { + aggregate.push(new TypeError(`Base (bottom item) must not be {compose: true}`)); + } + + const exposeFunctionOrder = []; + const exposeDependencies = new Set(base.expose?.dependencies); + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const message = + (step.annotation + ? `Errors in step #${i + 1} (${step.annotation})` + : `Errors in step #${i + 1}`); + + aggregate.nest({message}, ({push}) => { + if (!step.flags.compose) { + push(new TypeError(`Steps (all but bottom item) must be {compose: true}`)); + } + + if (step.flags.update) { + push(new Error(`Steps which update aren't supported yet`)); + } + + if (step.flags.expose) expose: { + if (!step.expose.transform && !step.expose.compute) { + push(new TypeError(`Steps which expose must provide at least one of transform or compute`)); + break expose; + } + + if (step.expose.dependencies) { + for (const dependency of step.expose.dependencies) { + exposeDependencies.add(dependency); + } + } + + if (base.flags.update) { + if (step.expose.transform) { + exposeFunctionOrder.push({type: 'transform', fn: step.expose.transform}); + } else { + exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + } + } else { + if (step.expose.transform && !step.expose.compute) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + break expose; + } + + exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + } + } + }); + } + + aggregate.close(); + + const constructedDescriptor = {}; + + constructedDescriptor.flags = { + update: !!base.flags.update, + expose: !!base.flags.expose, + compose: false, + }; + + if (base.flags.update) { + constructedDescriptor.update = base.flags.update; + } + + if (base.flags.expose) { + const expose = constructedDescriptor.expose = {}; + expose.dependencies = Array.from(exposeDependencies); + + const continuationSymbol = Symbol(); + + if (base.flags.update) { + expose.transform = (value, initialDependencies) => { + const dependencies = {...initialDependencies}; + let valueSoFar = value; + + for (const {type, fn} of exposeFunctionOrder) { + const result = + (type === 'transform' + ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { + valueSoFar = updatedValue; + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + }) + : fn(dependencies, providedDependencies => { + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + })); + + if (result !== continuationSymbol) { + return result; + } + } + + return base.expose.transform(valueSoFar, dependencies); + }; + } else { + expose.compute = (initialDependencies) => { + const dependencies = {...initialDependencies}; + + for (const {fn} of exposeFunctionOrder) { + const result = + fn(valueSoFar, dependencies, providedDependencies => { + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + }); + + if (result !== continuationSymbol) { + return result; + } + } + + return base.expose.compute(dependencies); + }; + } + } + + return constructedDescriptor; + }, + }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 39c2930f..fe6af205 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -54,49 +54,53 @@ export class Track extends Thing { // track's unique cover artwork, if any, and does not inherit the cover's // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) - coverArtFileExtension: { - flags: {update: true, expose: true}, - - update: {validate: isFileExtension}, - - expose: Track.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension'], { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - - transform(coverArtFileExtension, { - coverArtistContribsByRef, - disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension}, - }) { - if (disableUniqueCoverArt) return null; - if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; - return coverArtFileExtension ?? trackCoverArtFileExtension ?? 'jpg'; + coverArtFileExtension: Thing.composite.from([ + Track.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), + + { + flags: {update: true, expos: true}, + update: {validate: isFileExtension}, + expose: { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + + transform(coverArtFileExtension, { + coverArtistContribsByRef, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension}, + }) { + if (disableUniqueCoverArt) return null; + if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; + return coverArtFileExtension ?? trackCoverArtFileExtension ?? 'jpg'; + }, }, - }), - }, + }, + ]), // Date of cover art release. Like coverArtFileExtension, this represents // only the track's own unique cover artwork, if any. This exposes only as // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. - coverArtDate: { - flags: {update: true, expose: true}, - - update: {validate: isDate}, - - expose: Track.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef'], { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - - transform(coverArtDate, { - coverArtistContribsByRef, - disableUniqueCoverArt, - album: {trackArtDate, trackCoverArtistContribsByRef}, - }) { - if (disableUniqueCoverArt) return null; - if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; - return coverArtDate ?? trackArtDate; + coverArtDate: Thing.composite.from([ + Track.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']), + + { + flags: {update: true, expose: true}, + update: {validate: isDate}, + expose: { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + + transform(coverArtDate, { + coverArtistContribsByRef, + disableUniqueCoverArt, + album: {trackArtDate, trackCoverArtistContribsByRef}, + }) { + if (disableUniqueCoverArt) return null; + if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; + return coverArtDate ?? trackArtDate; + }, }, - }), - }, + } + ]), originalReleaseTrackByRef: Thing.common.singleReference(Track), @@ -176,23 +180,26 @@ export class Track extends Thing { // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) - hasUniqueCoverArt: { - flags: {expose: true}, - - expose: Track.withAlbumProperties(['trackCoverArtistContribsByRef'], { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - compute({ - coverArtistContribsByRef, - disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef}, - }) { - if (disableUniqueCoverArt) return false; - if (!empty(coverArtistContribsByRef)) true; - if (!empty(trackCoverArtistContribsByRef)) return true; - return false; + hasUniqueCoverArt: Thing.composite.from([ + Track.withAlbumProperties(['trackCoverArtistContribsByRef']), + + { + flags: {expose: true}, + expose: { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + compute({ + coverArtistContribsByRef, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef}, + }) { + if (disableUniqueCoverArt) return false; + if (!empty(coverArtistContribsByRef)) true; + if (!empty(trackCoverArtistContribsByRef)) return true; + return false; + }, }, - }), - }, + }, + ]), originalReleaseTrack: Thing.common.dynamicThingFromSingleReference( 'originalReleaseTrackByRef', @@ -228,43 +235,70 @@ export class Track extends Thing { }, }, - artistContribs: - Track.inheritFromOriginalRelease('artistContribs', [], - Thing.common.dynamicInheritContribs( - null, - 'artistContribsByRef', - 'artistContribsByRef', - 'albumData', - Track.findAlbum)), + artistContribs: Thing.composite.from([ + Track.inheritFromOriginalRelease('artistContribs'), + + { + flags: {expose: true}, + expose: { + dependencies: ['artistContribs'], + + compute({ + artistContribsByRef: contribsFromTrack, + album: {artistContribsByRef: contribsFromAlbum}, + }) { + let contribsByRef = contribsFromTrack; + if (empty(contribsByRef)) contribsByRef = contribsFromAlbum; + if (empty(contribsByRef)) return null; - contributorContribs: - Track.inheritFromOriginalRelease('contributorContribs', [], - Thing.common.dynamicContribs('contributorContribsByRef')), + return Thing.findArtistsFromContribs(contribsByRef, artistData); + }, + }, + }, + ]), + + contributorContribs: Thing.composite.from([ + Track.inheritFromOriginalRelease('contributorContribs'), + Thing.common.dynamicContribs('contributorContribsByRef'), + ]), // Cover artists aren't inherited from the original release, since it // typically varies by release and isn't defined by the musical qualities // of the track. - coverArtistContribs: - Thing.common.dynamicInheritContribs( - 'hasCoverArt', - 'coverArtistContribsByRef', - 'trackCoverArtistContribsByRef', - 'albumData', - Track.findAlbum), - - referencedTracks: - Track.inheritFromOriginalRelease('referencedTracks', [], - Thing.common.dynamicThingsFromReferenceList( - 'referencedTracksByRef', - 'trackData', - find.track)), - - sampledTracks: - Track.inheritFromOriginalRelease('sampledTracks', [], - Thing.common.dynamicThingsFromReferenceList( - 'sampledTracksByRef', - 'trackData', - find.track)), + coverArtistContribs: Thing.composite.from([ + Track.withAlbumProperties(['trackCoverArtistContribsByRef']), + + { + flags: {expose: true}, + expose: { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + + compute({ + coverArtistContribsByRef: contribsFromTrack, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef: contribsFromAlbum}, + }) { + if (disableUniqueCoverArt) return null; + + let contribsByRef = contribsFromTrack; + if (empty(contribsByRef)) contribsByRef = contribsFromAlbum; + if (empty(contribsByRef)) return null; + + return Thing.findArtistsFromContribs(contribsByRef, artistData); + }, + }, + }, + ]), + + referencedTracks: Thing.composite.from([ + Track.inheritFromOriginalRelease('referencedTracks'), + Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), + ]), + + sampledTracks: Thing.composite.from([ + Track.inheritFromOriginalRelease('sampledTracks'), + Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), + ]), // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't @@ -317,72 +351,44 @@ export class Track extends Thing { ), }); - static inheritFromOriginalRelease( - originalProperty, - originalMissingValue, - ownPropertyDescriptor - ) { - return { - flags: {expose: true}, + static inheritFromOriginalRelease = originalProperty => ({ + flags: {expose: true, compose: true}, - expose: { - dependencies: [ - ...ownPropertyDescriptor.expose.dependencies, - 'originalReleaseTrackByRef', - 'trackData', - ], - - compute(dependencies) { - const { - originalReleaseTrackByRef, - trackData, - } = dependencies; - - if (originalReleaseTrackByRef) { - if (!trackData) return originalMissingValue; - const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); - if (!original) return originalMissingValue; - return original[originalProperty]; - } + expose: { + dependencies: ['originalReleaseTrackByRef', 'trackData'], - return ownPropertyDescriptor.expose.compute(dependencies); - }, - }, - }; - } + compute({originalReleaseTrackByRef, trackData}, callback) { + if (!originalReleaseTrackByRef) return callback(); - static withAlbumProperties(albumProperties, oldExpose) { - const applyAlbumDependency = dependencies => { - const track = dependencies[Track.instance]; - const album = - dependencies.albumData - ?.find((album) => album.tracks.includes(track)); - - const filteredAlbum = Object.create(null); - for (const property of albumProperties) { - filteredAlbum[property] = - (album - ? album[property] - : null); - } + if (!trackData) return null; + const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); + if (!original) return null; + return original[originalProperty]; + }, + }, + }); - return {...dependencies, album: filteredAlbum}; - }; + static withAlbumProperties = albumProperties => ({ + flags: {expose: true, compose: true}, - const newExpose = {dependencies: [...oldExpose.dependencies, 'albumData']}; + expose: { + dependencies: ['albumData'], - if (oldExpose.compute) { - newExpose.compute = dependencies => - oldExpose.compute(applyAlbumDependency(dependencies)); - } + compute({albumData, [Track.instance]: track}, callback) { + const album = albumData?.find((album) => album.tracks.includes(track)); - if (oldExpose.transform) { - newExpose.transform = (value, dependencies) => - oldExpose.transform(value, applyAlbumDependency(dependencies)); - } + const filteredAlbum = Object.create(null); + for (const property of albumProperties) { + filteredAlbum[property] = + (album + ? album[property] + : null); + } - return newExpose; - } + return callback({album: filteredAlbum}); + }, + }, + }); [inspect.custom]() { const base = Thing.prototype[inspect.custom].apply(this); -- cgit 1.3.0-6-gf8a5 From 9e23a5a9eff30af0d7c8e356520dec791aebd38f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 21:15:42 -0300 Subject: content, data: be more guarded about track contribs arrays --- src/content/dependencies/generateAlbumInfoPage.js | 2 +- src/content/dependencies/generateAlbumTrackListItem.js | 4 +++- src/content/dependencies/generateTrackInfoPage.js | 7 +++++-- src/data/things/artist.js | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index ce17ab21..51ea5927 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -44,7 +44,7 @@ export default { relations.coverArtistChronologyContributions = getChronologyRelations(album, { - contributions: album.coverArtistContribs, + contributions: album.coverArtistContribs ?? [], linkArtist: artist => relation('linkArtist', artist), diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js index f65b47c9..8b443baf 100644 --- a/src/content/dependencies/generateAlbumTrackListItem.js +++ b/src/content/dependencies/generateAlbumTrackListItem.js @@ -1,4 +1,4 @@ -import {compareArrays} from '#sugar'; +import {compareArrays, empty} from '#sugar'; export default { contentDependencies: [ @@ -31,6 +31,8 @@ export default { } data.showArtists = + empty(track.artistContribs) || + empty(album.artistContribs) || !compareArrays( track.artistContribs.map(c => c.who), album.artistContribs.map(c => c.who), diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 334c5422..7002204c 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -51,7 +51,10 @@ export default { relations.artistChronologyContributions = getChronologyRelations(track, { - contributions: [...track.artistContribs, ...track.contributorContribs], + contributions: [ + ...track.artistContribs ?? [], + ...track.contributorContribs ?? [], + ], linkArtist: artist => relation('linkArtist', artist), linkThing: track => relation('linkTrack', track), @@ -65,7 +68,7 @@ export default { relations.coverArtistChronologyContributions = getChronologyRelations(track, { - contributions: track.coverArtistContribs, + contributions: track.coverArtistContribs ?? [], linkArtist: artist => relation('linkArtist', artist), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 522ca5f9..43628b6b 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -156,7 +156,7 @@ export class Artist extends Thing { }) => thingData?.filter(thing => thing[contribsProperty] - .some(contrib => contrib.who === artist)) ?? [], + ?.some(contrib => contrib.who === artist)) ?? [], }, }); } -- cgit 1.3.0-6-gf8a5 From 128c47001a639d1569bdfadf783ccede22116350 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 21:17:02 -0300 Subject: data: fix compute() bugs in Thing.composite.from() --- src/data/things/thing.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 143c1515..111de550 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -420,6 +420,8 @@ export default class Thing extends CacheableObject { } static findArtistsFromContribs(contribsByRef, artistData) { + if (empty(contribsByRef)) return null; + return ( contribsByRef .map(({who, what}) => ({ @@ -518,7 +520,7 @@ export default class Thing extends CacheableObject { const result = (type === 'transform' ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { - valueSoFar = updatedValue; + valueSoFar = updatedValue ?? null; Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }) @@ -532,7 +534,11 @@ export default class Thing extends CacheableObject { } } - return base.expose.transform(valueSoFar, dependencies); + if (base.expose.transform) { + return base.expose.transform(valueSoFar, dependencies); + } else { + return base.expose.compute(dependencies); + } }; } else { expose.compute = (initialDependencies) => { @@ -540,7 +546,7 @@ export default class Thing extends CacheableObject { for (const {fn} of exposeFunctionOrder) { const result = - fn(valueSoFar, dependencies, providedDependencies => { + fn(dependencies, providedDependencies => { Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }); -- cgit 1.3.0-6-gf8a5 From d4af649bfdd546bd87b1a440bdba8152d010937e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 21:17:38 -0300 Subject: yaml: fix disableCoverArt -> disableUniqueCoverArt --- src/data/yaml.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index 13412f17..25eda3c5 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -340,7 +340,7 @@ export const processTrackDocument = makeProcessDocument(T.Track, { dateFirstReleased: 'Date First Released', coverArtDate: 'Cover Art Date', coverArtFileExtension: 'Cover Art File Extension', - disableCoverArt: 'Has Cover Art', // This gets transformed to flip true/false. + disableUniqueCoverArt: 'Has Cover Art', // This gets transformed to flip true/false. lyrics: 'Lyrics', commentary: 'Commentary', -- cgit 1.3.0-6-gf8a5 From 0fd10f2997db8ddec95e3caff94343eafdd9dda1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 21:18:23 -0300 Subject: data: track: more composite shenanigans --- src/data/things/thing.js | 22 ++++-- src/data/things/track.js | 186 ++++++++++++++++++++++++++--------------------- 2 files changed, 116 insertions(+), 92 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 111de550..5d14b296 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -250,14 +250,7 @@ export default class Thing extends CacheableObject { expose: { dependencies: ['artistData', contribsByRefProperty], compute: ({artistData, [contribsByRefProperty]: contribsByRef}) => - contribsByRef && artistData - ? contribsByRef - .map(({who: ref, what}) => ({ - who: find.artist(ref, artistData), - what, - })) - .filter(({who}) => who) - : [], + Thing.findArtistsFromContribs(contribsByRef, artistData), }, }), @@ -563,5 +556,18 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + + withDynamicContribs: (contribsByRefProperty, dependencyName) => ({ + flags: {expose: true, compose: true}, + + expose: { + dependencies: ['artistData', contribsByRefProperty], + compute: ({artistData, [contribsByRefProperty]: contribsByRef}, callback) => + callback({ + [dependencyName]: + Thing.findArtistsFromContribs(contribsByRef, artistData), + }), + }, + }), }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index fe6af205..8aa7ba26 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -44,6 +44,26 @@ export class Track extends Thing { sampledTracksByRef: Thing.common.referenceList(Track), artTagsByRef: Thing.common.referenceList(ArtTag), + color: Thing.composite.from([ + { + flags: {expose: true, compose: true}, + expose: { + transform: (color, {}, continuation) => + color ?? continuation(), + }, + }, + + Track.composite.withAlbumProperties(['color']), + + { + flags: {update: true, expose: true}, + update: {validate: isColor}, + expose: { + compute: ({album: {color}}) => color, + }, + }, + ]), + // 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 @@ -55,7 +75,7 @@ export class Track extends Thing { // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) coverArtFileExtension: Thing.composite.from([ - Track.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), + Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), { flags: {update: true, expos: true}, @@ -81,7 +101,7 @@ export class Track extends Thing { // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. coverArtDate: Thing.composite.from([ - Track.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']), + Track.composite.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']), { flags: {update: true, expose: true}, @@ -147,31 +167,25 @@ export class Track extends Thing { find.album ), - date: { - flags: {expose: true}, - - expose: { - dependencies: ['albumData', 'dateFirstReleased'], - compute: ({albumData, dateFirstReleased, [Track.instance]: track}) => - dateFirstReleased ?? Track.findAlbum(track, albumData)?.date ?? null, + date: Thing.composite.from([ + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['dateFirstReleased'], + compute: ({dateFirstReleased}, continuation) => + dateFirstReleased ?? continuation(), + }, }, - }, - color: { - flags: {update: true, expose: true}, - - update: {validate: isColor}, + Track.composite.withAlbumProperties(['date']), - expose: { - dependencies: ['albumData'], - - transform: (color, {albumData, [Track.instance]: track}) => - color ?? - Track.findAlbum(track, albumData) - ?.trackSections.find(({tracks}) => tracks.includes(track)) - ?.color ?? null, + { + flags: {expose: true}, + expose: { + compute: ({album: {date}}) => date, + }, }, - }, + ]), // Whether or not the track has "unique" cover artwork - a cover which is // specifically associated with this track in particular, rather than with @@ -181,7 +195,7 @@ export class Track extends Thing { // the usual hasCoverArt to emphasize that it does not inherit from the // album.) hasUniqueCoverArt: Thing.composite.from([ - Track.withAlbumProperties(['trackCoverArtistContribsByRef']), + Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef']), { flags: {expose: true}, @@ -236,29 +250,27 @@ export class Track extends Thing { }, artistContribs: Thing.composite.from([ - Track.inheritFromOriginalRelease('artistContribs'), + Track.composite.inheritFromOriginalRelease('artistContribs'), + + Thing.composite.withDynamicContribs('artistContribsByRef', 'artistContribs'), + Track.composite.withAlbumProperties(['artistContribs']), { flags: {expose: true}, expose: { - dependencies: ['artistContribs'], - - compute({ - artistContribsByRef: contribsFromTrack, - album: {artistContribsByRef: contribsFromAlbum}, - }) { - let contribsByRef = contribsFromTrack; - if (empty(contribsByRef)) contribsByRef = contribsFromAlbum; - if (empty(contribsByRef)) return null; - - return Thing.findArtistsFromContribs(contribsByRef, artistData); - }, + compute: ({ + artistContribs: contribsFromTrack, + album: {artistContribs: contribsFromAlbum}, + }) => + (empty(contribsFromTrack) + ? contribsFromAlbum + : contribsFromTrack), }, }, ]), contributorContribs: Thing.composite.from([ - Track.inheritFromOriginalRelease('contributorContribs'), + Track.composite.inheritFromOriginalRelease('contributorContribs'), Thing.common.dynamicContribs('contributorContribsByRef'), ]), @@ -266,37 +278,41 @@ export class Track extends Thing { // typically varies by release and isn't defined by the musical qualities // of the track. coverArtistContribs: Thing.composite.from([ - Track.withAlbumProperties(['trackCoverArtistContribsByRef']), - { - flags: {expose: true}, + flags: {expose: true, compose: true}, expose: { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - - compute({ - coverArtistContribsByRef: contribsFromTrack, - disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef: contribsFromAlbum}, - }) { - if (disableUniqueCoverArt) return null; + dependencies: ['disableUniqueCoverArt'], + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? null + : continuation()), + }, + }, - let contribsByRef = contribsFromTrack; - if (empty(contribsByRef)) contribsByRef = contribsFromAlbum; - if (empty(contribsByRef)) return null; + Track.composite.withAlbumProperties(['trackCoverArtistContribs']), + Thing.composite.withDynamicContribs('coverArtistContribsByRef', 'coverArtistContribs'), - return Thing.findArtistsFromContribs(contribsByRef, artistData); - }, + { + flags: {expose: true}, + expose: { + compute: ({ + coverArtistContribs: contribsFromTrack, + album: {trackCoverArtistContribs: contribsFromAlbum}, + }) => + (empty(contribsFromTrack) + ? contribsFromAlbum + : contribsFromTrack), }, }, ]), referencedTracks: Thing.composite.from([ - Track.inheritFromOriginalRelease('referencedTracks'), + Track.composite.inheritFromOriginalRelease('referencedTracks'), Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), ]), sampledTracks: Thing.composite.from([ - Track.inheritFromOriginalRelease('sampledTracks'), + Track.composite.inheritFromOriginalRelease('sampledTracks'), Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), ]), @@ -351,44 +367,46 @@ export class Track extends Thing { ), }); - static inheritFromOriginalRelease = originalProperty => ({ - flags: {expose: true, compose: true}, + static composite = { + inheritFromOriginalRelease: originalProperty => ({ + flags: {expose: true, compose: true}, - expose: { - dependencies: ['originalReleaseTrackByRef', 'trackData'], + expose: { + dependencies: ['originalReleaseTrackByRef', 'trackData'], - compute({originalReleaseTrackByRef, trackData}, callback) { - if (!originalReleaseTrackByRef) return callback(); + compute({originalReleaseTrackByRef, trackData}, continuation) { + if (!originalReleaseTrackByRef) return continuation(); - if (!trackData) return null; - const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); - if (!original) return null; - return original[originalProperty]; + if (!trackData) return null; + const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); + if (!original) return null; + return original[originalProperty]; + }, }, - }, - }); + }), - static withAlbumProperties = albumProperties => ({ - flags: {expose: true, compose: true}, + withAlbumProperties: albumProperties => ({ + flags: {expose: true, compose: true}, - expose: { - dependencies: ['albumData'], + expose: { + dependencies: ['albumData'], - compute({albumData, [Track.instance]: track}, callback) { - const album = albumData?.find((album) => album.tracks.includes(track)); + compute({albumData, [Track.instance]: track}, continuation) { + const album = albumData?.find((album) => album.tracks.includes(track)); - const filteredAlbum = Object.create(null); - for (const property of albumProperties) { - filteredAlbum[property] = - (album - ? album[property] - : null); - } + const filteredAlbum = Object.create(null); + for (const property of albumProperties) { + filteredAlbum[property] = + (album + ? album[property] + : null); + } - return callback({album: filteredAlbum}); + return continuation({album: filteredAlbum}); + }, }, - }, - }); + }), + }; [inspect.custom]() { const base = Thing.prototype[inspect.custom].apply(this); -- cgit 1.3.0-6-gf8a5 From 8f8361c7c45b02a2221c01acc492ba4d3ae1c42e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 21:50:25 -0300 Subject: data: 2x facepalm combobob --- src/data/things/track.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 8aa7ba26..30c6fe58 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -78,7 +78,7 @@ export class Track extends Thing { Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), { - flags: {update: true, expos: true}, + flags: {update: true, expose: true}, update: {validate: isFileExtension}, expose: { dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], @@ -207,7 +207,7 @@ export class Track extends Thing { album: {trackCoverArtistContribsByRef}, }) { if (disableUniqueCoverArt) return false; - if (!empty(coverArtistContribsByRef)) true; + if (!empty(coverArtistContribsByRef)) return true; if (!empty(trackCoverArtistContribsByRef)) return true; return false; }, -- cgit 1.3.0-6-gf8a5 From 93448ef747b681d3b87b050b555311c0172b83cc Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 21 Aug 2023 22:12:24 -0300 Subject: content, data: be even more guarded about contrib arrays --- .../dependencies/generateAlbumTrackListItem.js | 20 ++++---- src/content/dependencies/generateTrackList.js | 59 +++++++++++++--------- src/data/things/artist.js | 6 +-- 3 files changed, 48 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js index 8b443baf..f92712f9 100644 --- a/src/content/dependencies/generateAlbumTrackListItem.js +++ b/src/content/dependencies/generateAlbumTrackListItem.js @@ -11,9 +11,11 @@ export default { relations(relation, track) { const relations = {}; - relations.contributionLinks = - track.artistContribs - .map(contrib => relation('linkContribution', contrib)); + if (!empty(track.artistContribs)) { + relations.contributionLinks = + track.artistContribs + .map(contrib => relation('linkContribution', contrib)); + } relations.trackLink = relation('linkTrack', track); @@ -31,12 +33,12 @@ export default { } data.showArtists = - empty(track.artistContribs) || - empty(album.artistContribs) || - !compareArrays( - track.artistContribs.map(c => c.who), - album.artistContribs.map(c => c.who), - {checkOrder: false}); + !empty(track.artistContribs) && + (empty(album.artistContribs) || + !compareArrays( + track.artistContribs.map(c => c.who), + album.artistContribs.map(c => c.who), + {checkOrder: false})); return data; }, diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js index f001c3b3..65f5552b 100644 --- a/src/content/dependencies/generateTrackList.js +++ b/src/content/dependencies/generateTrackList.js @@ -1,4 +1,4 @@ -import {empty} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; export default { contentDependencies: ['linkTrack', 'linkContribution'], @@ -11,14 +11,17 @@ export default { } return { - items: tracks.map(track => ({ - trackLink: - relation('linkTrack', track), + trackLinks: + tracks + .map(track => relation('linkTrack', track)), - contributionLinks: - track.artistContribs - .map(contrib => relation('linkContribution', contrib)), - })), + contributionLinks: + tracks + .map(track => + (empty(track.artistContribs) + ? null + : track.artistContribs + .map(contrib => relation('linkContribution', contrib)))), }; }, @@ -28,22 +31,28 @@ export default { }, generate(relations, slots, {html, language}) { - return html.tag('ul', - relations.items.map(({trackLink, contributionLinks}) => - html.tag('li', - language.$('trackList.item.withArtists', { - track: trackLink, - by: - html.tag('span', {class: 'by'}, - language.$('trackList.item.withArtists.by', { - artists: - language.formatConjunctionList( - contributionLinks.map(link => - link.slots({ - showContribution: slots.showContribution, - showIcons: slots.showIcons, - }))), - })), - })))); + return ( + html.tag('ul', + stitchArrays({ + trackLink: relations.trackLinks, + contributionLinks: relations.contributionLinks, + }).map(({trackLink, contributionLinks}) => + html.tag('li', + (empty(contributionLinks) + ? trackLink + : language.$('trackList.item.withArtists', { + track: trackLink, + by: + html.tag('span', {class: 'by'}, + language.$('trackList.item.withArtists.by', { + artists: + language.formatConjunctionList( + contributionLinks.map(link => + link.slots({ + showContribution: slots.showContribution, + showIcons: slots.showIcons, + }))), + })), + })))))); }, }; diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 43628b6b..4f157bc6 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -71,9 +71,9 @@ export class Artist extends Thing { compute: ({trackData, [Artist.instance]: artist}) => trackData?.filter((track) => [ - ...track.artistContribs, - ...track.contributorContribs, - ...track.coverArtistContribs, + ...track.artistContribs ?? [], + ...track.contributorContribs ?? [], + ...track.coverArtistContribs ?? [], ].some(({who}) => who === artist)) ?? [], }, }, -- cgit 1.3.0-6-gf8a5 From 75691866ed68b9261dd920b79d4ab214df3f049b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 22 Aug 2023 13:02:19 -0300 Subject: data: filter only requested deps, require requesting 'this' * Thing.composite.from() only provides the dependencies specified in each step and the base, and prevents '#'-prefixed keys from being specified on the main (composite) dependency list. * CacheableObject no longer provides a "reflection" dependency to every compute/transform function, and now requires the property 'this' to be specified instead of the constructor.instance symbol. (The static CacheableObject.instance, inherited by all subclasses, was also removed.) * Also minor improvements to sugar.js data processing utility functions. --- src/data/things/art-tag.js | 4 +- src/data/things/artist.js | 16 +++---- src/data/things/cacheable-object.js | 33 ++++++++------ src/data/things/flash.js | 8 ++-- src/data/things/group.js | 13 +++--- src/data/things/thing.js | 48 +++++++++++++++------ src/data/things/track.js | 85 ++++++++++++++++++++++++------------- src/util/sugar.js | 24 ++++++++--- 8 files changed, 147 insertions(+), 84 deletions(-) (limited to 'src') diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index c103c4d5..bb36e09e 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -37,8 +37,8 @@ export class ArtTag extends Thing { flags: {expose: true}, expose: { - dependencies: ['albumData', 'trackData'], - compute: ({albumData, trackData, [ArtTag.instance]: artTag}) => + dependencies: ['this', 'albumData', 'trackData'], + compute: ({this: artTag, albumData, trackData}) => sortAlbumsTracksChronologically( [...albumData, ...trackData] .filter(({artTags}) => artTags.includes(artTag)), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 4f157bc6..bde84cfa 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -66,9 +66,9 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: ['trackData'], + dependencies: ['this', 'trackData'], - compute: ({trackData, [Artist.instance]: artist}) => + compute: ({this: artist, trackData}) => trackData?.filter((track) => [ ...track.artistContribs ?? [], @@ -82,9 +82,9 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: ['trackData'], + dependencies: ['this', 'trackData'], - compute: ({trackData, [Artist.instance]: artist}) => + compute: ({this: artist, trackData}) => trackData?.filter(({commentatorArtists}) => commentatorArtists.includes(artist)) ?? [], }, @@ -103,9 +103,9 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: ['albumData'], + dependencies: [this, 'albumData'], - compute: ({albumData, [Artist.instance]: artist}) => + compute: ({this: artist, albumData}) => albumData?.filter(({commentatorArtists}) => commentatorArtists.includes(artist)) ?? [], }, @@ -148,11 +148,11 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: [thingDataProperty], + dependencies: ['this', thingDataProperty], compute: ({ + this: artist, [thingDataProperty]: thingData, - [Artist.instance]: artist }) => thingData?.filter(thing => thing[contribsProperty] diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index ea705a61..24a6cf01 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -83,8 +83,6 @@ function inspect(value) { } export default class CacheableObject { - static instance = Symbol('CacheableObject `this` instance'); - #propertyUpdateValues = Object.create(null); #propertyUpdateCacheInvalidators = Object.create(null); @@ -250,20 +248,27 @@ export default class CacheableObject { let getAllDependencies; - const dependencyKeys = expose.dependencies; - if (dependencyKeys?.length > 0) { - const reflectionEntry = [this.constructor.instance, this]; - const dependencyGetters = dependencyKeys - .map(key => () => [key, this.#propertyUpdateValues[key]]); + if (expose.dependencies?.length > 0) { + const dependencyKeys = expose.dependencies.slice(); + const shouldReflect = dependencyKeys.includes('this'); + + getAllDependencies = () => { + const dependencies = Object.create(null); + + for (const key of dependencyKeys) { + dependencies[key] = this.#propertyUpdateValues[key]; + } - getAllDependencies = () => - Object.fromEntries(dependencyGetters - .map(f => f()) - .concat([reflectionEntry])); + if (shouldReflect) { + dependencies.this = this; + } + + return dependencies; + }; } else { - const allDependencies = {[this.constructor.instance]: this}; - Object.freeze(allDependencies); - getAllDependencies = () => allDependencies; + const dependencies = Object.create(null); + Object.freeze(dependencies); + getAllDependencies = () => dependencies; } if (flags.update) { diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 6eb5234f..445fd07c 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -77,9 +77,9 @@ export class Flash extends Thing { flags: {expose: true}, expose: { - dependencies: ['flashActData'], + dependencies: ['this', 'flashActData'], - compute: ({flashActData, [Flash.instance]: flash}) => + compute: ({this: flash, flashActData}) => flashActData.find((act) => act.flashes.includes(flash)) ?? null, }, }, @@ -88,9 +88,9 @@ export class Flash extends Thing { flags: {expose: true}, expose: { - dependencies: ['flashActData'], + dependencies: ['this', 'flashActData'], - compute: ({flashActData, [Flash.instance]: flash}) => + compute: ({this: flash, flashActData}) => flashActData.find((act) => act.flashes.includes(flash))?.color ?? null, }, }, diff --git a/src/data/things/group.js b/src/data/things/group.js index ba339b3e..f552b8f3 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -41,8 +41,8 @@ export class Group extends Thing { flags: {expose: true}, expose: { - dependencies: ['albumData'], - compute: ({albumData, [Group.instance]: group}) => + dependencies: ['this', 'albumData'], + compute: ({this: group, albumData}) => albumData?.filter((album) => album.groups.includes(group)) ?? [], }, }, @@ -51,9 +51,8 @@ export class Group extends Thing { flags: {expose: true}, expose: { - dependencies: ['groupCategoryData'], - - compute: ({groupCategoryData, [Group.instance]: group}) => + dependencies: ['this', 'groupCategoryData'], + compute: ({this: group, groupCategoryData}) => groupCategoryData.find((category) => category.groups.includes(group)) ?.color, }, @@ -63,8 +62,8 @@ export class Group extends Thing { flags: {expose: true}, expose: { - dependencies: ['groupCategoryData'], - compute: ({groupCategoryData, [Group.instance]: group}) => + dependencies: ['this', 'groupCategoryData'], + compute: ({this: group, groupCategoryData}) => groupCategoryData.find((category) => category.groups.includes(group)) ?? null, }, diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5d14b296..bc10e06b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty, openAggregate} from '#sugar'; +import {empty, filterProperties, openAggregate} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { @@ -278,6 +278,7 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { dependencies: [ + 'this', contribsByRefProperty, thingDataProperty, nullerProperty, @@ -285,7 +286,7 @@ export default class Thing extends CacheableObject { ].filter(Boolean), compute({ - [Thing.instance]: thing, + this: thing, [nullerProperty]: nuller, [contribsByRefProperty]: contribsByRef, [thingDataProperty]: thingData, @@ -330,9 +331,9 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { - dependencies: [thingDataProperty], + dependencies: ['this', thingDataProperty], - compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) => + compute: ({this: thing, [thingDataProperty]: thingData}) => thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [], }, }), @@ -344,9 +345,9 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { - dependencies: [thingDataProperty], + dependencies: ['this', thingDataProperty], - compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) => + compute: ({this: thing, [thingDataProperty]: thingData}) => thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [], }, }), @@ -462,15 +463,19 @@ export default class Thing extends CacheableObject { if (step.expose.dependencies) { for (const dependency of step.expose.dependencies) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; exposeDependencies.add(dependency); } } + let fn, type; if (base.flags.update) { if (step.expose.transform) { - exposeFunctionOrder.push({type: 'transform', fn: step.expose.transform}); + type = 'transform'; + fn = step.expose.transform; } else { - exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + type = 'compute'; + fn = step.expose.compute; } } else { if (step.expose.transform && !step.expose.compute) { @@ -478,8 +483,15 @@ export default class Thing extends CacheableObject { break expose; } - exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + type = 'compute'; + fn = step.expose.compute; } + + exposeFunctionOrder.push({ + type, + fn, + ownDependencies: step.expose.dependencies, + }); } }); } @@ -509,15 +521,20 @@ export default class Thing extends CacheableObject { const dependencies = {...initialDependencies}; let valueSoFar = value; - for (const {type, fn} of exposeFunctionOrder) { + for (const {type, fn, ownDependencies} of exposeFunctionOrder) { + const filteredDependencies = + (ownDependencies + ? filterProperties(dependencies, ownDependencies) + : {}) + const result = (type === 'transform' - ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { + ? fn(valueSoFar, filteredDependencies, (updatedValue, providedDependencies) => { valueSoFar = updatedValue ?? null; Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }) - : fn(dependencies, providedDependencies => { + : fn(filteredDependencies, providedDependencies => { Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; })); @@ -527,10 +544,13 @@ export default class Thing extends CacheableObject { } } + const filteredDependencies = + filterProperties(dependencies, base.expose.dependencies); + if (base.expose.transform) { - return base.expose.transform(valueSoFar, dependencies); + return base.expose.transform(valueSoFar, filteredDependencies); } else { - return base.expose.compute(dependencies); + return base.expose.compute(filteredDependencies); } }; } else { diff --git a/src/data/things/track.js b/src/data/things/track.js index 30c6fe58..551d9345 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -59,7 +59,8 @@ export class Track extends Thing { flags: {update: true, expose: true}, update: {validate: isColor}, expose: { - compute: ({album: {color}}) => color, + dependencies: ['#album.color'], + compute: ({'#album.color': color}) => color, }, }, ]), @@ -75,18 +76,27 @@ export class Track extends Thing { // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) coverArtFileExtension: Thing.composite.from([ - Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), + Track.composite.withAlbumProperties([ + 'trackCoverArtistContribsByRef', + 'trackCoverArtFileExtension', + ]), { flags: {update: true, expose: true}, update: {validate: isFileExtension}, expose: { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + dependencies: [ + 'coverArtistContribsByRef', + 'disableUniqueCoverArt', + '#album.trackCoverArtistContribsByRef', + '#album.trackCoverArtFileExtension', + ], transform(coverArtFileExtension, { coverArtistContribsByRef, disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension}, + '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, + '#album.trackCoverArtFileExtension': trackCoverArtFileExtension, }) { if (disableUniqueCoverArt) return null; if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; @@ -101,18 +111,27 @@ export class Track extends Thing { // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. coverArtDate: Thing.composite.from([ - Track.composite.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']), + Track.composite.withAlbumProperties([ + 'trackArtDate', + 'trackCoverArtistContribsByRef', + ]), { flags: {update: true, expose: true}, update: {validate: isDate}, expose: { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + dependencies: [ + 'coverArtistContribsByRef', + 'disableUniqueCoverArt', + '#album.trackArtDate', + '#album.trackCoverArtistContribsByRef', + ], transform(coverArtDate, { coverArtistContribsByRef, disableUniqueCoverArt, - album: {trackArtDate, trackCoverArtistContribsByRef}, + '#album.trackArtDate': trackArtDate, + '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, }) { if (disableUniqueCoverArt) return null; if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; @@ -148,8 +167,8 @@ export class Track extends Thing { flags: {expose: true}, expose: { - dependencies: ['albumData'], - compute: ({[Track.instance]: track, albumData}) => + dependencies: ['this', 'albumData'], + compute: ({this: track, albumData}) => albumData?.find((album) => album.tracks.includes(track)) ?? null, }, }, @@ -182,7 +201,8 @@ export class Track extends Thing { { flags: {expose: true}, expose: { - compute: ({album: {date}}) => date, + dependencies: ['#album.date'], + compute: ({'#album.date': date}) => date, }, }, ]), @@ -200,11 +220,16 @@ export class Track extends Thing { { flags: {expose: true}, expose: { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + dependencies: [ + 'coverArtistContribsByRef', + 'disableUniqueCoverArt', + '#album.trackCoverArtistContribsByRef', + ], + compute({ coverArtistContribsByRef, disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef}, + '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, }) { if (disableUniqueCoverArt) return false; if (!empty(coverArtistContribsByRef)) return true; @@ -225,12 +250,12 @@ export class Track extends Thing { flags: {expose: true}, expose: { - dependencies: ['originalReleaseTrackByRef', 'trackData'], + dependencies: ['this', 'originalReleaseTrackByRef', 'trackData'], compute: ({ + this: t1, originalReleaseTrackByRef: t1origRef, trackData, - [Track.instance]: t1, }) => { if (!trackData) { return []; @@ -252,15 +277,16 @@ export class Track extends Thing { artistContribs: Thing.composite.from([ Track.composite.inheritFromOriginalRelease('artistContribs'), - Thing.composite.withDynamicContribs('artistContribsByRef', 'artistContribs'), + Thing.composite.withDynamicContribs('artistContribsByRef', '#artistContribs'), Track.composite.withAlbumProperties(['artistContribs']), { flags: {expose: true}, expose: { + dependencies: ['#artistContribs', '#album.artistContribs'], compute: ({ - artistContribs: contribsFromTrack, - album: {artistContribs: contribsFromAlbum}, + '#artistContribs': contribsFromTrack, + '#album.artistContribs': contribsFromAlbum, }) => (empty(contribsFromTrack) ? contribsFromAlbum @@ -290,14 +316,15 @@ export class Track extends Thing { }, Track.composite.withAlbumProperties(['trackCoverArtistContribs']), - Thing.composite.withDynamicContribs('coverArtistContribsByRef', 'coverArtistContribs'), + Thing.composite.withDynamicContribs('coverArtistContribsByRef', '#coverArtistContribs'), { flags: {expose: true}, expose: { + dependencies: ['#coverArtistContribs', '#album.trackCoverArtistContribs'], compute: ({ - coverArtistContribs: contribsFromTrack, - album: {trackCoverArtistContribs: contribsFromAlbum}, + '#coverArtistContribs': contribsFromTrack, + '#album.trackCoverArtistContribs': contribsFromAlbum, }) => (empty(contribsFromTrack) ? contribsFromAlbum @@ -328,9 +355,9 @@ export class Track extends Thing { flags: {expose: true}, expose: { - dependencies: ['trackData'], + dependencies: ['this', 'trackData'], - compute: ({trackData, [Track.instance]: track}) => + compute: ({this: track, trackData}) => trackData ? trackData .filter((t) => !t.originalReleaseTrack) @@ -344,9 +371,9 @@ export class Track extends Thing { flags: {expose: true}, expose: { - dependencies: ['trackData'], + dependencies: ['this', 'trackData'], - compute: ({trackData, [Track.instance]: track}) => + compute: ({this: track, trackData}) => trackData ? trackData .filter((t) => !t.originalReleaseTrack) @@ -389,20 +416,20 @@ export class Track extends Thing { flags: {expose: true, compose: true}, expose: { - dependencies: ['albumData'], + dependencies: ['this', 'albumData'], - compute({albumData, [Track.instance]: track}, continuation) { + compute({this: track, albumData}, continuation) { const album = albumData?.find((album) => album.tracks.includes(track)); + const newDependencies = {}; - const filteredAlbum = Object.create(null); for (const property of albumProperties) { - filteredAlbum[property] = + newDependencies['#album.' + property] = (album ? album[property] : null); } - return continuation({album: filteredAlbum}); + return continuation(newDependencies); }, }, }), diff --git a/src/util/sugar.js b/src/util/sugar.js index 5b1f3193..1ba3f3ae 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -168,12 +168,24 @@ export function setIntersection(set1, set2) { return intersection; } -export function filterProperties(obj, properties) { - const set = new Set(properties); - return Object.fromEntries( - Object - .entries(obj) - .filter(([key]) => set.has(key))); +export function filterProperties(object, properties) { + if (typeof object !== 'object' || object === null) { + throw new TypeError(`Expected object to be an object, got ${object}`); + } + + if (!Array.isArray(properties)) { + throw new TypeError(`Expected properties to be an array, got ${properties}`); + } + + const filteredObject = {}; + + for (const property of properties) { + if (Object.hasOwn(object, property)) { + filteredObject[property] = object[property]; + } + } + + return filteredObject; } export function queue(array, max = 50) { -- cgit 1.3.0-6-gf8a5 From 1481db921e645ab09aad3a57b4ce308e2c57d738 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 22 Aug 2023 13:52:43 -0300 Subject: data: signature changes to misc compositional functions --- src/data/things/thing.js | 12 +++++--- src/data/things/track.js | 75 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index bc10e06b..f1ae6c71 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -577,14 +577,18 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, - withDynamicContribs: (contribsByRefProperty, dependencyName) => ({ + // Resolves the contribsByRef contained in the provided dependency, + // providing (named by the second argument) the result. "Resolving" + // means mapping the "who" reference of each contribution to an artist + // object, and filtering out those whose "who" doesn't match any artist. + withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ flags: {expose: true, compose: true}, expose: { - dependencies: ['artistData', contribsByRefProperty], - compute: ({artistData, [contribsByRefProperty]: contribsByRef}, callback) => + dependencies: ['artistData', contribsByRefDependency], + compute: ({artistData, [contribsByRefDependency]: contribsByRef}, callback) => callback({ - [dependencyName]: + [outputDependency]: Thing.findArtistsFromContribs(contribsByRef, artistData), }), }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 551d9345..985de594 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -53,7 +53,9 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties(['color']), + Track.composite.withAlbumProperties({ + properties: ['color'], + }), { flags: {update: true, expose: true}, @@ -76,10 +78,12 @@ export class Track extends Thing { // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) coverArtFileExtension: Thing.composite.from([ - Track.composite.withAlbumProperties([ - 'trackCoverArtistContribsByRef', - 'trackCoverArtFileExtension', - ]), + Track.composite.withAlbumProperties({ + properties: [ + 'trackCoverArtistContribsByRef', + 'trackCoverArtFileExtension', + ], + }), { flags: {update: true, expose: true}, @@ -111,10 +115,12 @@ export class Track extends Thing { // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. coverArtDate: Thing.composite.from([ - Track.composite.withAlbumProperties([ - 'trackArtDate', - 'trackCoverArtistContribsByRef', - ]), + Track.composite.withAlbumProperties({ + properties: [ + 'trackArtDate', + 'trackCoverArtistContribsByRef', + ], + }), { flags: {update: true, expose: true}, @@ -196,7 +202,9 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties(['date']), + Track.composite.withAlbumProperties({ + properties: ['date'], + }), { flags: {expose: true}, @@ -215,7 +223,9 @@ export class Track extends Thing { // the usual hasCoverArt to emphasize that it does not inherit from the // album.) hasUniqueCoverArt: Thing.composite.from([ - Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef']), + Track.composite.withAlbumProperties({ + properties: ['trackCoverArtistContribsByRef'], + }), { flags: {expose: true}, @@ -277,8 +287,14 @@ export class Track extends Thing { artistContribs: Thing.composite.from([ Track.composite.inheritFromOriginalRelease('artistContribs'), - Thing.composite.withDynamicContribs('artistContribsByRef', '#artistContribs'), - Track.composite.withAlbumProperties(['artistContribs']), + Track.composite.withAlbumProperties({ + properties: 'artistContribs', + }), + + Thing.composite.withResolvedContribs({ + from: 'artistContribsByRef', + to: '#artistContribs', + }), { flags: {expose: true}, @@ -315,8 +331,14 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties(['trackCoverArtistContribs']), - Thing.composite.withDynamicContribs('coverArtistContribsByRef', '#coverArtistContribs'), + Track.composite.withAlbumProperties({ + properties: ['trackCoverArtistContribs'], + }), + + Thing.composite.withResolvedContribs({ + from: 'coverArtistContribsByRef', + to: '#coverArtistContribs', + }), { flags: {expose: true}, @@ -395,7 +417,12 @@ export class Track extends Thing { }); static composite = { - inheritFromOriginalRelease: originalProperty => ({ + // Returns a value inherited from the original release, if this track + // is a rerelease, and otherwise continues with no further provided + // dependencies. If the second argument is provided true, then the + // continuation will also be called if the original release exposed + // the requested property as null. + inheritFromOriginalRelease: (originalProperty, allowOverride = false) => ({ flags: {expose: true, compose: true}, expose: { @@ -407,12 +434,20 @@ export class Track extends Thing { if (!trackData) return null; const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); if (!original) return null; - return original[originalProperty]; + + const value = original[originalProperty]; + if (allowOverride && value === null) return continuation(); + + return value; }, }, }), - withAlbumProperties: albumProperties => ({ + // Gets the listed properties from this track's album, providing them as + // dependencies (by default) with '#album.' prefixed before each property + // name. If the track's album isn't available, the same dependency names + // will each be provided as null. + withAlbumProperties: ({properties, prefix = '#album'}) => ({ flags: {expose: true, compose: true}, expose: { @@ -422,8 +457,8 @@ export class Track extends Thing { const album = albumData?.find((album) => album.tracks.includes(track)); const newDependencies = {}; - for (const property of albumProperties) { - newDependencies['#album.' + property] = + for (const property of properties) { + newDependencies[prefix + '.' + property] = (album ? album[property] : null); -- cgit 1.3.0-6-gf8a5 From 6483809c6d9c67f1311a64f2572b4fe5881d3a0d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 22 Aug 2023 22:36:20 -0300 Subject: data: composition docs, annotations, nesting --- src/data/things/thing.js | 316 +++++++++++++++++++++++++++++++++++++++++++++-- src/data/things/track.js | 81 +++++++----- 2 files changed, 359 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f1ae6c71..1186c389 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -426,14 +426,222 @@ export default class Thing extends CacheableObject { } static composite = { - from(composition) { + // Composes multiple compositional "steps" and a "base" to form a property + // descriptor out of modular building blocks. This is an extension to the + // more general-purpose CacheableObject property descriptor syntax, and + // aims to make modular data processing - which lends to declarativity - + // much easier, without fundamentally altering much of the typical syntax + // or terminology, nor building on it to an excessive degree. + // + // Think of a composition as being a chain of steps which lead into a final + // base property, which is usually responsible for returning the value that + // will actually get exposed when the property being described is accessed. + // + // == The compositional base: == + // + // The final item in a compositional list is its base, and it identifies + // the essential qualities of the property descriptor. The compositional + // steps preceding it may exit early, in which case the expose function + // defined on the base won't be called; or they will provide dependencies + // that the base may use to compute the final value that gets exposed for + // this property. + // + // The base indicates the capabilities of the composition as a whole. + // It should be {expose: true}, since that's the only area that preceding + // compositional steps (currently) can actually influence. If it's also + // {update: true}, then the composition as a whole accepts an update value + // just like normal update-flag property descriptors - meaning it can be + // set with `thing.someProperty = value` and that value will be paseed + // into each (implementing) step's transform() function, as well as the + // base. Bases usually aren't {compose: true}, but can be - check out the + // section on "nesting compositions" for details about that. + // + // Every composition always has exactly one compositional base, and it's + // always the last item in the composition list. All items preceding it + // are compositional steps, described below. + // + // == Compositional steps: == + // + // Compositional steps are, in essence, typical property descriptors with + // the extra flag {compose: true}. They operate on existing dependencies, + // and are typically dynamically constructed by "utility" functions (but + // can also be manually declared within the step list of a composition). + // Compositional steps serve two purposes: + // + // 1. exit early, if some condition is matched, returning and exposing + // some value directly from that step instead of continuing further + // down the step list; + // + // 2. and/or provide new, dynamically created "private" dependencies which + // can be accessed by further steps down the list, or at the base at + // the bottom, modularly supplying information that will contribute to + // the final value exposed for this property. + // + // Usually it's just one of those two, but it's fine for a step to perform + // both jobs if the situation benefits. + // + // Compositional steps are the real "modular" or "compositional" part of + // this data processing style - they're designed to be combined together + // in dynamic, versatile ways, as each property demands it. You usually + // define a compositional step to be returned by some ordinary static + // property-descriptor-returning function (customarily namespaced under + // the relevant Thing class's static `composite` field) - that lets you + // reuse it in multiple compositions later on. + // + // Compositional steps are implemented with "continuation passing style", + // meaning the connection to the next link on the chain is passed right to + // each step's compute (or transform) function, and the implementation gets + // to decide whether to continue on that chain or exit early by returning + // some other value. + // + // Every step along the chain, apart from the base at the bottom, has to + // have the {compose: true} step. That means its compute() or transform() + // function will be passed an extra argument at the end, `continuation`. + // To provide new dependencies to items further down the chain, just pass + // them directly to this continuation() function, customarily with a hash + // ('#') prefixing each name - for example: + // + // compute({..some dependencies..}, continuation) { + // return continuation({ + // '#excitingProperty': (..a value made from dependencies..), + // }); + // } + // + // Performing an early exit is as simple as returning some other value, + // instead of the continuation. + // + // It may be fine to simply provide new dependencies under a hard-coded + // name, such as '#excitingProperty' above, but if you're writing a utility + // that dynamically returns the compositional step and you suspect you + // might want to use this step multiple times in a single composition, + // it's customary to accept a name for the result. + // + // Here's a detailed example showing off early exit, dynamically operating + // on a provided dependency name, and then providing a result in another + // also-provided dependency name: + // + // static Thing.composite.withResolvedContribs = ({ + // from: contribsByRefDependency, + // to: outputDependency, + // }) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: [contribsByRefDependency, 'artistData'], + // compute({ + // [contribsByRefDependency]: contribsByRef, + // artistData, + // }, continuation) { + // if (!artistData) return null; /* early exit! */ + // return continuation({ + // [outputDependency]: /* this is the important part */ + // (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // And how you might work that into a composition: + // + // static Track[Thing.getPropertyDescriptors].coverArtists = + // Thing.composite.from([ + // Track.composite.doSomethingWhichMightEarlyExit(), + // Thing.composite.withResolvedContribs({ + // from: 'coverArtistContribsByRef', + // to: '#coverArtistContribs', + // }), + // + // { + // flags: {expose: true}, + // expose: { + // dependencies: ['#coverArtistContribs'], + // compute({'#coverArtistContribs': coverArtistContribs}) { + // return coverArtistContribs.map(({who}) => who); + // }, + // }, + // }, + // ]); + // + // == To compute or to transform: == + // + // A compositional step can work directly on a property's stored update + // value, transforming it in place and either early exiting with it or + // passing it on (via continuation) to the next item(s) in the + // compositional step list. (If needed, these can provide dependencies + // the same way as compute functions too - just pass that object after + // the updated (or same) transform value in your call to continuation().) + // + // But in order to make them more versatile, compositional steps have an + // extra trick up their sleeve. If a compositional step implements compute + // and *not* transform, it can still be used in a composition targeting a + // property which updates! These retain their full dependency-providing and + // early exit functionality - they just won't be provided the update value. + // If a compute-implementing step returns its continuation, then whichever + // later step (or the base) next implements transform() will receive the + // update value that had so far been running - as well as any dependencies + // the compute() step returned, of course! + // + // Please note that a compositional step which transforms *should not* + // specify, in its flags, {update: true}. Just provide the transform() + // function in its expose descriptor; it will be automatically detected + // and used when appropriate. + // + // It's actually possible for a step to specify both transform and compute, + // in which case the transform() implementation will only be selected if + // the composition's base is {update: true}. It's not exactly known why you + // would want to specify unique-but-related transform and compute behavior, + // but the basic possibility was too cool to skip out on. + // + // == Nesting compositions: == + // + // Compositional steps are so convenient that you just might want to bundle + // them together, and form a whole new step-shaped unit of its own! + // + // In order to allow for this while helping to ensure internal dependencies + // remain neatly isolated from the composition which nests your bundle, + // the Thing.composite.from() function will accept and adapt to a base that + // specifies the {compose: true} flag, just like the steps preceding it. + // + // The continuation function that gets provided to the base will be mildly + // special - after all, nothing follows the base within the composition's + // own list! Instead of appending dependencies alongside any previously + // provided ones to be available to the next step, the base's continuation + // function should be used to define "exports" of the composition as a + // whole. It's similar to the usual behavior of the continuation, just + // expanded to the scope of the composition instead of following steps. + // + // For example, suppose your composition (which you expect to include in + // other compositions) brings about several internal, hash-prefixed + // dependencies to contribute to its own results. Those dependencies won't + // end up "bleeding" into the dependency list of whichever composition is + // nesting this one - they will totally disappear once all the steps in + // the nested composition have finished up. + // + // To "export" the results of processing all those dependencies (provided + // that's something you want to do and this composition isn't used purely + // for a conditional early-exit), you'll want to define them in the + // continuation passed to the base. (Customarily, those should start with + // a hash just like the exports from any other compositional step; they're + // still dynamically provided dependencies!) + // + from(firstArg, secondArg) { + let annotation, composition; + if (typeof firstArg === 'string') { + [annotation, composition] = [firstArg, secondArg]; + } else { + [annotation, composition] = [null, firstArg]; + } + const base = composition.at(-1); const steps = composition.slice(0, -1); - const aggregate = openAggregate({message: `Errors preparing Thing.composite.from() composition`}); + const aggregate = openAggregate({ + message: + `Errors preparing Thing.composite.from() composition` + + (annotation ? ` (${annotation})` : ''), + }); - if (base.flags.compose) { - aggregate.push(new TypeError(`Base (bottom item) must not be {compose: true}`)); + if (base.flags.compose && base.flags.compute) { + push(new TypeError(`Base which composes can't also update yet`)); } const exposeFunctionOrder = []; @@ -500,14 +708,18 @@ export default class Thing extends CacheableObject { const constructedDescriptor = {}; + if (annotation) { + constructedDescriptor.annotation = annotation; + } + constructedDescriptor.flags = { update: !!base.flags.update, expose: !!base.flags.expose, - compose: false, + compose: !!base.flags.compose, }; if (base.flags.update) { - constructedDescriptor.update = base.flags.update; + constructedDescriptor.update = base.update; } if (base.flags.expose) { @@ -547,6 +759,9 @@ export default class Thing extends CacheableObject { const filteredDependencies = filterProperties(dependencies, base.expose.dependencies); + // Note: base.flags.compose is not compatible with base.flags.update, + // so the base.flags.compose case is not handled here. + if (base.expose.transform) { return base.expose.transform(valueSoFar, filteredDependencies); } else { @@ -554,7 +769,7 @@ export default class Thing extends CacheableObject { } }; } else { - expose.compute = (initialDependencies) => { + expose.compute = (initialDependencies, continuationIfApplicable) => { const dependencies = {...initialDependencies}; for (const {fn} of exposeFunctionOrder) { @@ -569,7 +784,23 @@ export default class Thing extends CacheableObject { } } - return base.expose.compute(dependencies); + if (base.flags.compose) { + let exportDependencies; + + const result = + base.expose.compute(dependencies, providedDependencies => { + exportDependencies = providedDependencies; + return continuationSymbol; + }); + + if (result !== continuationSymbol) { + return result; + } + + return exportDependencies; + } else { + return base.expose.compute(dependencies); + } }; } } @@ -577,11 +808,48 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // Provides dependencies exactly as they are (or null if not defined) to the + // continuation. Although this can *technically* be used to alias existing + // dependencies to some other name within the middle of a composition, it's + // intended to be used only as a composition's base - doing so makes the + // composition as a whole suitable as a step in some other composition, + // providing the listed (internal) dependencies to later steps just like + // other compositional steps. + export(mapping) { + const mappingEntries = Object.entries(mapping); + + return { + annotation: `Thing.composite.export`, + flags: {expose: true, compose: true}, + + expose: { + dependencies: Object.values(mapping), + + compute(dependencies, continuation) { + const exports = {}; + + // Note: This is slightly different behavior from filterProperties, + // as defined in sugar.js, which doesn't fall back to null for + // properties which don't exist on the original object. + for (const [exportKey, dependencyKey] of mappingEntries) { + exports[exportKey] = + (Object.hasOwn(dependencies, dependencyKey) + ? dependencies[dependencyKey] + : null); + } + + return continuation(exports); + } + }, + }; + }, + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ + annotation: `Thing.composite.withResolvedContribs`, flags: {expose: true, compose: true}, expose: { @@ -593,5 +861,37 @@ export default class Thing extends CacheableObject { }), }, }), + + // Resolves a reference by using the provided find function to match it + // within the provided thingData dependency. This will early exit if the + // data dependency is null, or, if earlyExitIfNotFound is set to true, + // if the find function doesn't match anything for the reference. + // Otherwise, the data object (or null, if not found) is provided on + // the output dependency. + withResolvedReference({ + ref: refDependency, + data: dataDependency, + to: outputDependency, + find: findFunction, + earlyExitIfNotFound = false, + }) { + return { + annotation: `Thing.composite.withResolvedReference`, + flags: {expose: true, compose: true}, + + expose: { + dependencies: [refDependency, dataDependency], + + compute({[refDependency]: ref, [dataDependency]: data}, continuation) { + if (data === null) return null; + + const match = findFunction(ref, data, {mode: 'quiet'}); + if (match === null && earlyExitIfNotFound) return null; + + return continuation({[outputDependency]: match}); + }, + }, + }; + } }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 985de594..718eb07e 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -44,7 +44,7 @@ export class Track extends Thing { sampledTracksByRef: Thing.common.referenceList(Track), artTagsByRef: Thing.common.referenceList(ArtTag), - color: Thing.composite.from([ + color: Thing.composite.from(`Track.color`, [ { flags: {expose: true, compose: true}, expose: { @@ -77,7 +77,7 @@ export class Track extends Thing { // track's unique cover artwork, if any, and does not inherit the cover's // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) - coverArtFileExtension: Thing.composite.from([ + coverArtFileExtension: Thing.composite.from(`Track.coverArtFileExtension`, [ Track.composite.withAlbumProperties({ properties: [ 'trackCoverArtistContribsByRef', @@ -114,7 +114,7 @@ export class Track extends Thing { // only the track's own unique cover artwork, if any. This exposes only as // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. - coverArtDate: Thing.composite.from([ + coverArtDate: Thing.composite.from(`Track.coverArtDate`, [ Track.composite.withAlbumProperties({ properties: [ 'trackArtDate', @@ -192,7 +192,7 @@ export class Track extends Thing { find.album ), - date: Thing.composite.from([ + date: Thing.composite.from(`Track.date`, [ { flags: {expose: true, compose: true}, expose: { @@ -222,7 +222,7 @@ export class Track extends Thing { // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) - hasUniqueCoverArt: Thing.composite.from([ + hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [ Track.composite.withAlbumProperties({ properties: ['trackCoverArtistContribsByRef'], }), @@ -284,7 +284,7 @@ export class Track extends Thing { }, }, - artistContribs: Thing.composite.from([ + artistContribs: Thing.composite.from(`Track.artistContribs`, [ Track.composite.inheritFromOriginalRelease('artistContribs'), Track.composite.withAlbumProperties({ @@ -311,7 +311,7 @@ export class Track extends Thing { }, ]), - contributorContribs: Thing.composite.from([ + contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ Track.composite.inheritFromOriginalRelease('contributorContribs'), Thing.common.dynamicContribs('contributorContribsByRef'), ]), @@ -319,7 +319,7 @@ export class Track extends Thing { // Cover artists aren't inherited from the original release, since it // typically varies by release and isn't defined by the musical qualities // of the track. - coverArtistContribs: Thing.composite.from([ + coverArtistContribs: Thing.composite.from(`Track.coverArtistContribs`, [ { flags: {expose: true, compose: true}, expose: { @@ -355,12 +355,12 @@ export class Track extends Thing { }, ]), - referencedTracks: Thing.composite.from([ + referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ Track.composite.inheritFromOriginalRelease('referencedTracks'), Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), ]), - sampledTracks: Thing.composite.from([ + sampledTracks: Thing.composite.from(`Track.sampledTracks`, [ Track.composite.inheritFromOriginalRelease('sampledTracks'), Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), ]), @@ -417,37 +417,39 @@ export class Track extends Thing { }); static composite = { - // Returns a value inherited from the original release, if this track - // is a rerelease, and otherwise continues with no further provided - // dependencies. If the second argument is provided true, then the - // continuation will also be called if the original release exposed - // the requested property as null. - inheritFromOriginalRelease: (originalProperty, allowOverride = false) => ({ - flags: {expose: true, compose: true}, + // Early exits with a value inherited from the original release, if + // this track is a rerelease, and otherwise continues with no further + // dependencies provided. If allowOverride is true, then the continuation + // will also be called if the original release exposed the requested + // property as null. + inheritFromOriginalRelease: ({property: originalProperty, allowOverride = false}) => + Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ + Track.composite.withOriginalRelease({to: '#originalRelease'}), - expose: { - dependencies: ['originalReleaseTrackByRef', 'trackData'], + { + flags: {expose: true, compose: true}, - compute({originalReleaseTrackByRef, trackData}, continuation) { - if (!originalReleaseTrackByRef) return continuation(); + expose: { + dependencies: ['#originalRelease'], - if (!trackData) return null; - const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); - if (!original) return null; + compute({'#originalRelease': originalRelease}, continuation) { + if (!originalRelease) return continuation(); - const value = original[originalProperty]; - if (allowOverride && value === null) return continuation(); + const value = originalRelease[originalProperty]; + if (allowOverride && value === null) return continuation(); - return value; - }, - }, - }), + return value; + }, + }, + } + ]), // Gets the listed properties from this track's album, providing them as // dependencies (by default) with '#album.' prefixed before each property // name. If the track's album isn't available, the same dependency names // will each be provided as null. withAlbumProperties: ({properties, prefix = '#album'}) => ({ + annotation: `Track.composite.withAlbumProperties`, flags: {expose: true, compose: true}, expose: { @@ -468,6 +470,25 @@ export class Track extends Thing { }, }, }), + + // Just includes the original release of this track as a dependency, or + // null, if it's not a rerelease. Note that this will early exit if the + // original release is specified by reference and that reference doesn't + // resolve to anything. + withOriginalRelease: ({to: outputDependency = '#originalRelease'}) => + Thing.composite.from(`Track.composite.withOriginalRelease`, [ + Thing.composite.withResolvedReference({ + ref: 'originalReleaseTrackByRef', + data: 'trackData', + to: '#originalRelease', + find: find.track, + earlyExitIfNotFound: true, + }), + + Thing.composite.export({ + [outputDependency]: '#originalRelease', + }), + ]), }; [inspect.custom]() { -- cgit 1.3.0-6-gf8a5 From c7e624684069fb9325c426500a5fcc153cf26b41 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 22 Aug 2023 22:39:22 -0300 Subject: data: track: remove unneeded explicit {to} on withOriginalRelease call --- src/data/things/track.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 718eb07e..118e3db0 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -424,7 +424,7 @@ export class Track extends Thing { // property as null. inheritFromOriginalRelease: ({property: originalProperty, allowOverride = false}) => Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ - Track.composite.withOriginalRelease({to: '#originalRelease'}), + Track.composite.withOriginalRelease(), { flags: {expose: true, compose: true}, @@ -474,8 +474,8 @@ export class Track extends Thing { // Just includes the original release of this track as a dependency, or // null, if it's not a rerelease. Note that this will early exit if the // original release is specified by reference and that reference doesn't - // resolve to anything. - withOriginalRelease: ({to: outputDependency = '#originalRelease'}) => + // resolve to anything. Outputs to '#originalRelease' by default. + withOriginalRelease: ({to: outputDependency = '#originalRelease'} = {}) => Thing.composite.from(`Track.composite.withOriginalRelease`, [ Thing.composite.withResolvedReference({ ref: 'originalReleaseTrackByRef', -- cgit 1.3.0-6-gf8a5 From 94f684138329e17ab43bfe552056d7ea3ed28b17 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 23 Aug 2023 12:22:11 -0300 Subject: data: track.hasUniqueCoverArt: operate on resolved contributions --- src/data/things/track.js | 50 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 118e3db0..414d5f29 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -223,29 +223,45 @@ export class Track extends Thing { // the usual hasCoverArt to emphasize that it does not inherit from the // album.) hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [ + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['disableUniqueCoverArt'], + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? false + : continuation()), + }, + }, + + Thing.composite.withResolvedContribs({ + from: 'coverArtistContribsByRef', + to: '#coverArtistContribs', + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#coverArtistContribs'], + compute: ({'#coverArtistContribs': coverArtistContribs}, continuation) => + (empty(coverArtistContribs) + ? continuation() + : true), + }, + }, + Track.composite.withAlbumProperties({ - properties: ['trackCoverArtistContribsByRef'], + properties: ['trackCoverArtistContribs'], }), { flags: {expose: true}, expose: { - dependencies: [ - 'coverArtistContribsByRef', - 'disableUniqueCoverArt', - '#album.trackCoverArtistContribsByRef', - ], - - compute({ - coverArtistContribsByRef, - disableUniqueCoverArt, - '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, - }) { - if (disableUniqueCoverArt) return false; - if (!empty(coverArtistContribsByRef)) return true; - if (!empty(trackCoverArtistContribsByRef)) return true; - return false; - }, + dependencies: ['#album.trackCoverArtistContribs'], + compute: ({'#album.trackCoverArtistContribs': trackCoverArtistContribs}) => + (empty(trackCoverArtistContribs) + ? false + : true), }, }, ]), -- cgit 1.3.0-6-gf8a5 From 13914b9f07f60d6d8aaaddc7df675d41950320c3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 23 Aug 2023 12:22:34 -0300 Subject: test: Track.{color,date,hasUniqueCoverArt} (unit) --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1186c389..decde6f4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -419,7 +419,7 @@ export default class Thing extends CacheableObject { return ( contribsByRef .map(({who, what}) => ({ - who: find.artist(who, artistData), + who: find.artist(who, artistData, {mode: 'quiet'}), what, })) .filter(({who}) => who)); -- cgit 1.3.0-6-gf8a5 From 0f4e27426384536c179583a8ffaf3dd9f121766b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 23 Aug 2023 19:00:35 -0300 Subject: data: Thing.composite.from: fix not calling export continuation --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index decde6f4..c1f969b2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -797,7 +797,7 @@ export default class Thing extends CacheableObject { return result; } - return exportDependencies; + return continuationIfApplicable(exportDependencies); } else { return base.expose.compute(dependencies); } -- cgit 1.3.0-6-gf8a5 From 8dd100d04fdd13b4ab8348d61378de5fd74f72d4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 23 Aug 2023 19:01:05 -0300 Subject: data: Thing.composite.withResolvedReference: fix null refs The `earlyExitIfNotFound` flag is only supposed to exit if the reference really existed and failed to match anything. If it was null in the first place, withResolvedReferences should always just pass null ahead. --- src/data/things/thing.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c1f969b2..798a057a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -866,8 +866,9 @@ export default class Thing extends CacheableObject { // within the provided thingData dependency. This will early exit if the // data dependency is null, or, if earlyExitIfNotFound is set to true, // if the find function doesn't match anything for the reference. - // Otherwise, the data object (or null, if not found) is provided on - // the output dependency. + // Otherwise, the data object is provided on the output dependency; + // or null, if the reference doesn't match anything or itself was null + // to begin with. withResolvedReference({ ref: refDependency, data: dataDependency, @@ -883,6 +884,8 @@ export default class Thing extends CacheableObject { dependencies: [refDependency, dataDependency], compute({[refDependency]: ref, [dataDependency]: data}, continuation) { + if (!ref) return continuation({[outputDependency]: null}); + if (data === null) return null; const match = findFunction(ref, data, {mode: 'quiet'}); -- cgit 1.3.0-6-gf8a5 From fab4b46d13795bcc82c8b4dd6b5a39ef23c42430 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 23 Aug 2023 19:02:44 -0300 Subject: data: fix more bad function signatures --- src/data/things/thing.js | 4 ++-- src/data/things/track.js | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 798a057a..eaf4655d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -854,8 +854,8 @@ export default class Thing extends CacheableObject { expose: { dependencies: ['artistData', contribsByRefDependency], - compute: ({artistData, [contribsByRefDependency]: contribsByRef}, callback) => - callback({ + compute: ({artistData, [contribsByRefDependency]: contribsByRef}, continuation) => + continuation({ [outputDependency]: Thing.findArtistsFromContribs(contribsByRef, artistData), }), diff --git a/src/data/things/track.js b/src/data/things/track.js index 414d5f29..74f5d7fb 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -301,12 +301,9 @@ export class Track extends Thing { }, artistContribs: Thing.composite.from(`Track.artistContribs`, [ - Track.composite.inheritFromOriginalRelease('artistContribs'), - - Track.composite.withAlbumProperties({ - properties: 'artistContribs', - }), + Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), + Track.composite.withAlbumProperties({properties: ['artistContribs']}), Thing.composite.withResolvedContribs({ from: 'artistContribsByRef', to: '#artistContribs', @@ -328,7 +325,7 @@ export class Track extends Thing { ]), contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ - Track.composite.inheritFromOriginalRelease('contributorContribs'), + Track.composite.inheritFromOriginalRelease({property: 'contributorContribs'}), Thing.common.dynamicContribs('contributorContribsByRef'), ]), @@ -372,12 +369,12 @@ export class Track extends Thing { ]), referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ - Track.composite.inheritFromOriginalRelease('referencedTracks'), + Track.composite.inheritFromOriginalRelease({property: 'referencedTracks'}), Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), ]), sampledTracks: Thing.composite.from(`Track.sampledTracks`, [ - Track.composite.inheritFromOriginalRelease('sampledTracks'), + Track.composite.inheritFromOriginalRelease({property: 'sampledTracks'}), Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), ]), -- cgit 1.3.0-6-gf8a5 From 2b2bbe9083d6f205e6b04b08c8bc4339a6a9ed87 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 24 Aug 2023 18:47:09 -0300 Subject: data: Thing.composite.from: mapDependencies/mapContinuation --- src/data/things/thing.js | 162 ++++++++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 71 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index eaf4655d..a9fd220f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -644,7 +644,7 @@ export default class Thing extends CacheableObject { push(new TypeError(`Base which composes can't also update yet`)); } - const exposeFunctionOrder = []; + const exposeSteps = []; const exposeDependencies = new Set(base.expose?.dependencies); for (let i = 0; i < steps.length; i++) { @@ -695,11 +695,7 @@ export default class Thing extends CacheableObject { fn = step.expose.compute; } - exposeFunctionOrder.push({ - type, - fn, - ownDependencies: step.expose.dependencies, - }); + exposeSteps.push(step.expose); } }); } @@ -727,81 +723,104 @@ export default class Thing extends CacheableObject { expose.dependencies = Array.from(exposeDependencies); const continuationSymbol = Symbol(); + const noTransformSymbol = Symbol(); - if (base.flags.update) { - expose.transform = (value, initialDependencies) => { - const dependencies = {...initialDependencies}; - let valueSoFar = value; - - for (const {type, fn, ownDependencies} of exposeFunctionOrder) { - const filteredDependencies = - (ownDependencies - ? filterProperties(dependencies, ownDependencies) - : {}) - - const result = - (type === 'transform' - ? fn(valueSoFar, filteredDependencies, (updatedValue, providedDependencies) => { + const _filterDependencies = (dependencies, step) => { + const filteredDependencies = + (step.dependencies + ? filterProperties(dependencies, step.dependencies) + : {}); + + if (step.mapDependencies) { + for (const [to, from] of Object.entries(step.mapDependencies)) { + filteredDependencies[to] = dependencies[from] ?? null; + } + } + + return filteredDependencies; + }; + + const _assignDependencies = (continuationAssignment, step) => { + if (!step.mapContinuation) { + return continuationAssignment; + } + + const assignDependencies = {}; + + for (const [from, to] of Object.entries(step.mapContinuation)) { + assignDependencies[to] = continuationAssignment[from] ?? null; + } + + return assignDependencies; + }; + + const _computeOrTransform = (value, initialDependencies) => { + const dependencies = {...initialDependencies}; + + let valueSoFar = value; + + for (const step of exposeSteps) { + const filteredDependencies = _filterDependencies(dependencies, step); + + let assignDependencies = null; + + const result = + (valueSoFar !== noTransformSymbol && step.transform + ? step.transform( + valueSoFar, filteredDependencies, + (updatedValue, providedDependencies) => { valueSoFar = updatedValue ?? null; - Object.assign(dependencies, providedDependencies ?? {}); + assignDependencies = providedDependencies; return continuationSymbol; }) - : fn(filteredDependencies, providedDependencies => { - Object.assign(dependencies, providedDependencies ?? {}); + : step.compute( + filteredDependencies, + (providedDependencies) => { + assignDependencies = providedDependencies; return continuationSymbol; })); - if (result !== continuationSymbol) { - return result; - } + if (result !== continuationSymbol) { + return result; } - const filteredDependencies = - filterProperties(dependencies, base.expose.dependencies); + Object.assign(dependencies, _assignDependencies(assignDependencies, step)); + } - // Note: base.flags.compose is not compatible with base.flags.update, - // so the base.flags.compose case is not handled here. + const filteredDependencies = _filterDependencies(dependencies, base.expose); - if (base.expose.transform) { - return base.expose.transform(valueSoFar, filteredDependencies); - } else { - return base.expose.compute(filteredDependencies); - } - }; - } else { - expose.compute = (initialDependencies, continuationIfApplicable) => { - const dependencies = {...initialDependencies}; - - for (const {fn} of exposeFunctionOrder) { - const result = - fn(dependencies, providedDependencies => { - Object.assign(dependencies, providedDependencies ?? {}); - return continuationSymbol; - }); - - if (result !== continuationSymbol) { - return result; - } - } + // Note: base.flags.compose is not compatible with base.flags.update. + if (base.expose.transform) { + return base.expose.transform(valueSoFar, filteredDependencies); + } else if (base.flags.compose) { + const continuation = continuationIfApplicable; - if (base.flags.compose) { - let exportDependencies; + let exportDependencies; - const result = - base.expose.compute(dependencies, providedDependencies => { - exportDependencies = providedDependencies; - return continuationSymbol; - }); + const result = + base.expose.compute(filteredDependencies, providedDependencies => { + exportDependencies = providedDependencies; + return continuationSymbol; + }); - if (result !== continuationSymbol) { - return result; - } - - return continuationIfApplicable(exportDependencies); - } else { - return base.expose.compute(dependencies); + if (result !== continuationSymbol) { + return result; } - }; + + return continuation(_assignDependencies(exportDependencies, base.expose)); + } else { + return base.expose.compute(filteredDependencies); + } + }; + + if (base.flags.update) { + expose.transform = + (value, initialDependencies) => + _computeOrTransform(value, initialDependencies); + } else { + expose.compute = + (initialDependencies) => + _computeOrTransform(undefined, initialDependencies); } } @@ -848,16 +867,17 @@ export default class Thing extends CacheableObject { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. - withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ + withResolvedContribs: ({from, to}) => ({ annotation: `Thing.composite.withResolvedContribs`, flags: {expose: true, compose: true}, expose: { - dependencies: ['artistData', contribsByRefDependency], - compute: ({artistData, [contribsByRefDependency]: contribsByRef}, continuation) => + dependencies: ['artistData'], + mapDependencies: {from}, + mapContinuation: {to}, + compute: ({artistData, from}, continuation) => continuation({ - [outputDependency]: - Thing.findArtistsFromContribs(contribsByRef, artistData), + to: Thing.findArtistsFromContribs(from, artistData), }), }, }), -- cgit 1.3.0-6-gf8a5 From 57edc116016f45f1bc9e7e3e6560450b6c480602 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 24 Aug 2023 18:49:11 -0300 Subject: data: fix not passing noTransformSymbol --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a9fd220f..d553a3ec 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -820,7 +820,7 @@ export default class Thing extends CacheableObject { } else { expose.compute = (initialDependencies) => - _computeOrTransform(undefined, initialDependencies); + _computeOrTransform(noTransformSymbol, initialDependencies); } } -- cgit 1.3.0-6-gf8a5 From 287de65cea2fb72833eb2fe596f7e61c61939481 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 24 Aug 2023 19:00:14 -0300 Subject: data: Track.coverArtistContribs: lazier steps --- src/data/things/track.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 74f5d7fb..2a3148ef 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -344,28 +344,36 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({ - properties: ['trackCoverArtistContribs'], - }), - Thing.composite.withResolvedContribs({ from: 'coverArtistContribsByRef', to: '#coverArtistContribs', }), { - flags: {expose: true}, + flags: {expose: true, compose: true}, expose: { - dependencies: ['#coverArtistContribs', '#album.trackCoverArtistContribs'], - compute: ({ - '#coverArtistContribs': contribsFromTrack, - '#album.trackCoverArtistContribs': contribsFromAlbum, - }) => + mapDependencies: {contribsFromTrack: '#coverArtistContribs'}, + compute: ({contribsFromTrack}, continuation) => (empty(contribsFromTrack) - ? contribsFromAlbum + ? continuation() : contribsFromTrack), }, }, + + Track.composite.withAlbumProperties({ + properties: ['trackCoverArtistContribs'], + }), + + { + flags: {expose: true}, + expose: { + mapDependencies: {contribsFromAlbum: '#album.trackCoverArtistContribs'}, + compute: ({contribsFromAlbum}) => + (empty(contribsFromAlbum) + ? null + : contribsFromAlbum), + }, + }, ]), referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ -- cgit 1.3.0-6-gf8a5 From eb869dd1b786a4180647e5b8b1b6f20aefb6c004 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 24 Aug 2023 19:43:28 -0300 Subject: data: Track.compposite.from: 'options', cache-safe documentation --- src/data/things/thing.js | 126 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d553a3ec..1bca6c38 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -561,6 +561,97 @@ export default class Thing extends CacheableObject { // }, // ]); // + // == Cache-safe dependency names: == + // + // [Disclosure: The caching engine hasn't actually been implemented yet. + // As such, this section is subject to change, and simply provides sound + // forward-facing advice and interfaces.] + // + // It's a good idea to write individual compositional steps in such a way + // that they're "cache-safe" - meaning the same input (dependency) values + // will always result in the same output (continuation or early exit). + // + // In order to facilitate this, compositional step descriptors may specify + // unique `mapDependencies`, `mapContinuation`, and `options` values. + // + // Consider the `withResolvedContribs` example adjusted to make use of + // two of these options below: + // + // static Thing.composite.withResolvedContribs = ({ + // from: contribsByRefDependency, + // to: outputDependency, + // }) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: ['artistData'], + // mapDependencies: {contribsByRef: contribsByRefDependency}, + // mapContinuation: {outputDependency}, + // compute({ + // contribsByRef, /* no longer in square brackets */ + // artistData, + // }, continuation) { + // if (!artistData) return null; + // return continuation({ + // outputDependency: /* no longer in square brackets */ + // (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // With a little destructuring and restructuring JavaScript sugar, the + // above can be simplified some more: + // + // static Thing.composite.withResolvedContribs = ({from, to}) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: ['artistData'], + // mapDependencies: {from}, + // mapContinuation: {to}, + // compute({artistData, from: contribsByRef}, continuation) { + // if (!artistData) return null; + // return continuation({ + // to: (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // These two properties let you separate the name-mapping behavior (for + // dependencies and the continuation) from the main body of the compute + // function. That means the compute function will *always* get inputs in + // the same form (dependencies 'artistData' and 'from' above), and will + // *always* provide its output in the same form (early return or 'to'). + // + // Thanks to that, this `compute` function is cache-safe! Its outputs can + // be cached corresponding to each set of mapped inputs. So it won't matter + // whether the `from` dependency is named `coverArtistContribsByRef` or + // `contributorContribsByRef` or something else - the compute function + // doesn't care, and only expects that value to be provided via its `from` + // argument. Likewise, it doesn't matter if the output should be sent to + // '#coverArtistContribs` or `#contributorContribs` or some other name; + // the mapping is handled automatically outside, and compute will always + // output its value to the continuation's `to`. + // + // Note that `mapDependencies` and `mapContinuation` should be objects of + // the same "shape" each run - that is, the values will change depending on + // outside context, but the keys are always the same. You shouldn't use + // `mapDependencies` to dynamically select more or fewer dependencies. + // If you need to dynamically select a range of dependencies, just specify + // them in the `dependencies` array like usual. The caching engine will + // understand that differently named `dependencies` indicate separate + // input-output caches should be used. + // + // The 'options' property makes it possible to specify external arguments + // that fundamentally change the behavior of the `compute` function, while + // still remaining cache-safe. It indicates that the caching engine should + // use a completely different input-to-output cache for each permutation + // of the 'options' values. This way, those functions are still cacheable + // at all; they'll just be cached separately for each set of option values. + // Values on the 'options' property will always be provided in compute's + // dependencies under '#options' (to avoid name conflicts with other + // dependencies). + // // == To compute or to transform: == // // A compositional step can work directly on a property's stored update @@ -725,7 +816,7 @@ export default class Thing extends CacheableObject { const continuationSymbol = Symbol(); const noTransformSymbol = Symbol(); - const _filterDependencies = (dependencies, step) => { + function _filterDependencies(dependencies, step) { const filteredDependencies = (step.dependencies ? filterProperties(dependencies, step.dependencies) @@ -737,10 +828,14 @@ export default class Thing extends CacheableObject { } } + if (step.options) { + filteredDependencies['#options'] = step.options; + } + return filteredDependencies; - }; + } - const _assignDependencies = (continuationAssignment, step) => { + function _assignDependencies(continuationAssignment, step) { if (!step.mapContinuation) { return continuationAssignment; } @@ -752,9 +847,9 @@ export default class Thing extends CacheableObject { } return assignDependencies; - }; + } - const _computeOrTransform = (value, initialDependencies) => { + function _computeOrTransform(value, initialDependencies) { const dependencies = {...initialDependencies}; let valueSoFar = value; @@ -811,7 +906,7 @@ export default class Thing extends CacheableObject { } else { return base.expose.compute(filteredDependencies); } - }; + } if (base.flags.update) { expose.transform = @@ -842,9 +937,10 @@ export default class Thing extends CacheableObject { flags: {expose: true, compose: true}, expose: { + options: {mappingEntries}, dependencies: Object.values(mapping), - compute(dependencies, continuation) { + compute({'#options': {mappingEntries}, ...dependencies}, continuation) { const exports = {}; // Note: This is slightly different behavior from filterProperties, @@ -890,9 +986,9 @@ export default class Thing extends CacheableObject { // or null, if the reference doesn't match anything or itself was null // to begin with. withResolvedReference({ - ref: refDependency, - data: dataDependency, - to: outputDependency, + ref, + data, + to, find: findFunction, earlyExitIfNotFound = false, }) { @@ -901,17 +997,19 @@ export default class Thing extends CacheableObject { flags: {expose: true, compose: true}, expose: { - dependencies: [refDependency, dataDependency], + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {to}, - compute({[refDependency]: ref, [dataDependency]: data}, continuation) { - if (!ref) return continuation({[outputDependency]: null}); + compute({ref, data, findFunction, earlyExitIfNotFound}, continuation) { + if (!ref) return continuation({to: null}); if (data === null) return null; const match = findFunction(ref, data, {mode: 'quiet'}); if (match === null && earlyExitIfNotFound) return null; - return continuation({[outputDependency]: match}); + return continuation({to: match}); }, }, }; -- cgit 1.3.0-6-gf8a5 From ba1cf3fe611661c85ef4a7150c924f99e1e94ba3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 24 Aug 2023 21:10:46 -0300 Subject: data: bug fixes & Thing.composite.from.debug mode --- src/data/things/thing.js | 272 +++++++++++++++++++++++++++++++++++++++-------- src/data/things/track.js | 3 +- 2 files changed, 229 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1bca6c38..555e443d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -508,7 +508,9 @@ export default class Thing extends CacheableObject { // } // // Performing an early exit is as simple as returning some other value, - // instead of the continuation. + // instead of the continuation. You may also use `continuation.exit(value)` + // to perform the exact same kind of early exit - it's just a different + // syntax that might fit in better in certain longer compositions. // // It may be fine to simply provide new dependencies under a hard-coded // name, such as '#excitingProperty' above, but if you're writing a utility @@ -715,6 +717,17 @@ export default class Thing extends CacheableObject { // still dynamically provided dependencies!) // from(firstArg, secondArg) { + const debug = fn => { + if (Thing.composite.from.debug === true) { + const result = fn(); + if (Array.isArray(result)) { + console.log(`[composite]`, ...result); + } else { + console.log(`[composite]`, result); + } + } + }; + let annotation, composition; if (typeof firstArg === 'string') { [annotation, composition] = [firstArg, secondArg]; @@ -738,6 +751,13 @@ export default class Thing extends CacheableObject { const exposeSteps = []; const exposeDependencies = new Set(base.expose?.dependencies); + if (base.expose?.mapDependencies) { + for (const dependency of Object.values(base.expose.mapDependencies)) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } + for (let i = 0; i < steps.length; i++) { const step = steps[i]; const message = @@ -767,6 +787,13 @@ export default class Thing extends CacheableObject { } } + if (step.expose.mapDependencies) { + for (const dependency of Object.values(step.expose.mapDependencies)) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } + let fn, type; if (base.flags.update) { if (step.expose.transform) { @@ -813,8 +840,8 @@ export default class Thing extends CacheableObject { const expose = constructedDescriptor.expose = {}; expose.dependencies = Array.from(exposeDependencies); - const continuationSymbol = Symbol(); - const noTransformSymbol = Symbol(); + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); function _filterDependencies(dependencies, step) { const filteredDependencies = @@ -849,48 +876,153 @@ export default class Thing extends CacheableObject { return assignDependencies; } + function _prepareContinuation(transform, step) { + const continuationStorage = { + returnedWith: null, + providedDependencies: null, + providedValue: null, + }; + + const continuation = + (transform + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.exit = (providedValue) => { + continuationStorage.returnedWith = 'exit'; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + }; + + if (base.flags.compose) { + continuation.raise = + (transform + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'raise'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'raise'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + } + + return {continuation, continuationStorage}; + } + function _computeOrTransform(value, initialDependencies) { const dependencies = {...initialDependencies}; - let valueSoFar = value; + let valueSoFar = value; // Set only for {update: true} compositions + let exportDependencies = null; // Set only for {compose: true} compositions + + debug(() => color.bright(`begin composition (annotation: ${annotation})`)); + + for (let i = 0; i < exposeSteps.length; i++) { + const step = exposeSteps[i]; + debug(() => [`step #${i+1}:`, step]); + + const transform = + valueSoFar !== noTransformSymbol && + step.transform; - for (const step of exposeSteps) { const filteredDependencies = _filterDependencies(dependencies, step); + const {continuation, continuationStorage} = _prepareContinuation(transform, step); - let assignDependencies = null; + if (transform) { + debug(() => `step #${i+1} - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + } else { + debug(() => `step #${i+1} - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + } const result = - (valueSoFar !== noTransformSymbol && step.transform - ? step.transform( - valueSoFar, filteredDependencies, - (updatedValue, providedDependencies) => { - valueSoFar = updatedValue ?? null; - assignDependencies = providedDependencies; - return continuationSymbol; - }) - : step.compute( - filteredDependencies, - (providedDependencies) => { - assignDependencies = providedDependencies; - return continuationSymbol; - })); + (transform + ? step.transform(valueSoFar, filteredDependencies, continuation) + : step.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { + if (base.flags.compose) { + throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); + } + + debug(() => `step #${i+1} - early-exit (inferred)`); + debug(() => `early-exit: ${inspect(result, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return result; } - Object.assign(dependencies, _assignDependencies(assignDependencies, step)); + if (continuationStorage.returnedWith === 'exit') { + debug(() => `step #${i+1} - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationSymbol.providedValue; + } + + if (continuationStorage.returnedWith === 'raise') { + if (transform) { + valueSoFar = continuationStorage.providedValue; + } + + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + + debug(() => `step #${i+1} - result: raise`); + + break; + } + + if (continuationStorage.returnedWith === 'continuation') { + if (transform) { + valueSoFar = continuationStorage.providedValue; + } + + debug(() => `step #${i+1} - result: continuation`); + + if (continuationStorage.providedDependencies) { + const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + Object.assign(dependencies, assignDependencies); + + debug(() => [`assign dependencies:`, assignDependencies]); + } + } } + if (exportDependencies) { + debug(() => [`raise dependencies:`, exportDependencies]); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationIfApplicable(exportDependencies); + } + + debug(() => `completed all steps, reached base`); + const filteredDependencies = _filterDependencies(dependencies, base.expose); // Note: base.flags.compose is not compatible with base.flags.update. if (base.expose.transform) { - return base.expose.transform(valueSoFar, filteredDependencies); + debug(() => `base - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + + const result = base.expose.transform(valueSoFar, filteredDependencies); + + debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); + + return result; } else if (base.flags.compose) { - const continuation = continuationIfApplicable; + const {continuation, continuationStorage} = _prepareContinuation(transform, base.expose); - let exportDependencies; + debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); const result = base.expose.compute(filteredDependencies, providedDependencies => { @@ -899,12 +1031,39 @@ export default class Thing extends CacheableObject { }); if (result !== continuationSymbol) { - return result; + throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); + } + + if (continuationStorage.returnedWith === 'continuation') { + throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + } + + if (continuationStorage.returnedWith === 'exit') { + debug(() => `base - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationStorage.providedValue; } - return continuation(_assignDependencies(exportDependencies, base.expose)); + if (continuationStorage.returnedWith === 'raise') { + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base.expose); + + debug(() => `base - result: raise`); + debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationIfApplicable(exportDependencies); + } } else { - return base.expose.compute(filteredDependencies); + debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + + const result = base.expose.compute(filteredDependencies); + + debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return result; } } @@ -953,7 +1112,7 @@ export default class Thing extends CacheableObject { : null); } - return continuation(exports); + return continuation.raise(exports); } }, }; @@ -985,34 +1144,57 @@ export default class Thing extends CacheableObject { // Otherwise, the data object is provided on the output dependency; // or null, if the reference doesn't match anything or itself was null // to begin with. - withResolvedReference({ + withResolvedReference: ({ ref, data, to, find: findFunction, earlyExitIfNotFound = false, - }) { - return { - annotation: `Thing.composite.withResolvedReference`, - flags: {expose: true, compose: true}, + }) => + Thing.composite.from(`Thing.composite.withResolvedReference`, [ + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {ref}, + mapContinuation: {to}, + + compute: ({ref}, continuation) => + (ref + ? continuation() + : continuation.raise({to: null})), + }, + }, - expose: { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {to}, + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {data}, + + compute: ({data}, continuation) => + (data === null + ? continuation.exit(null) + : continuation()), + }, + }, - compute({ref, data, findFunction, earlyExitIfNotFound}, continuation) { - if (!ref) return continuation({to: null}); + { + flags: {expose: true, compose: true}, + expose: { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, - if (data === null) return null; + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); - const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) return null; + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } - return continuation({to: match}); + return continuation({match}); + }, }, }, - }; - } + ]), }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 2a3148ef..23b6da56 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -475,8 +475,9 @@ export class Track extends Thing { expose: { dependencies: ['this', 'albumData'], + options: {properties, prefix}, - compute({this: track, albumData}, continuation) { + compute({this: track, albumData, '#options': {properties, prefix}}, continuation) { const album = albumData?.find((album) => album.tracks.includes(track)); const newDependencies = {}; -- cgit 1.3.0-6-gf8a5 From 2a87cbaf06e364585bae2b48919c46b6d1f7aa1f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 25 Aug 2023 13:28:29 -0300 Subject: data: Thing.composite.from bugfixes --- src/data/things/thing.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 555e443d..16dd786d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -922,7 +922,7 @@ export default class Thing extends CacheableObject { return {continuation, continuationStorage}; } - function _computeOrTransform(value, initialDependencies) { + function _computeOrTransform(value, initialDependencies, continuationIfApplicable) { const dependencies = {...initialDependencies}; let valueSoFar = value; // Set only for {update: true} compositions @@ -1020,15 +1020,11 @@ export default class Thing extends CacheableObject { return result; } else if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(transform, base.expose); + const {continuation, continuationStorage} = _prepareContinuation(false, base.expose); debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - const result = - base.expose.compute(filteredDependencies, providedDependencies => { - exportDependencies = providedDependencies; - return continuationSymbol; - }); + const result = base.expose.compute(filteredDependencies, continuation); if (result !== continuationSymbol) { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); @@ -1069,12 +1065,12 @@ export default class Thing extends CacheableObject { if (base.flags.update) { expose.transform = - (value, initialDependencies) => - _computeOrTransform(value, initialDependencies); + (value, initialDependencies, continuationIfApplicable) => + _computeOrTransform(value, initialDependencies, continuationIfApplicable); } else { expose.compute = - (initialDependencies) => - _computeOrTransform(noTransformSymbol, initialDependencies); + (initialDependencies, continuationIfApplicable) => + _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); } } -- cgit 1.3.0-6-gf8a5 From fe9bd87d1e6b71c3019b38ca2f99e0c21d916186 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 25 Aug 2023 13:28:56 -0300 Subject: data: use continuation.exit and continuation.raise where needed --- src/data/things/thing.js | 2 +- src/data/things/track.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 16dd786d..578a5a4e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1187,7 +1187,7 @@ export default class Thing extends CacheableObject { return continuation.exit(null); } - return continuation({match}); + return continuation.raise({match}); }, }, }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 23b6da56..5c3a1d46 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -454,12 +454,12 @@ export class Track extends Thing { dependencies: ['#originalRelease'], compute({'#originalRelease': originalRelease}, continuation) { - if (!originalRelease) return continuation(); + if (!originalRelease) return continuation.raise(); const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation(); + if (allowOverride && value === null) return continuation.raise(); - return value; + return continuation.exit(value); }, }, } -- cgit 1.3.0-6-gf8a5 From c7d8ab3286854faa2ccf54e84b8efbff43a416fc Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 25 Aug 2023 13:29:38 -0300 Subject: data: Track.artistContribs: be lazy, like coverArtistContribs --- src/data/things/track.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 5c3a1d46..016f5199 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -303,25 +303,34 @@ export class Track extends Thing { artistContribs: Thing.composite.from(`Track.artistContribs`, [ Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), - Track.composite.withAlbumProperties({properties: ['artistContribs']}), Thing.composite.withResolvedContribs({ from: 'artistContribsByRef', to: '#artistContribs', }), { - flags: {expose: true}, + flags: {expose: true, compose: true}, expose: { - dependencies: ['#artistContribs', '#album.artistContribs'], - compute: ({ - '#artistContribs': contribsFromTrack, - '#album.artistContribs': contribsFromAlbum, - }) => + mapDependencies: {contribsFromTrack: '#artistContribs'}, + compute: ({contribsFromTrack}, continuation) => (empty(contribsFromTrack) - ? contribsFromAlbum + ? continuation() : contribsFromTrack), }, }, + + Track.composite.withAlbumProperties({properties: ['artistContribs']}), + + { + flags: {expose: true}, + expose: { + mapDependencies: {contribsFromAlbum: '#album.artistContribs'}, + compute: ({contribsFromAlbum}) => + (empty(contribsFromAlbum) + ? null + : contribsFromAlbum), + }, + }, ]), contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ -- cgit 1.3.0-6-gf8a5 From f562896d4d67558a32726f7086beebf29019a44d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 25 Aug 2023 14:06:00 -0300 Subject: yaml, test: mutate/decache wikiData in more reusable ways --- src/data/yaml.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index 25eda3c5..2ad2d41d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1320,13 +1320,27 @@ export async function loadAndProcessDataDocuments({dataPath}) { // Data linking! Basically, provide (portions of) wikiData to the Things which // require it - they'll expose dynamically computed properties as a result (many -// of which are required for page HTML generation). -export function linkWikiDataArrays(wikiData) { +// of which are required for page HTML generation and other expected behavior). +// +// The XXX_decacheWikiData option should be used specifically to mark +// points where you *aren't* replacing any of the arrays under wikiData with +// new values, and are using linkWikiDataArrays to instead "decache" data +// properties which depend on any of them. It's currently not possible for +// a CacheableObject to depend directly on the value of a property exposed +// on some other CacheableObject, so when those values change, you have to +// manually decache before the object will realize its cache isn't valid +// anymore. +export function linkWikiDataArrays(wikiData, { + XXX_decacheWikiData = false, +} = {}) { function assignWikiData(things, ...keys) { + if (things === undefined) return; for (let i = 0; i < things.length; i++) { const thing = things[i]; for (let j = 0; j < keys.length; j++) { const key = keys[j]; + if (!(key in wikiData)) continue; + if (XXX_decacheWikiData) thing[key] = []; thing[key] = wikiData[key]; } } @@ -1344,7 +1358,7 @@ export function linkWikiDataArrays(wikiData) { assignWikiData(WD.flashData, 'artistData', 'flashActData', 'trackData'); assignWikiData(WD.flashActData, 'flashData'); assignWikiData(WD.artTagData, 'albumData', 'trackData'); - assignWikiData(WD.homepageLayout.rows, 'albumData', 'groupData'); + assignWikiData(WD.homepageLayout?.rows, 'albumData', 'groupData'); } export function sortWikiDataArrays(wikiData) { -- cgit 1.3.0-6-gf8a5 From 809ae313afdb6c7bb859a94170f6fd2c6c888591 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 26 Aug 2023 18:28:19 -0300 Subject: data: Track.composite.withAlbum --- src/data/things/track.js | 83 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 016f5199..6c08aa01 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -474,34 +474,85 @@ export class Track extends Thing { } ]), - // Gets the listed properties from this track's album, providing them as - // dependencies (by default) with '#album.' prefixed before each property - // name. If the track's album isn't available, the same dependency names - // will each be provided as null. - withAlbumProperties: ({properties, prefix = '#album'}) => ({ - annotation: `Track.composite.withAlbumProperties`, + // Gets the track's album. Unless earlyExitIfNotFound is overridden false, + // this will early-exit with null in two cases - albumData being missing, + // or not including an album whose .tracks array includes this track. + withAlbum: ({to = '#album', earlyExitIfNotFound = true} = {}) => ({ + annotation: `Track.composite.withAlbum`, flags: {expose: true, compose: true}, expose: { dependencies: ['this', 'albumData'], - options: {properties, prefix}, + mapContinuation: {to}, + options: {earlyExitIfNotFound}, + + compute({ + this: track, + albumData, + '#options': {earlyExitIfNotFound}, + }, continuation) { + if (empty(albumData)) { + return ( + (earlyExitIfNotFound + ? continuation.exit(null) + : continuation({to: null}))); + } - compute({this: track, albumData, '#options': {properties, prefix}}, continuation) { - const album = albumData?.find((album) => album.tracks.includes(track)); - const newDependencies = {}; + const album = + albumData?.find(album => album.tracks.includes(track)); - for (const property of properties) { - newDependencies[prefix + '.' + property] = - (album - ? album[property] - : null); + if (!album) { + return ( + (earlyExitIfNotFound + ? continuation.exit(null) + : continuation({to: null}))); } - return continuation(newDependencies); + return continuation({to: album}); }, }, }), + // Gets the listed properties from this track's album, providing them as + // dependencies (by default) with '#album.' prefixed before each property + // name. If the track's album isn't available, and earlyExitIfNotFound + // hasn't been set, the same dependency names will be provided as null. + withAlbumProperties: ({ + properties, + prefix = '#album', + earlyExitIfNotFound = false, + }) => + Thing.composite.from(`Track.composite.withAlbumProperties`, [ + Track.composite.withAlbum({earlyExitIfNotFound}), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#album'], + options: {properties, prefix}, + + compute({ + '#album': album, + '#options': {properties, prefix}, + }, continuation) { + const raise = {}; + + if (album) { + for (const property of properties) { + raise[prefix + '.' + property] = album[property]; + } + } else { + for (const property of properties) { + raise[prefix + '.' + property] = null; + } + } + + return continuation.raise(raise); + }, + }, + }, + ]), + // Just includes the original release of this track as a dependency, or // null, if it's not a rerelease. Note that this will early exit if the // original release is specified by reference and that reference doesn't -- cgit 1.3.0-6-gf8a5 From e2f1cd30f8d5804f97043faedc5aea9fe06cea32 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 26 Aug 2023 18:46:14 -0300 Subject: data: Thing.composite.from: fix undefined return for explicit exit --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 578a5a4e..c870b89c 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -969,7 +969,7 @@ export default class Thing extends CacheableObject { debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); debug(() => color.bright(`end composition (annotation: ${annotation})`)); - return continuationSymbol.providedValue; + return continuationStorage.providedValue; } if (continuationStorage.returnedWith === 'raise') { -- cgit 1.3.0-6-gf8a5 From 25beb8731d756bfa4fe6babb9e4b0a707c7823e0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 26 Aug 2023 19:22:38 -0300 Subject: data, test: misc. additions * Thing.composite.expose * Thing.composite.exposeUpdateValueOrContinue * Track.composite.withAlbumProperty * refactor: Track.color, Track.album, Track.date * refactor: Track.coverArtistContribs * test: Track.album (unit) --- src/data/things/thing.js | 51 ++++++++++++++++++++++++++ src/data/things/track.js | 95 +++++++++++++++++++++--------------------------- 2 files changed, 92 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c870b89c..2af06904 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1114,6 +1114,57 @@ export default class Thing extends CacheableObject { }; }, + // Exposes a dependency exactly as it is; this is typically the base of a + // composition which was created to serve as one property's descriptor. + // Since this serves as a base, specify {update: true} to indicate that + // the property as a whole updates (and some previous compositional step + // works with that update value). + // + // Please note that this *doesn't* verify that the dependency exists, so + // if you provide the wrong name or it hasn't been set by a previous + // compositional step, the property will be exposed as undefined instead + // of null. + // + expose: (dependency, {update = false} = {}) => ({ + annotation: `Thing.composite.expose`, + flags: {expose: true, update}, + + expose: { + mapDependencies: {dependency}, + compute: ({dependency}) => dependency, + }, + }), + + // Exposes the update value of an {update: true} property, or continues if + // it's unavailable. By default, "unavailable" means value === null, but + // set {mode: 'empty'} to + exposeUpdateValueOrContinue({mode = 'null'} = {}) { + if (mode !== 'null' && mode !== 'empty') { + throw new TypeError(`Expected mode to be null or empty`); + } + + return { + annotation: `Thing.composite.exposeUpdateValueOrContinue`, + flags: {expose: true, compose: true}, + expose: { + options: {mode}, + + transform(value, {'#options': {mode}}, continuation) { + const shouldContinue = + (mode === 'empty' + ? empty(value) + : value === null); + + if (shouldContinue) { + return continuation(); + } else { + return continuation.exit(value); + } + } + }, + }; + }, + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist diff --git a/src/data/things/track.js b/src/data/things/track.js index 6c08aa01..15a48bb4 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -45,26 +45,9 @@ export class Track extends Thing { artTagsByRef: Thing.common.referenceList(ArtTag), color: Thing.composite.from(`Track.color`, [ - { - flags: {expose: true, compose: true}, - expose: { - transform: (color, {}, continuation) => - color ?? continuation(), - }, - }, - - Track.composite.withAlbumProperties({ - properties: ['color'], - }), - - { - flags: {update: true, expose: true}, - update: {validate: isColor}, - expose: { - dependencies: ['#album.color'], - compute: ({'#album.color': color}) => color, - }, - }, + Thing.composite.exposeUpdateValueOrContinue(), + Track.composite.withAlbumProperty('color'), + Thing.composite.expose('#album.color', {update: true}), ]), // Disables presenting the track as though it has its own unique artwork. @@ -169,15 +152,11 @@ export class Track extends Thing { commentatorArtists: Thing.common.commentatorArtists(), - album: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'albumData'], - compute: ({this: track, albumData}) => - albumData?.find((album) => album.tracks.includes(track)) ?? null, - }, - }, + album: + Thing.composite.from(`Track.album`, [ + Track.composite.withAlbum(), + Thing.composite.expose('#album'), + ]), // Note - this is an internal property used only to help identify a track. // It should not be assumed in general that the album and dataSourceAlbum match @@ -202,17 +181,8 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({ - properties: ['date'], - }), - - { - flags: {expose: true}, - expose: { - dependencies: ['#album.date'], - compute: ({'#album.date': date}) => date, - }, - }, + Track.composite.withAlbumProperties({properties: ['date']}), + Thing.composite.expose('#album.date'), ]), // Whether or not the track has "unique" cover artwork - a cover which is @@ -369,20 +339,8 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({ - properties: ['trackCoverArtistContribs'], - }), - - { - flags: {expose: true}, - expose: { - mapDependencies: {contribsFromAlbum: '#album.trackCoverArtistContribs'}, - compute: ({contribsFromAlbum}) => - (empty(contribsFromAlbum) - ? null - : contribsFromAlbum), - }, - }, + Track.composite.withAlbumProperty('trackCoverArtistContribs'), + Thing.composite.expose('#album.trackCoverArtistContribs'), ]), referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ @@ -513,6 +471,35 @@ export class Track extends Thing { }, }), + // Gets a single property from this track's album, providing it as the same + // property name prefixed with '#album.' (by default). If the track's album + // isn't available, and earlyExitIfNotFound hasn't been set, the property + // will be provided as null. + withAlbumProperty: (property, { + to = '#album.' + property, + earlyExitIfNotFound = false, + } = {}) => + Thing.composite.from(`Track.composite.withAlbumProperty`, [ + Track.composite.withAlbum({earlyExitIfNotFound}), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#album'], + options: {property}, + mapContinuation: {to}, + + compute: ({ + '#album': album, + '#options': {property}, + }, continuation) => + (album + ? continuation.raise({to: album[property]}) + : continuation.raise({to: null})), + }, + }, + ]), + // Gets the listed properties from this track's album, providing them as // dependencies (by default) with '#album.' prefixed before each property // name. If the track's album isn't available, and earlyExitIfNotFound -- cgit 1.3.0-6-gf8a5 From 12b8040b05e81a523ef59ba583dde751206f2e1d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 26 Aug 2023 20:38:27 -0300 Subject: data, test: retain validator for Track.color --- src/data/things/thing.js | 20 ++++++++++++++------ src/data/things/track.js | 4 +++- 2 files changed, 17 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 2af06904..2adba5c4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1116,9 +1116,11 @@ export default class Thing extends CacheableObject { // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. - // Since this serves as a base, specify {update: true} to indicate that - // the property as a whole updates (and some previous compositional step - // works with that update value). + // Since this serves as a base, specify a value for {update} to indicate + // that the property as a whole updates (and some previous compositional + // step works with that update value). Set {update: true} to only enable + // the update flag, or set update to an object to specify a descriptor + // (e.g. for custom value validation). // // Please note that this *doesn't* verify that the dependency exists, so // if you provide the wrong name or it hasn't been set by a previous @@ -1127,17 +1129,23 @@ export default class Thing extends CacheableObject { // expose: (dependency, {update = false} = {}) => ({ annotation: `Thing.composite.expose`, - flags: {expose: true, update}, + flags: {expose: true, update: !!update}, expose: { mapDependencies: {dependency}, compute: ({dependency}) => dependency, }, + + update: + (typeof update === 'object' + ? update + : null), }), // Exposes the update value of an {update: true} property, or continues if // it's unavailable. By default, "unavailable" means value === null, but - // set {mode: 'empty'} to + // set {mode: 'empty'} to check with empty() instead, continuing for empty + // arrays also. exposeUpdateValueOrContinue({mode = 'null'} = {}) { if (mode !== 'null' && mode !== 'empty') { throw new TypeError(`Expected mode to be null or empty`); @@ -1156,7 +1164,7 @@ export default class Thing extends CacheableObject { : value === null); if (shouldContinue) { - return continuation(); + return continuation(value); } else { return continuation.exit(value); } diff --git a/src/data/things/track.js b/src/data/things/track.js index 15a48bb4..8d0a7ad4 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -47,7 +47,9 @@ export class Track extends Thing { color: Thing.composite.from(`Track.color`, [ Thing.composite.exposeUpdateValueOrContinue(), Track.composite.withAlbumProperty('color'), - Thing.composite.expose('#album.color', {update: true}), + Thing.composite.expose('#album.color', { + update: {validate: isColor}, + }), ]), // Disables presenting the track as though it has its own unique artwork. -- cgit 1.3.0-6-gf8a5 From e6038d8c07971447f444cf597328ca8d9863f8fd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 26 Aug 2023 21:18:43 -0300 Subject: data, test: Track.color inherits from track section --- src/data/things/thing.js | 2 +- src/data/things/track.js | 69 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 2adba5c4..f5dc786e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1127,7 +1127,7 @@ export default class Thing extends CacheableObject { // compositional step, the property will be exposed as undefined instead // of null. // - expose: (dependency, {update = false} = {}) => ({ + exposeDependency: (dependency, {update = false} = {}) => ({ annotation: `Thing.composite.expose`, flags: {expose: true, update: !!update}, diff --git a/src/data/things/track.js b/src/data/things/track.js index 8d0a7ad4..621044d5 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -46,8 +46,25 @@ export class Track extends Thing { color: Thing.composite.from(`Track.color`, [ Thing.composite.exposeUpdateValueOrContinue(), + Track.composite.withContainingTrackSection({earlyExitIfNotFound: false}), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#trackSection'], + compute: ({'#trackSection': trackSection}, continuation) => + // Album.trackSections guarantees the track section will have a + // color property (inheriting from the album's own color), but only + // if it's actually present! Color will be inherited directly from + // album otherwise. + (trackSection + ? trackSection.color + : continuation()), + }, + }, + Track.composite.withAlbumProperty('color'), - Thing.composite.expose('#album.color', { + Thing.composite.exposeDependency('#album.color', { update: {validate: isColor}, }), ]), @@ -157,7 +174,7 @@ export class Track extends Thing { album: Thing.composite.from(`Track.album`, [ Track.composite.withAlbum(), - Thing.composite.expose('#album'), + Thing.composite.exposeDependency('#album'), ]), // Note - this is an internal property used only to help identify a track. @@ -183,8 +200,8 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({properties: ['date']}), - Thing.composite.expose('#album.date'), + Track.composite.withAlbumProperty('date'), + Thing.composite.exposeDependency('#album.date'), ]), // Whether or not the track has "unique" cover artwork - a cover which is @@ -342,7 +359,7 @@ export class Track extends Thing { }, Track.composite.withAlbumProperty('trackCoverArtistContribs'), - Thing.composite.expose('#album.trackCoverArtistContribs'), + Thing.composite.exposeDependency('#album.trackCoverArtistContribs'), ]), referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ @@ -435,7 +452,7 @@ export class Track extends Thing { ]), // Gets the track's album. Unless earlyExitIfNotFound is overridden false, - // this will early-exit with null in two cases - albumData being missing, + // this will early exit with null in two cases - albumData being missing, // or not including an album whose .tracks array includes this track. withAlbum: ({to = '#album', earlyExitIfNotFound = true} = {}) => ({ annotation: `Track.composite.withAlbum`, @@ -542,6 +559,46 @@ export class Track extends Thing { }, ]), + // Gets the track section containing this track from its album's track list. + // Unless earlyExitIfNotFound is overridden false, this will early exit if + // the album can't be found or if none of its trackSections includes the + // track for some reason. + withContainingTrackSection: ({ + to = '#trackSection', + earlyExitIfNotFound = true, + } = {}) => + Thing.composite.from(`Track.composite.withContainingTrackSection`, [ + Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['this', '#album.trackSections'], + mapContinuation: {to}, + + compute({ + this: track, + '#album.trackSections': trackSections, + }, continuation) { + if (!trackSections) { + return continuation.raise({to: null}); + } + + const trackSection = + trackSections.find(({tracks}) => tracks.includes(track)); + + if (trackSection) { + return continuation.raise({to: trackSection}); + } else if (earlyExitIfNotFound) { + return continuation.exit(null); + } else { + return continuation.raise({to: null}); + } + }, + }, + }, + ]), + // Just includes the original release of this track as a dependency, or // null, if it's not a rerelease. Note that this will early exit if the // original release is specified by reference and that reference doesn't -- cgit 1.3.0-6-gf8a5 From 618f49e0ddcea245a4e0972efe5450419b27c639 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 26 Aug 2023 21:31:07 -0300 Subject: data: Thing.composite.exposeDependencyOrContinue --- src/data/things/thing.js | 48 +++++++++++++++++++++++++++++++++++++++++++----- src/data/things/track.js | 10 +--------- 2 files changed, 44 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f5dc786e..f88e8726 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1077,6 +1077,8 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // -- Compositional steps for compositions to nest -- + // Provides dependencies exactly as they are (or null if not defined) to the // continuation. Although this can *technically* be used to alias existing // dependencies to some other name within the middle of a composition, it's @@ -1114,6 +1116,8 @@ export default class Thing extends CacheableObject { }; }, + // -- Compositional steps for top-level property descriptors -- + // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. // Since this serves as a base, specify a value for {update} to indicate @@ -1128,7 +1132,7 @@ export default class Thing extends CacheableObject { // of null. // exposeDependency: (dependency, {update = false} = {}) => ({ - annotation: `Thing.composite.expose`, + annotation: `Thing.composite.exposeDependency`, flags: {expose: true, update: !!update}, expose: { @@ -1142,10 +1146,42 @@ export default class Thing extends CacheableObject { : null), }), - // Exposes the update value of an {update: true} property, or continues if - // it's unavailable. By default, "unavailable" means value === null, but - // set {mode: 'empty'} to check with empty() instead, continuing for empty - // arrays also. + // Exposes a dependency as it is, or continues if it's unavailable. + // By default, "unavailable" means dependency === null; provide + // {mode: 'empty'} to check with empty() instead, continuing for + // empty arrays also. + exposeDependencyOrContinue(dependency, {mode = 'null'} = {}) { + if (mode !== 'null' && mode !== 'empty') { + throw new TypeError(`Expected mode to be null or empty`); + } + + return { + annotation: `Thing.composite.exposeDependencyOrContinue`, + flags: {expose: true, compose: true}, + expose: { + options: {mode}, + mapDependencies: {dependency}, + + compute({dependency, '#options': {mode}}, continuation) { + const shouldContinue = + (mode === 'empty' + ? empty(dependency) + : dependency === null); + + if (shouldContinue) { + return continuation(); + } else { + return continuation.exit(dependency); + } + }, + }, + }; + }, + + // Exposes the update value of an {update: true} property as it is, + // or continues if it's unavailable. By default, "unavailable" means + // value === null; provide {mode: 'empty'} to check with empty() instead, + // continuing for empty arrays also. exposeUpdateValueOrContinue({mode = 'null'} = {}) { if (mode !== 'null' && mode !== 'empty') { throw new TypeError(`Expected mode to be null or empty`); @@ -1173,6 +1209,8 @@ export default class Thing extends CacheableObject { }; }, + // -- Compositional steps for processing data -- + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist diff --git a/src/data/things/track.js b/src/data/things/track.js index 621044d5..228b2af1 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -191,15 +191,7 @@ export class Track extends Thing { ), date: Thing.composite.from(`Track.date`, [ - { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['dateFirstReleased'], - compute: ({dateFirstReleased}, continuation) => - dateFirstReleased ?? continuation(), - }, - }, - + Thing.composite.exposeDependencyOrContinue('dateFirstReleased'), Track.composite.withAlbumProperty('date'), Thing.composite.exposeDependency('#album.date'), ]), -- cgit 1.3.0-6-gf8a5 From 083a4b8c3a0e545a2d8195255d57c5b7e0c49028 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 27 Aug 2023 16:15:34 -0300 Subject: data: misc. additions, fixes & refactoring Thing.composite.from: * Transparently support expose.transform steps inside nested compositions, w/ various Thing.composite.from clean-up * Support continuation.raise() without provided dependencies * add Thing.composite.exposeConstant * add Thing.composite.withResultOfAvailabilityCheck * supports {mode: 'null' | 'empty' | 'falsy'} * works with dependency or update value * add Thing.composite.earlyExitWithoutDependency * refactor Thing.composite.exposeDependencyOrContinue * refactor Thing.composite.exposeUpdateValueOrContinue * add Track.withHasUniqueCoverArt * refactor Track.coverArtFileExtension * refactor Track.hasUniqueCoverArt --- src/data/things/thing.js | 433 +++++++++++++++++++++++++++++------------------ src/data/things/track.js | 137 +++++++-------- 2 files changed, 331 insertions(+), 239 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f88e8726..892a3a4b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -780,6 +780,16 @@ export default class Thing extends CacheableObject { break expose; } + if ( + step.expose.transform && + !step.expose.compute && + !base.flags.update && + !base.flags.compose + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + break expose; + } + if (step.expose.dependencies) { for (const dependency of step.expose.dependencies) { if (typeof dependency === 'string' && dependency.startsWith('#')) continue; @@ -794,26 +804,7 @@ export default class Thing extends CacheableObject { } } - let fn, type; - if (base.flags.update) { - if (step.expose.transform) { - type = 'transform'; - fn = step.expose.transform; - } else { - type = 'compute'; - fn = step.expose.compute; - } - } else { - if (step.expose.transform && !step.expose.compute) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - break expose; - } - - type = 'compute'; - fn = step.expose.compute; - } - - exposeSteps.push(step.expose); + exposeSteps.push(step); } }); } @@ -845,38 +836,38 @@ export default class Thing extends CacheableObject { function _filterDependencies(dependencies, step) { const filteredDependencies = - (step.dependencies - ? filterProperties(dependencies, step.dependencies) + (step.expose.dependencies + ? filterProperties(dependencies, step.expose.dependencies) : {}); - if (step.mapDependencies) { - for (const [to, from] of Object.entries(step.mapDependencies)) { + if (step.expose.mapDependencies) { + for (const [to, from] of Object.entries(step.expose.mapDependencies)) { filteredDependencies[to] = dependencies[from] ?? null; } } - if (step.options) { - filteredDependencies['#options'] = step.options; + if (step.expose.options) { + filteredDependencies['#options'] = step.expose.options; } return filteredDependencies; } function _assignDependencies(continuationAssignment, step) { - if (!step.mapContinuation) { + if (!step.expose.mapContinuation) { return continuationAssignment; } const assignDependencies = {}; - for (const [from, to] of Object.entries(step.mapContinuation)) { + for (const [from, to] of Object.entries(step.expose.mapContinuation)) { assignDependencies[to] = continuationAssignment[from] ?? null; } return assignDependencies; } - function _prepareContinuation(transform, step) { + function _prepareContinuation(transform) { const continuationStorage = { returnedWith: null, providedDependencies: null, @@ -930,27 +921,25 @@ export default class Thing extends CacheableObject { debug(() => color.bright(`begin composition (annotation: ${annotation})`)); - for (let i = 0; i < exposeSteps.length; i++) { + stepLoop: for (let i = 0; i < exposeSteps.length; i++) { const step = exposeSteps[i]; debug(() => [`step #${i+1}:`, step]); const transform = valueSoFar !== noTransformSymbol && - step.transform; + step.expose.transform; const filteredDependencies = _filterDependencies(dependencies, step); - const {continuation, continuationStorage} = _prepareContinuation(transform, step); + const {continuation, continuationStorage} = _prepareContinuation(transform); - if (transform) { - debug(() => `step #${i+1} - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - } else { - debug(() => `step #${i+1} - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - } + debug(() => + `step #${i+1} - ${transform ? 'transform' : 'compute'} ` + + `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); const result = (transform - ? step.transform(valueSoFar, filteredDependencies, continuation) - : step.compute(filteredDependencies, continuation)); + ? step.expose.transform(valueSoFar, filteredDependencies, continuation) + : step.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { if (base.flags.compose) { @@ -964,39 +953,34 @@ export default class Thing extends CacheableObject { return result; } - if (continuationStorage.returnedWith === 'exit') { - debug(() => `step #${i+1} - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationStorage.providedValue; - } - - if (continuationStorage.returnedWith === 'raise') { - if (transform) { - valueSoFar = continuationStorage.providedValue; - } - - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - - debug(() => `step #${i+1} - result: raise`); - - break; - } - - if (continuationStorage.returnedWith === 'continuation') { - if (transform) { - valueSoFar = continuationStorage.providedValue; - } - - debug(() => `step #${i+1} - result: continuation`); + switch (continuationStorage.returnedWith) { + case 'exit': + debug(() => `step #${i+1} - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationStorage.providedValue; + + case 'raise': + debug(() => `step #${i+1} - result: raise`); + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step) ?? {}; + if (transform) valueSoFar = continuationStorage.providedValue; + break stepLoop; + + case 'continuation': + if (transform) { + valueSoFar = continuationStorage.providedValue; + } - if (continuationStorage.providedDependencies) { - const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - Object.assign(dependencies, assignDependencies); + if (continuationStorage.providedDependencies) { + const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + Object.assign(dependencies, assignDependencies); + debug(() => `step #${i+1} - result: continuation`); + debug(() => [`assign dependencies:`, assignDependencies]); + } else { + debug(() => `step #${i+1} - result: continuation (no provided dependencies)`); + } - debug(() => [`assign dependencies:`, assignDependencies]); - } + break; } } @@ -1008,53 +992,50 @@ export default class Thing extends CacheableObject { debug(() => `completed all steps, reached base`); - const filteredDependencies = _filterDependencies(dependencies, base.expose); + const filteredDependencies = _filterDependencies(dependencies, base); - // Note: base.flags.compose is not compatible with base.flags.update. - if (base.expose.transform) { - debug(() => `base - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + const transform = + valueSoFar !== noTransformSymbol && + base.expose.transform; - const result = base.expose.transform(valueSoFar, filteredDependencies); + debug(() => + `base - ${transform ? 'transform' : 'compute'} ` + + `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); - - return result; - } else if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(false, base.expose); - - debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + if (base.flags.compose) { + const {continuation, continuationStorage} = _prepareContinuation(transform); - const result = base.expose.compute(filteredDependencies, continuation); + const result = + (transform + ? base.expose.transform(valueSoFar, filteredDependencies, continuation) + : base.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); } - if (continuationStorage.returnedWith === 'continuation') { - throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); - } - - if (continuationStorage.returnedWith === 'exit') { - debug(() => `base - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationStorage.providedValue; - } - - if (continuationStorage.returnedWith === 'raise') { - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base.expose); - - debug(() => `base - result: raise`); - debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationIfApplicable(exportDependencies); + switch (continuationStorage.returnedWith) { + case 'continuation': + throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + + case 'exit': + debug(() => `base - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationStorage.providedValue; + + case 'raise': + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); + debug(() => `base - result: raise`); + debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationIfApplicable(exportDependencies); } } else { - debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - - const result = base.expose.compute(filteredDependencies); + const result = + (transform + ? base.expose.transform(valueSoFar, filteredDependencies) + : base.expose.compute(filteredDependencies)); debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); debug(() => color.bright(`end composition (annotation: ${annotation})`)); @@ -1063,14 +1044,23 @@ export default class Thing extends CacheableObject { } } - if (base.flags.update) { - expose.transform = - (value, initialDependencies, continuationIfApplicable) => - _computeOrTransform(value, initialDependencies, continuationIfApplicable); + const transformFn = + (value, initialDependencies, continuationIfApplicable) => + _computeOrTransform(value, initialDependencies, continuationIfApplicable); + + const computeFn = + (initialDependencies, continuationIfApplicable) => + _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); + + if (base.flags.compose) { + if (exposeSteps.some(step => step.expose.transform)) { + expose.transform = transformFn; + } + expose.compute = computeFn; + } else if (base.flags.update) { + expose.transform = transformFn; } else { - expose.compute = - (initialDependencies, continuationIfApplicable) => - _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); + expose.compute = computeFn; } } @@ -1146,68 +1136,177 @@ export default class Thing extends CacheableObject { : null), }), - // Exposes a dependency as it is, or continues if it's unavailable. - // By default, "unavailable" means dependency === null; provide - // {mode: 'empty'} to check with empty() instead, continuing for - // empty arrays also. - exposeDependencyOrContinue(dependency, {mode = 'null'} = {}) { - if (mode !== 'null' && mode !== 'empty') { - throw new TypeError(`Expected mode to be null or empty`); + // Exposes a constant value exactly as it is; like exposeDependency, this + // is typically the base of a composition serving as a particular property + // descriptor. It generally follows steps which will conditionally early + // exit with some other value, with the exposeConstant base serving as the + // fallback default value. Like exposeDependency, set {update} to true or + // an object to indicate that the property as a whole updates. + exposeConstant: (value, {update = false} = {}) => ({ + annotation: `Thing.composite.exposeConstant`, + flags: {expose: true, update: !!update}, + + expose: { + options: {value}, + compute: ({'#options': {value}}) => value, + }, + + update: + (typeof update === 'object' + ? update + : null), + }), + + // Checks the availability of a dependency or the update value and provides + // the result to later steps under '#availability' (by default). This is + // mainly intended for use by the more specific utilities, which you should + // consider using instead. Customize {mode} to select one of these modes, + // or leave unset and default to 'null': + // + // * 'null': Check that the value isn't null. + // * 'empty': Check that the value is neither null nor an empty array. + // * 'falsy': Check that the value isn't false when treated as a boolean + // (nor an empty array). Keep in mind this will also be false + // for values like zero and the empty string! + // + withResultOfAvailabilityCheck({ + fromUpdateValue, + fromDependency, + mode = 'null', + to = '#availability', + }) { + if (!['null', 'empty', 'falsy'].includes(mode)) { + throw new TypeError(`Expected mode to be null, empty, or falsy`); } - return { - annotation: `Thing.composite.exposeDependencyOrContinue`, - flags: {expose: true, compose: true}, - expose: { - options: {mode}, - mapDependencies: {dependency}, - - compute({dependency, '#options': {mode}}, continuation) { - const shouldContinue = - (mode === 'empty' - ? empty(dependency) - : dependency === null); - - if (shouldContinue) { - return continuation(); - } else { - return continuation.exit(dependency); - } - }, - }, + if (fromUpdateValue && fromDependency) { + throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); + } + + if (!fromUpdateValue && !fromDependency) { + throw new TypeError(`Missing dependency name (or fromUpdateValue)`); + } + + const checkAvailability = (value, mode) => { + switch (mode) { + case 'null': return value !== null; + case 'empty': return !empty(value); + case 'falsy': return !empty(value) && !!value; + default: return false; + } }; + + if (fromDependency) { + return { + annotation: `Thing.composite.withResultOfCommonComparison.fromDependency`, + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {from: fromDependency}, + mapContinuation: {to}, + options: {mode}, + compute: ({from, '#options': {mode}}, continuation) => + continuation({to: checkAvailability(from, mode)}), + }, + }; + } else { + return { + annotation: `Thing.composite.withResultOfCommonComparison.fromUpdateValue`, + flags: {expose: true, compose: true}, + expose: { + mapContinuation: {to}, + options: {mode}, + transform: (value, {'#options': {mode}}, continuation) => + continuation(value, {to: checkAvailability(value, mode)}), + }, + }; + } }, + // Exposes a dependency as it is, or continues if it's unavailable. + // See withResultOfAvailabilityCheck for {mode} options! + exposeDependencyOrContinue: (dependency, {mode = 'null'} = {}) => + Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromDependency: dependency, + mode, + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {dependency}, + compute: ({dependency}, continuation) => + continuation.exit(dependency), + }, + }, + ]), + // Exposes the update value of an {update: true} property as it is, - // or continues if it's unavailable. By default, "unavailable" means - // value === null; provide {mode: 'empty'} to check with empty() instead, - // continuing for empty arrays also. - exposeUpdateValueOrContinue({mode = 'null'} = {}) { - if (mode !== 'null' && mode !== 'empty') { - throw new TypeError(`Expected mode to be null or empty`); - } + // or continues if it's unavailable. See withResultOfAvailabilityCheck + // for {mode} options! + exposeUpdateValueOrContinue: ({mode = 'null'} = {}) => + Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromUpdateValue: true, + mode, + }), - return { - annotation: `Thing.composite.exposeUpdateValueOrContinue`, - flags: {expose: true, compose: true}, - expose: { - options: {mode}, - - transform(value, {'#options': {mode}}, continuation) { - const shouldContinue = - (mode === 'empty' - ? empty(value) - : value === null); - - if (shouldContinue) { - return continuation(value); - } else { - return continuation.exit(value); - } - } + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, }, - }; - }, + + { + flags: {expose: true, compose: true}, + expose: { + transform: (value, {}, continuation) => + continuation.exit(value), + }, + }, + ]), + + // Early exits if a dependency isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + earlyExitWithoutDependency: (dependency, {mode = 'null', value = null} = {}) => + Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromDependency: dependency, + mode, + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + options: {value}, + + compute: ({ + '#availability': availability, + '#options': {value}, + }, continuation) => + (availability + ? continuation() + : continuation.exit(value)), + }, + }, + ]), // -- Compositional steps for processing data -- diff --git a/src/data/things/track.js b/src/data/things/track.js index 228b2af1..dc1f5f2a 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -76,40 +76,24 @@ export class Track extends Thing { disableUniqueCoverArt: Thing.common.flag(), // File extension for track's corresponding media file. This represents the - // track's unique cover artwork, if any, and does not inherit the cover's - // main artwork. (It does inherit `trackCoverArtFileExtension` if present - // on the album.) + // 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: Thing.composite.from(`Track.coverArtFileExtension`, [ - Track.composite.withAlbumProperties({ - properties: [ - 'trackCoverArtistContribsByRef', - 'trackCoverArtFileExtension', - ], - }), + // No cover art file extension if the track doesn't have unique artwork + // in the first place. + Track.composite.withHasUniqueCoverArt(), + Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), - { - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: { - dependencies: [ - 'coverArtistContribsByRef', - 'disableUniqueCoverArt', - '#album.trackCoverArtistContribsByRef', - '#album.trackCoverArtFileExtension', - ], + // Expose custom coverArtFileExtension update value first. + Thing.composite.exposeUpdateValueOrContinue(), - transform(coverArtFileExtension, { - coverArtistContribsByRef, - disableUniqueCoverArt, - '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, - '#album.trackCoverArtFileExtension': trackCoverArtFileExtension, - }) { - if (disableUniqueCoverArt) return null; - if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; - return coverArtFileExtension ?? trackCoverArtFileExtension ?? 'jpg'; - }, - }, - }, + // Expose album's trackCoverArtFileExtension if no update value set. + Track.composite.withAlbumProperty('trackCoverArtFileExtension'), + Thing.composite.exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), + + // Fallback to 'jpg'. + Thing.composite.exposeConstant('jpg'), ]), // Date of cover art release. Like coverArtFileExtension, this represents @@ -204,47 +188,8 @@ export class Track extends Thing { // the usual hasCoverArt to emphasize that it does not inherit from the // album.) hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [ - { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['disableUniqueCoverArt'], - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? false - : continuation()), - }, - }, - - Thing.composite.withResolvedContribs({ - from: 'coverArtistContribsByRef', - to: '#coverArtistContribs', - }), - - { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#coverArtistContribs'], - compute: ({'#coverArtistContribs': coverArtistContribs}, continuation) => - (empty(coverArtistContribs) - ? continuation() - : true), - }, - }, - - Track.composite.withAlbumProperties({ - properties: ['trackCoverArtistContribs'], - }), - - { - flags: {expose: true}, - expose: { - dependencies: ['#album.trackCoverArtistContribs'], - compute: ({'#album.trackCoverArtistContribs': trackCoverArtistContribs}) => - (empty(trackCoverArtistContribs) - ? false - : true), - }, - }, + Track.composite.withHasUniqueCoverArt(), + Thing.composite.exposeDependency('#hasUniqueCoverArt'), ]), originalReleaseTrack: Thing.common.dynamicThingFromSingleReference( @@ -609,6 +554,54 @@ export class Track extends Thing { [outputDependency]: '#originalRelease', }), ]), + + // The algorithm for checking if a track has unique cover art is used in a + // couple places, so it's defined in full as a compositional step. + withHasUniqueCoverArt: ({to = '#hasUniqueCoverArt'} = {}) => + Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['disableUniqueCoverArt'], + mapContinuation: {to}, + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? continuation.raise({to: false}) + : continuation()), + }, + }, + + Thing.composite.withResolvedContribs({ + from: 'coverArtistContribsByRef', + to: '#coverArtistContribs', + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#coverArtistContribs'], + mapContinuation: {to}, + compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : continuation.raise({to: true})), + }, + }, + + Track.composite.withAlbumProperty('trackCoverArtistContribs'), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#album.trackCoverArtistContribs'], + mapContinuation: {to}, + compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => + (empty(contribsFromAlbum) + ? continuation.raise({to: false}) + : continuation.raise({to: true})), + }, + }, + ]), }; [inspect.custom]() { -- cgit 1.3.0-6-gf8a5 From 29580733b79872333f3f9e45d50d987218d334ea Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 28 Aug 2023 13:19:57 -0300 Subject: data: fix annotation typo --- src/data/things/thing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 892a3a4b..78ff4c81 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1198,7 +1198,7 @@ export default class Thing extends CacheableObject { if (fromDependency) { return { - annotation: `Thing.composite.withResultOfCommonComparison.fromDependency`, + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, @@ -1210,7 +1210,7 @@ export default class Thing extends CacheableObject { }; } else { return { - annotation: `Thing.composite.withResultOfCommonComparison.fromUpdateValue`, + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { mapContinuation: {to}, -- cgit 1.3.0-6-gf8a5 From 895712f5a0381c41557c6d306d6697019368bb7b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 30 Aug 2023 15:33:46 -0300 Subject: data: clean up Thing.composite.from debug messaging * print annotation next to every log message, instead of just the begin/end messages * add Thing.composite.debug() to conveniently wrap one property access * don't output (and don't access) track album in inspect.custom when depth < 0 --- src/data/things/thing.js | 69 ++++++++++++++++++++++++++++++++---------------- src/data/things/track.js | 40 ++++++++++------------------ 2 files changed, 60 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 78ff4c81..4fd6a26a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -719,11 +719,18 @@ export default class Thing extends CacheableObject { from(firstArg, secondArg) { const debug = fn => { if (Thing.composite.from.debug === true) { + const label = + (annotation + ? color.dim(`[composite: ${annotation}]`) + : color.dim(`[composite]`)); const result = fn(); if (Array.isArray(result)) { - console.log(`[composite]`, ...result); + console.log(label, ...result.map(value => + (typeof value === 'object' + ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) + : value))); } else { - console.log(`[composite]`, result); + console.log(label, result); } } }; @@ -919,7 +926,7 @@ export default class Thing extends CacheableObject { let valueSoFar = value; // Set only for {update: true} compositions let exportDependencies = null; // Set only for {compose: true} compositions - debug(() => color.bright(`begin composition (annotation: ${annotation})`)); + debug(() => color.bright(`begin composition`)); stepLoop: for (let i = 0; i < exposeSteps.length; i++) { const step = exposeSteps[i]; @@ -932,9 +939,9 @@ export default class Thing extends CacheableObject { const filteredDependencies = _filterDependencies(dependencies, step); const {continuation, continuationStorage} = _prepareContinuation(transform); - debug(() => - `step #${i+1} - ${transform ? 'transform' : 'compute'} ` + - `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + debug(() => [ + `step #${i+1} - ${transform ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); const result = (transform @@ -946,18 +953,15 @@ export default class Thing extends CacheableObject { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); } - debug(() => `step #${i+1} - early-exit (inferred)`); - debug(() => `early-exit: ${inspect(result, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - + debug(() => [`step #${i+1} - early-exit (inferred) ->`, result]); + debug(() => color.bright(`end composition`)); return result; } switch (continuationStorage.returnedWith) { case 'exit': - debug(() => `step #${i+1} - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`step #${i+1} - result: early-exit (explicit) ->`, continuationStorage.providedValue]); + debug(() => color.bright(`end composition`)); return continuationStorage.providedValue; case 'raise': @@ -986,7 +990,7 @@ export default class Thing extends CacheableObject { if (exportDependencies) { debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => color.bright(`end composition`)); return continuationIfApplicable(exportDependencies); } @@ -998,9 +1002,9 @@ export default class Thing extends CacheableObject { valueSoFar !== noTransformSymbol && base.expose.transform; - debug(() => - `base - ${transform ? 'transform' : 'compute'} ` + - `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + debug(() => [ + `base - ${transform ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); if (base.flags.compose) { const {continuation, continuationStorage} = _prepareContinuation(transform); @@ -1020,15 +1024,15 @@ export default class Thing extends CacheableObject { case 'exit': debug(() => `base - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`early-exit:`, continuationStorage.providedValue]); + debug(() => color.bright(`end composition`)); return continuationStorage.providedValue; case 'raise': exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); debug(() => `base - result: raise`); - debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`raise dependencies:`, exportDependencies]); + debug(() => color.bright(`end composition`)); return continuationIfApplicable(exportDependencies); } } else { @@ -1037,8 +1041,8 @@ export default class Thing extends CacheableObject { ? base.expose.transform(valueSoFar, filteredDependencies) : base.expose.compute(filteredDependencies)); - debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`base - non-compose (final) result:`, result]); + debug(() => color.bright(`end composition`)); return result; } @@ -1067,6 +1071,25 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // Evaluates a function with composite debugging enabled, turns debugging + // off again, and returns the result of the function. This is mostly syntax + // sugar, but also helps avoid unit tests avoid accidentally printing debug + // info for a bunch of unrelated composites (due to property enumeration + // when displaying an unexpected result). Use as so: + // + // Without debugging: + // t.same(thing.someProp, value) + // + // With debugging: + // t.same(Thing.composite.debug(() => thing.someProp), value) + // + debug(fn) { + Thing.composite.from.debug = true; + const value = fn(); + Thing.composite.from.debug = false; + return value; + }, + // -- Compositional steps for compositions to nest -- // Provides dependencies exactly as they are (or null if not defined) to the diff --git a/src/data/things/track.js b/src/data/things/track.js index dc1f5f2a..8ddf3624 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -604,38 +604,26 @@ export class Track extends Thing { ]), }; - [inspect.custom]() { - const base = Thing.prototype[inspect.custom].apply(this); + [inspect.custom](depth) { + const parts = []; - const rereleasePart = - (this.originalReleaseTrackByRef - ? `${color.yellow('[rerelease]')} ` - : ``); + parts.push(Thing.prototype[inspect.custom].apply(this)); - const {album, dataSourceAlbum} = this; + if (this.originalReleaseTrackByRef) { + parts.unshift(`${color.yellow('[rerelease]')} `); + } - const albumName = - (album - ? album.name - : dataSourceAlbum?.name); - - const albumIndex = - albumName && - (album - ? album.tracks.indexOf(this) - : dataSourceAlbum.tracks.indexOf(this)); - - const trackNum = - albumName && + let album; + if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) { + const albumName = album.name; + const albumIndex = album.tracks.indexOf(this); + const trackNum = (albumIndex === -1 ? '#?' : `#${albumIndex + 1}`); + parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); + } - const albumPart = - albumName - ? ` (${color.yellow(trackNum)} in ${color.green(albumName)})` - : ``; - - return rereleasePart + base + albumPart; + return parts.join(''); } } -- cgit 1.3.0-6-gf8a5 From 3336d5f15e29350656273a37c0a1c7a69d24663b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 30 Aug 2023 15:56:04 -0300 Subject: data: Thing.composite.from: fix including '#' deps from base ...in the final composition's dependencies. --- src/data/things/thing.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 4fd6a26a..25d8c8a3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -756,7 +756,14 @@ export default class Thing extends CacheableObject { } const exposeSteps = []; - const exposeDependencies = new Set(base.expose?.dependencies); + const exposeDependencies = new Set(); + + if (base.expose?.dependencies) { + for (const dependency of base.expose.dependencies) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } if (base.expose?.mapDependencies) { for (const dependency of Object.values(base.expose.mapDependencies)) { -- cgit 1.3.0-6-gf8a5 From 7d6d8a2839ece38c4a70bd9e3fda73b2e0aa39b8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 30 Aug 2023 15:57:22 -0300 Subject: data: Thing.composite.earlyExitWithoutDependency: latest syntax --- src/data/things/thing.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 25d8c8a3..6bdc897f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1321,19 +1321,25 @@ export default class Thing extends CacheableObject { mode, }), + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + { flags: {expose: true, compose: true}, expose: { dependencies: ['#availability'], options: {value}, - compute: ({ - '#availability': availability, - '#options': {value}, - }, continuation) => - (availability - ? continuation() - : continuation.exit(value)), + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), }, }, ]), -- cgit 1.3.0-6-gf8a5 From 011c197aeedab56d501b03b800433dd0cd9bc4f7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 30 Aug 2023 16:28:47 -0300 Subject: data: always define composite utilities with `key() {}` syntax Sublime Text doesn't index the key in `key: () => {}` as a symbol for function definitions if the parameter list takes up more than one line, but always works for `key() {}`. This also just makes it a little easier to add "preamble" before the main return value, when relevant. Consistent syntax is usually a plus for recurring behavioral forms! --- src/data/things/thing.js | 121 +++++++++++++++++++++++++++------------------- src/data/things/track.js | 123 ++++++++++++++++++++++++++--------------------- 2 files changed, 140 insertions(+), 104 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 6bdc897f..cd62288e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1151,20 +1151,24 @@ export default class Thing extends CacheableObject { // compositional step, the property will be exposed as undefined instead // of null. // - exposeDependency: (dependency, {update = false} = {}) => ({ - annotation: `Thing.composite.exposeDependency`, - flags: {expose: true, update: !!update}, + exposeDependency(dependency, { + update = false, + } = {}) { + return { + annotation: `Thing.composite.exposeDependency`, + flags: {expose: true, update: !!update}, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, - }, + expose: { + mapDependencies: {dependency}, + compute: ({dependency}) => dependency, + }, - update: - (typeof update === 'object' - ? update - : null), - }), + update: + (typeof update === 'object' + ? update + : null), + }; + }, // Exposes a constant value exactly as it is; like exposeDependency, this // is typically the base of a composition serving as a particular property @@ -1172,20 +1176,24 @@ export default class Thing extends CacheableObject { // exit with some other value, with the exposeConstant base serving as the // fallback default value. Like exposeDependency, set {update} to true or // an object to indicate that the property as a whole updates. - exposeConstant: (value, {update = false} = {}) => ({ - annotation: `Thing.composite.exposeConstant`, - flags: {expose: true, update: !!update}, + exposeConstant(value, { + update = false, + } = {}) { + return { + annotation: `Thing.composite.exposeConstant`, + flags: {expose: true, update: !!update}, - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, - }, + expose: { + options: {value}, + compute: ({'#options': {value}}) => value, + }, - update: - (typeof update === 'object' - ? update - : null), - }), + update: + (typeof update === 'object' + ? update + : null), + }; + }, // Checks the availability of a dependency or the update value and provides // the result to later steps under '#availability' (by default). This is @@ -1254,8 +1262,10 @@ export default class Thing extends CacheableObject { // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! - exposeDependencyOrContinue: (dependency, {mode = 'null'} = {}) => - Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ + exposeDependencyOrContinue(dependency, { + mode = 'null', + } = {}) { + return Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ Thing.composite.withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -1280,13 +1290,16 @@ export default class Thing extends CacheableObject { continuation.exit(dependency), }, }, - ]), + ]); + }, // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck // for {mode} options! - exposeUpdateValueOrContinue: ({mode = 'null'} = {}) => - Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ + exposeUpdateValueOrContinue({ + mode = 'null', + } = {}) { + return Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ Thing.composite.withResultOfAvailabilityCheck({ fromUpdateValue: true, mode, @@ -1310,12 +1323,16 @@ export default class Thing extends CacheableObject { continuation.exit(value), }, }, - ]), + ]); + }, // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutDependency: (dependency, {mode = 'null', value = null} = {}) => - Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + earlyExitWithoutDependency(dependency, { + mode = 'null', + value = null, + } = {}) { + return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ Thing.composite.withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -1342,7 +1359,8 @@ export default class Thing extends CacheableObject { continuation.exit(value), }, }, - ]), + ]); + }, // -- Compositional steps for processing data -- @@ -1350,20 +1368,22 @@ export default class Thing extends CacheableObject { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. - withResolvedContribs: ({from, to}) => ({ - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, + withResolvedContribs({from, to}) { + return { + annotation: `Thing.composite.withResolvedContribs`, + flags: {expose: true, compose: true}, - expose: { - dependencies: ['artistData'], - mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => - continuation({ - to: Thing.findArtistsFromContribs(from, artistData), - }), - }, - }), + expose: { + dependencies: ['artistData'], + mapDependencies: {from}, + mapContinuation: {to}, + compute: ({artistData, from}, continuation) => + continuation({ + to: Thing.findArtistsFromContribs(from, artistData), + }), + }, + }; + }, // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the @@ -1372,14 +1392,14 @@ export default class Thing extends CacheableObject { // Otherwise, the data object is provided on the output dependency; // or null, if the reference doesn't match anything or itself was null // to begin with. - withResolvedReference: ({ + withResolvedReference({ ref, data, to, find: findFunction, earlyExitIfNotFound = false, - }) => - Thing.composite.from(`Thing.composite.withResolvedReference`, [ + }) { + return Thing.composite.from(`Thing.composite.withResolvedReference`, [ { flags: {expose: true, compose: true}, expose: { @@ -1423,6 +1443,7 @@ export default class Thing extends CacheableObject { }, }, }, - ]), + ]); + }, }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 8ddf3624..cdc9cec3 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -366,8 +366,11 @@ export class Track extends Thing { // dependencies provided. If allowOverride is true, then the continuation // will also be called if the original release exposed the requested // property as null. - inheritFromOriginalRelease: ({property: originalProperty, allowOverride = false}) => - Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ + inheritFromOriginalRelease({ + property: originalProperty, + allowOverride = false, + }) { + return Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ Track.composite.withOriginalRelease(), { @@ -386,56 +389,59 @@ export class Track extends Thing { }, }, } - ]), + ]); + }, // Gets the track's album. Unless earlyExitIfNotFound is overridden false, // this will early exit with null in two cases - albumData being missing, // or not including an album whose .tracks array includes this track. - withAlbum: ({to = '#album', earlyExitIfNotFound = true} = {}) => ({ - annotation: `Track.composite.withAlbum`, - flags: {expose: true, compose: true}, - - expose: { - dependencies: ['this', 'albumData'], - mapContinuation: {to}, - options: {earlyExitIfNotFound}, - - compute({ - this: track, - albumData, - '#options': {earlyExitIfNotFound}, - }, continuation) { - if (empty(albumData)) { - return ( - (earlyExitIfNotFound - ? continuation.exit(null) - : continuation({to: null}))); - } - - const album = - albumData?.find(album => album.tracks.includes(track)); - - if (!album) { - return ( - (earlyExitIfNotFound - ? continuation.exit(null) - : continuation({to: null}))); - } + withAlbum({to = '#album', earlyExitIfNotFound = true} = {}) { + return { + annotation: `Track.composite.withAlbum`, + flags: {expose: true, compose: true}, - return continuation({to: album}); + expose: { + dependencies: ['this', 'albumData'], + mapContinuation: {to}, + options: {earlyExitIfNotFound}, + + compute({ + this: track, + albumData, + '#options': {earlyExitIfNotFound}, + }, continuation) { + if (empty(albumData)) { + return ( + (earlyExitIfNotFound + ? continuation.exit(null) + : continuation({to: null}))); + } + + const album = + albumData?.find(album => album.tracks.includes(track)); + + if (!album) { + return ( + (earlyExitIfNotFound + ? continuation.exit(null) + : continuation({to: null}))); + } + + return continuation({to: album}); + }, }, - }, - }), + }; + }, // Gets a single property from this track's album, providing it as the same // property name prefixed with '#album.' (by default). If the track's album // isn't available, and earlyExitIfNotFound hasn't been set, the property // will be provided as null. - withAlbumProperty: (property, { + withAlbumProperty(property, { to = '#album.' + property, earlyExitIfNotFound = false, - } = {}) => - Thing.composite.from(`Track.composite.withAlbumProperty`, [ + } = {}) { + return Thing.composite.from(`Track.composite.withAlbumProperty`, [ Track.composite.withAlbum({earlyExitIfNotFound}), { @@ -454,18 +460,19 @@ export class Track extends Thing { : continuation.raise({to: null})), }, }, - ]), + ]); + }, // Gets the listed properties from this track's album, providing them as // dependencies (by default) with '#album.' prefixed before each property // name. If the track's album isn't available, and earlyExitIfNotFound // hasn't been set, the same dependency names will be provided as null. - withAlbumProperties: ({ + withAlbumProperties({ properties, prefix = '#album', earlyExitIfNotFound = false, - }) => - Thing.composite.from(`Track.composite.withAlbumProperties`, [ + }) { + return Thing.composite.from(`Track.composite.withAlbumProperties`, [ Track.composite.withAlbum({earlyExitIfNotFound}), { @@ -494,17 +501,18 @@ export class Track extends Thing { }, }, }, - ]), + ]); + }, // Gets the track section containing this track from its album's track list. // Unless earlyExitIfNotFound is overridden false, this will early exit if // the album can't be found or if none of its trackSections includes the // track for some reason. - withContainingTrackSection: ({ + withContainingTrackSection({ to = '#trackSection', earlyExitIfNotFound = true, - } = {}) => - Thing.composite.from(`Track.composite.withContainingTrackSection`, [ + } = {}) { + return Thing.composite.from(`Track.composite.withContainingTrackSection`, [ Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}), { @@ -534,14 +542,17 @@ export class Track extends Thing { }, }, }, - ]), + ]); + }, // Just includes the original release of this track as a dependency, or // null, if it's not a rerelease. Note that this will early exit if the // original release is specified by reference and that reference doesn't // resolve to anything. Outputs to '#originalRelease' by default. - withOriginalRelease: ({to: outputDependency = '#originalRelease'} = {}) => - Thing.composite.from(`Track.composite.withOriginalRelease`, [ + withOriginalRelease({ + to = '#originalRelease', + } = {}) { + return Thing.composite.from(`Track.composite.withOriginalRelease`, [ Thing.composite.withResolvedReference({ ref: 'originalReleaseTrackByRef', data: 'trackData', @@ -553,12 +564,15 @@ export class Track extends Thing { Thing.composite.export({ [outputDependency]: '#originalRelease', }), - ]), + ]); + }, // The algorithm for checking if a track has unique cover art is used in a // couple places, so it's defined in full as a compositional step. - withHasUniqueCoverArt: ({to = '#hasUniqueCoverArt'} = {}) => - Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ + withHasUniqueCoverArt({ + to = '#hasUniqueCoverArt', + } = {}) { + return Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ { flags: {expose: true, compose: true}, expose: { @@ -601,7 +615,8 @@ export class Track extends Thing { : continuation.raise({to: true})), }, }, - ]), + ]); + }, }; [inspect.custom](depth) { -- cgit 1.3.0-6-gf8a5 From b38a3c787dc4fbec5e2dc0c297bbcd3ceae83349 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 30 Aug 2023 16:31:54 -0300 Subject: data: update Track.otherReleases implementation Also adds {selfIfOriginal} option to withOriginalRelease(). --- src/data/things/track.js | 81 +++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index cdc9cec3..3b2029e8 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -198,33 +198,30 @@ export class Track extends Thing { find.track ), - otherReleases: { - flags: {expose: true}, + otherReleases: + Thing.composite.from(`Track.otherReleases`, [ + Thing.composite.earlyExitWithoutDependency('trackData'), + Track.composite.withOriginalRelease({selfIfOriginal: true}), - expose: { - dependencies: ['this', 'originalReleaseTrackByRef', 'trackData'], - - compute: ({ - this: t1, - originalReleaseTrackByRef: t1origRef, - trackData, - }) => { - if (!trackData) { - return []; - } - - const t1orig = find.track(t1origRef, trackData); - - return [ - t1orig, - ...trackData.filter((t2) => { - const {originalReleaseTrack: t2orig} = t2; - return t2 !== t1 && t2orig && (t2orig === t1orig || t2orig === t1); - }), - ].filter(Boolean); + { + flags: {expose: true}, + expose: { + dependencies: ['this', 'trackData', '#originalRelease'], + compute: ({ + this: thisTrack, + trackData, + '#originalRelease': originalRelease, + }) => + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), + }, }, - }, - }, + ]), artistContribs: Thing.composite.from(`Track.artistContribs`, [ Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), @@ -545,12 +542,15 @@ export class Track extends Thing { ]); }, - // Just includes the original release of this track as a dependency, or - // null, if it's not a rerelease. Note that this will early exit if the - // original release is specified by reference and that reference doesn't - // resolve to anything. Outputs to '#originalRelease' by default. + // Just includes the original release of this track as a dependency. + // If this track isn't a rerelease, then it'll provide null, unless the + // {selfIfOriginal} option is set, in which case it'll provide this track + // itself. Note that this will early exit if the original release is + // specified by reference and that reference doesn't resolve to anything. + // Outputs to '#originalRelease' by default. withOriginalRelease({ to = '#originalRelease', + selfIfOriginal = false, } = {}) { return Thing.composite.from(`Track.composite.withOriginalRelease`, [ Thing.composite.withResolvedReference({ @@ -561,9 +561,26 @@ export class Track extends Thing { earlyExitIfNotFound: true, }), - Thing.composite.export({ - [outputDependency]: '#originalRelease', - }), + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['this', '#originalRelease'], + options: {selfIfOriginal}, + mapContinuation: {to}, + compute: ({ + this: track, + '#originalRelease': originalRelease, + '#options': {selfIfOriginal}, + }, continuation) => + continuation.raise({ + to: + (originalRelease ?? + (selfIfOriginal + ? track + : null)), + }), + }, + }, ]); }, -- cgit 1.3.0-6-gf8a5 From 1cf06b4898b517993a171a5f6c39d00609105253 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 30 Aug 2023 16:32:40 -0300 Subject: data, infra: only make exposed properties enumerable This prevents them from being displayed in, for example, node-tap mismatched test case output. AFAIK, we generally don't depend on the enumerability of properties anywhere in hsmusic's codebase, and it doesn't really make sense for unexposed properties to be enumerable in the first place. --- src/data/things/cacheable-object.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 24a6cf01..62c23d13 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -141,7 +141,7 @@ export default class CacheableObject { const definition = { configurable: false, - enumerable: true, + enumerable: flags.expose, }; if (flags.update) { -- cgit 1.3.0-6-gf8a5 From c98a1a5faa40bfad79bc3b07aa8e9d53111b10a7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 30 Aug 2023 16:46:58 -0300 Subject: data: Track: misc. minor fixes --- src/data/things/track.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 3b2029e8..724c8606 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -200,7 +200,7 @@ export class Track extends Thing { otherReleases: Thing.composite.from(`Track.otherReleases`, [ - Thing.composite.earlyExitWithoutDependency('trackData'), + Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}), Track.composite.withOriginalRelease({selfIfOriginal: true}), { @@ -242,7 +242,7 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({properties: ['artistContribs']}), + Track.composite.withAlbumProperty('artistContribs'), { flags: {expose: true}, -- cgit 1.3.0-6-gf8a5 From d4e5d37ef9bc09789715f978e1b636bc2a1f8e97 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 30 Aug 2023 16:47:16 -0300 Subject: data: update Track.composite.withAlbum implementation --- src/data/things/track.js | 84 ++++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 724c8606..22043688 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -393,41 +393,61 @@ export class Track extends Thing { // this will early exit with null in two cases - albumData being missing, // or not including an album whose .tracks array includes this track. withAlbum({to = '#album', earlyExitIfNotFound = true} = {}) { - return { - annotation: `Track.composite.withAlbum`, - flags: {expose: true, compose: true}, + return Thing.composite.from(`Track.composite.withAlbum`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromDependency: 'albumData', + mode: 'empty', + to: '#albumDataAvailability', + }), - expose: { - dependencies: ['this', 'albumData'], - mapContinuation: {to}, - options: {earlyExitIfNotFound}, - - compute({ - this: track, - albumData, - '#options': {earlyExitIfNotFound}, - }, continuation) { - if (empty(albumData)) { - return ( - (earlyExitIfNotFound - ? continuation.exit(null) - : continuation({to: null}))); - } - - const album = - albumData?.find(album => album.tracks.includes(track)); - - if (!album) { - return ( - (earlyExitIfNotFound - ? continuation.exit(null) - : continuation({to: null}))); - } - - return continuation({to: album}); + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#albumDataAvailability'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + compute: ({ + '#albumDataAvailability': albumDataAvailability, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (albumDataAvailability + ? continuation() + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: null}))), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['this', 'albumData'], + compute: ({this: track, albumData}, continuation) => + continuation({ + '#album': + albumData.find(album => album.tracks.includes(track)), + }), }, }, - }; + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#album'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + compute: ({ + '#album': album, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (album + ? continuation.raise({to: album}) + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: album}))), + }, + }, + ]); }, // Gets a single property from this track's album, providing it as the same -- cgit 1.3.0-6-gf8a5 From f874ea879e8b9555baaaa3a38ec6a00432721846 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 11:00:07 -0300 Subject: data, test: update & test Track.originalReleaseTrack --- src/data/things/track.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 22043688..9b1a8226 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -192,11 +192,10 @@ export class Track extends Thing { Thing.composite.exposeDependency('#hasUniqueCoverArt'), ]), - originalReleaseTrack: Thing.common.dynamicThingFromSingleReference( - 'originalReleaseTrackByRef', - 'trackData', - find.track - ), + originalReleaseTrack: Thing.composite.from(`Track.originalReleaseTrack`, [ + Track.composite.withOriginalRelease(), + Thing.composite.exposeDependency('#originalRelease'), + ]), otherReleases: Thing.composite.from(`Track.otherReleases`, [ -- cgit 1.3.0-6-gf8a5 From 59023bad2de5cd76edced5393cc38afc6b46fc1c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 11:01:25 -0300 Subject: data: fix mis-indented Thing.composite.from calls --- src/data/things/track.js | 52 +++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 9b1a8226..3a4e1585 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -155,11 +155,10 @@ export class Track extends Thing { commentatorArtists: Thing.common.commentatorArtists(), - album: - Thing.composite.from(`Track.album`, [ - Track.composite.withAlbum(), - Thing.composite.exposeDependency('#album'), - ]), + album: Thing.composite.from(`Track.album`, [ + Track.composite.withAlbum(), + Thing.composite.exposeDependency('#album'), + ]), // Note - this is an internal property used only to help identify a track. // It should not be assumed in general that the album and dataSourceAlbum match @@ -197,30 +196,29 @@ export class Track extends Thing { Thing.composite.exposeDependency('#originalRelease'), ]), - otherReleases: - Thing.composite.from(`Track.otherReleases`, [ - Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}), - Track.composite.withOriginalRelease({selfIfOriginal: true}), + otherReleases: Thing.composite.from(`Track.otherReleases`, [ + Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}), + Track.composite.withOriginalRelease({selfIfOriginal: true}), - { - flags: {expose: true}, - expose: { - dependencies: ['this', 'trackData', '#originalRelease'], - compute: ({ - this: thisTrack, - trackData, - '#originalRelease': originalRelease, - }) => - (originalRelease === thisTrack - ? [] - : [originalRelease]) - .concat(trackData.filter(track => - track !== originalRelease && - track !== thisTrack && - track.originalReleaseTrack === originalRelease)), - }, + { + flags: {expose: true}, + expose: { + dependencies: ['this', 'trackData', '#originalRelease'], + compute: ({ + this: thisTrack, + trackData, + '#originalRelease': originalRelease, + }) => + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), }, - ]), + }, + ]), artistContribs: Thing.composite.from(`Track.artistContribs`, [ Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), -- cgit 1.3.0-6-gf8a5 From 5e5c2d9e1ee9dbe1c715e4d53bcb244ffcf606b0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 11:05:58 -0300 Subject: data: misc. style consistency tweaks --- src/data/things/album.js | 15 +++------------ src/data/things/artist.js | 6 ++---- src/data/things/flash.js | 6 +----- src/data/things/track.js | 19 ++++--------------- src/data/things/wiki-info.js | 6 +----- 5 files changed, 11 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index c012c243..06982903 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -128,6 +128,9 @@ export class Album extends Thing { commentatorArtists: Thing.common.commentatorArtists(), + groups: Thing.common.dynamicThingsFromReferenceList('groupsByRef', 'groupData', find.group), + artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag), + hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'), hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'), hasBannerArt: Thing.common.contribsPresent('bannerArtistContribsByRef'), @@ -146,18 +149,6 @@ export class Album extends Thing { : [], }, }, - - groups: Thing.common.dynamicThingsFromReferenceList( - 'groupsByRef', - 'groupData', - find.group - ), - - artTags: Thing.common.dynamicThingsFromReferenceList( - 'artTagsByRef', - 'artTagData', - find.artTag - ), }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/things/artist.js b/src/data/things/artist.js index bde84cfa..b2383057 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -111,10 +111,8 @@ export class Artist extends Thing { }, }, - flashesAsContributor: Artist.filterByContrib( - 'flashData', - 'contributorContribs' - ), + flashesAsContributor: + Artist.filterByContrib('flashData', 'contributorContribs'), }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 445fd07c..3f870c51 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -141,10 +141,6 @@ export class FlashAct extends Thing { // Expose only - flashes: Thing.common.dynamicThingsFromReferenceList( - 'flashesByRef', - 'flashData', - find.flash - ), + flashes: Thing.common.dynamicThingsFromReferenceList('flashesByRef', 'flashData', find.flash), }) } diff --git a/src/data/things/track.js b/src/data/things/track.js index 3a4e1585..74713a00 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -167,11 +167,7 @@ export class Track extends Thing { // not generally relevant information). It's also not guaranteed that // dataSourceAlbum is available (depending on the Track creator to optionally // provide dataSourceAlbumByRef). - dataSourceAlbum: Thing.common.dynamicThingFromSingleReference( - 'dataSourceAlbumByRef', - 'albumData', - find.album - ), + dataSourceAlbum: Thing.common.dynamicThingFromSingleReference('dataSourceAlbumByRef', 'albumData', find.album), date: Thing.composite.from(`Track.date`, [ Thing.composite.exposeDependencyOrContinue('dateFirstReleased'), @@ -303,6 +299,8 @@ export class Track extends Thing { Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), ]), + artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag), + // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't // generally relevant from the perspective of the tracks being referenced. @@ -342,16 +340,7 @@ export class Track extends Thing { }, }, - featuredInFlashes: Thing.common.reverseReferenceList( - 'flashData', - 'featuredTracks' - ), - - artTags: Thing.common.dynamicThingsFromReferenceList( - 'artTagsByRef', - 'artTagData', - find.artTag - ), + featuredInFlashes: Thing.common.reverseReferenceList('flashData', 'featuredTracks'), }); static composite = { diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index e906cab1..e8279987 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -59,10 +59,6 @@ export class WikiInfo extends Thing { // Expose only - divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList( - 'divideTrackListsByGroupsByRef', - 'groupData', - find.group - ), + divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList('divideTrackListsByGroupsByRef', 'groupData', find.group), }); } -- cgit 1.3.0-6-gf8a5 From 001bcb69db4f4050fca222568ae2895f58a2f0df Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 15:51:01 -0300 Subject: data: simplify Thing.composite.from (needs docs update) --- src/data/things/thing.js | 402 +++++++++++++++++++++++++++-------------------- 1 file changed, 230 insertions(+), 172 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index cd62288e..782946ce 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -743,7 +743,7 @@ export default class Thing extends CacheableObject { } const base = composition.at(-1); - const steps = composition.slice(0, -1); + const steps = composition.slice(); const aggregate = openAggregate({ message: @@ -751,78 +751,118 @@ export default class Thing extends CacheableObject { (annotation ? ` (${annotation})` : ''), }); - if (base.flags.compose && base.flags.compute) { - push(new TypeError(`Base which composes can't also update yet`)); - } + const baseExposes = + (base.flags + ? base.flags.expose + : true); - const exposeSteps = []; - const exposeDependencies = new Set(); + const baseUpdates = + (base.flags + ? base.flags.update + : false); - if (base.expose?.dependencies) { - for (const dependency of base.expose.dependencies) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } - } + const baseComposes = + (base.flags + ? base.flags.compose + : true); - if (base.expose?.mapDependencies) { - for (const dependency of Object.values(base.expose.mapDependencies)) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + if (!baseExposes) { + aggregate.push(new TypeError(`All steps, including base, must expose`)); } + const exposeDependencies = new Set(); + + let anyStepsCompute = false; + let anyStepsTransform = false; + for (let i = 0; i < steps.length; i++) { const step = steps[i]; + const isBase = i === steps.length - 1; const message = - (step.annotation - ? `Errors in step #${i + 1} (${step.annotation})` - : `Errors in step #${i + 1}`); + `Errors in step #${i + 1}` + + (isBase ? ` (base)` : ``) + + (step.annotation ? ` (${step.annotation})` : ``); aggregate.nest({message}, ({push}) => { - if (!step.flags.compose) { - push(new TypeError(`Steps (all but bottom item) must be {compose: true}`)); - } + if (step.flags) { + let flagsErrored = false; - if (step.flags.update) { - push(new Error(`Steps which update aren't supported yet`)); - } - - if (step.flags.expose) expose: { - if (!step.expose.transform && !step.expose.compute) { - push(new TypeError(`Steps which expose must provide at least one of transform or compute`)); - break expose; + if (!step.flags.compose && !isBase) { + push(new TypeError(`All steps but base must compose`)); + flagsErrored = true; } - if ( - step.expose.transform && - !step.expose.compute && - !base.flags.update && - !base.flags.compose - ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - break expose; + if (!step.flags.expose) { + push(new TypeError(`All steps must expose`)); + flagsErrored = true; } - if (step.expose.dependencies) { - for (const dependency of step.expose.dependencies) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + if (flagsErrored) { + return; } + } - if (step.expose.mapDependencies) { - for (const dependency of Object.values(step.expose.mapDependencies)) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + const expose = + (step.flags + ? step.expose + : step); + + const stepComputes = !!expose.compute; + const stepTransforms = !!expose.transform; + + if (!stepComputes && !stepTransforms) { + push(new TypeError(`Steps must provide compute or transform (or both)`)); + return; + } + + if ( + stepTransforms && !stepComputes && + !baseUpdates && !baseComposes + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + return; + } + + if (stepComputes) { + anyStepsCompute = true; + } + + if (stepTransforms) { + anyStepsTransform = true; + } + + // Unmapped dependencies are exposed on the final composition only if + // they're "public", i.e. pointing to update values of other properties + // on the CacheableObject. + for (const dependency of expose.dependencies ?? []) { + if (typeof dependency === 'string' && dependency.startsWith('#')) { + continue; } - exposeSteps.push(step); + exposeDependencies.add(dependency); + } + + // Mapped dependencies are always exposed on the final composition. + // These are explicitly for reading values which are named outside of + // the current compositional step. + for (const dependency of Object.values(expose.mapDependencies ?? {})) { + exposeDependencies.add(dependency); } }); } + if (!baseComposes) { + if (baseUpdates) { + if (!anyStepsTransform) { + push(new TypeError(`Expected at least one step to transform`)); + } + } else { + if (!anyStepsCompute) { + push(new TypeError(`Expected at least one step to compute`)); + } + } + } + aggregate.close(); const constructedDescriptor = {}; @@ -832,64 +872,68 @@ export default class Thing extends CacheableObject { } constructedDescriptor.flags = { - update: !!base.flags.update, - expose: !!base.flags.expose, - compose: !!base.flags.compose, + update: baseUpdates, + expose: baseExposes, + compose: baseComposes, }; - if (base.flags.update) { + if (baseUpdates) { constructedDescriptor.update = base.update; } - if (base.flags.expose) { + if (baseExposes) { const expose = constructedDescriptor.expose = {}; expose.dependencies = Array.from(exposeDependencies); const continuationSymbol = Symbol('continuation symbol'); const noTransformSymbol = Symbol('no-transform symbol'); - function _filterDependencies(dependencies, step) { + function _filterDependencies(availableDependencies, { + dependencies, + mapDependencies, + options, + }) { const filteredDependencies = - (step.expose.dependencies - ? filterProperties(dependencies, step.expose.dependencies) + (dependencies + ? filterProperties(availableDependencies, dependencies) : {}); - if (step.expose.mapDependencies) { - for (const [to, from] of Object.entries(step.expose.mapDependencies)) { - filteredDependencies[to] = dependencies[from] ?? null; + if (mapDependencies) { + for (const [to, from] of Object.entries(mapDependencies)) { + filteredDependencies[to] = availableDependencies[from] ?? null; } } - if (step.expose.options) { - filteredDependencies['#options'] = step.expose.options; + if (options) { + filteredDependencies['#options'] = options; } return filteredDependencies; } - function _assignDependencies(continuationAssignment, step) { - if (!step.expose.mapContinuation) { + function _assignDependencies(continuationAssignment, {mapContinuation}) { + if (!mapContinuation) { return continuationAssignment; } const assignDependencies = {}; - for (const [from, to] of Object.entries(step.expose.mapContinuation)) { + for (const [from, to] of Object.entries(mapContinuation)) { assignDependencies[to] = continuationAssignment[from] ?? null; } return assignDependencies; } - function _prepareContinuation(transform) { + function _prepareContinuation(callingTransformForThisStep) { const continuationStorage = { returnedWith: null, - providedDependencies: null, - providedValue: null, + providedDependencies: undefined, + providedValue: undefined, }; const continuation = - (transform + (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { continuationStorage.returnedWith = 'continuation'; continuationStorage.providedDependencies = providedDependencies; @@ -908,150 +952,166 @@ export default class Thing extends CacheableObject { return continuationSymbol; }; - if (base.flags.compose) { - continuation.raise = - (transform + if (baseComposes) { + const makeRaiseLike = returnWith => + (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'raise'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; continuationStorage.providedValue = providedValue; return continuationSymbol; } : (providedDependencies = null) => { - continuationStorage.returnedWith = 'raise'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; return continuationSymbol; }); + + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); } return {continuation, continuationStorage}; } - function _computeOrTransform(value, initialDependencies, continuationIfApplicable) { - const dependencies = {...initialDependencies}; + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; - let valueSoFar = value; // Set only for {update: true} compositions - let exportDependencies = null; // Set only for {compose: true} compositions + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); - debug(() => color.bright(`begin composition`)); + const availableDependencies = {...initialDependencies}; - stepLoop: for (let i = 0; i < exposeSteps.length; i++) { - const step = exposeSteps[i]; - debug(() => [`step #${i+1}:`, step]); + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } - const transform = - valueSoFar !== noTransformSymbol && - step.expose.transform; + stepLoop: for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; - const filteredDependencies = _filterDependencies(dependencies, step); - const {continuation, continuationStorage} = _prepareContinuation(transform); + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); + + const expose = + (step.flags + ? step.expose + : step); + + const callingTransformForThisStep = + expectingTransform && expose.transform; + + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); debug(() => [ - `step #${i+1} - ${transform ? 'transform' : 'compute'}`, + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies]); const result = - (transform + (callingTransformForThisStep ? step.expose.transform(valueSoFar, filteredDependencies, continuation) : step.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { - if (base.flags.compose) { - throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - debug(() => [`step #${i+1} - early-exit (inferred) ->`, result]); - debug(() => color.bright(`end composition`)); + debug(() => color.bright(`end composition - exit (inferred)`)); + return result; } - switch (continuationStorage.returnedWith) { - case 'exit': - debug(() => [`step #${i+1} - result: early-exit (explicit) ->`, continuationStorage.providedValue]); - debug(() => color.bright(`end composition`)); - return continuationStorage.providedValue; - - case 'raise': - debug(() => `step #${i+1} - result: raise`); - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step) ?? {}; - if (transform) valueSoFar = continuationStorage.providedValue; - break stepLoop; + const {returnedWith} = continuationStorage; - case 'continuation': - if (transform) { - valueSoFar = continuationStorage.providedValue; - } + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; - if (continuationStorage.providedDependencies) { - const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - Object.assign(dependencies, assignDependencies); - debug(() => `step #${i+1} - result: continuation`); - debug(() => [`assign dependencies:`, assignDependencies]); - } else { - debug(() => `step #${i+1} - result: continuation (no provided dependencies)`); - } + debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); + debug(() => color.bright(`end composition - exit (explicit)`)); - break; + if (baseComposes) { + return continuationIfApplicable.exit(providedValue); + } else { + return providedValue; + } } - } - if (exportDependencies) { - debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition`)); - return continuationIfApplicable(exportDependencies); - } - - debug(() => `completed all steps, reached base`); + const {providedValue, providedDependencies} = continuationStorage; - const filteredDependencies = _filterDependencies(dependencies, base); + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); - const transform = - valueSoFar !== noTransformSymbol && - base.expose.transform; + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); - debug(() => [ - `base - ${transform ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + const continuationArgs = []; + if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); + if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(transform); - - const result = - (transform - ? base.expose.transform(valueSoFar, filteredDependencies, continuation) - : base.expose.compute(filteredDependencies, continuation)); + debug(() => { + const base = `step #${i+1} - result: ` + returnedWith; + const parts = []; - if (result !== continuationSymbol) { - throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); - } + if (callingTransformForThisStep) { + if (continuingWithValue === undefined) { + parts.push(`(no value)`); + } else { + parts.push(`value:`, providedValue); + } + } - switch (continuationStorage.returnedWith) { - case 'continuation': - throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } - case 'exit': - debug(() => `base - result: early-exit (explicit)`); - debug(() => [`early-exit:`, continuationStorage.providedValue]); - debug(() => color.bright(`end composition`)); - return continuationStorage.providedValue; + if (empty(parts)) { + return base; + } else { + return [base + ' ->', ...parts]; + } + }); + switch (returnedWith) { case 'raise': - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); - debug(() => `base - result: raise`); - debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition`)); - return continuationIfApplicable(exportDependencies); - } - } else { - const result = - (transform - ? base.expose.transform(valueSoFar, filteredDependencies) - : base.expose.compute(filteredDependencies)); + debug(() => + (isBase + ? color.bright(`end composition - raise (base: explicit)`) + : color.bright(`end composition - raise`))); + return continuationIfApplicable(...continuationArgs); - debug(() => [`base - non-compose (final) result:`, result]); - debug(() => color.bright(`end composition`)); + case 'raiseAbove': + debug(() => color.bright(`end composition - raiseAbove`)); + return continuationIfApplicable.raise(...continuationArgs); - return result; + case 'continuation': + if (isBase) { + debug(() => color.bright(`end composition - raise (inferred)`)); + return continuationIfApplicable(...continuationArgs); + } else { + Object.assign(availableDependencies, continuingWithDependencies); + break; + } + } } } @@ -1063,12 +1123,10 @@ export default class Thing extends CacheableObject { (initialDependencies, continuationIfApplicable) => _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - if (base.flags.compose) { - if (exposeSteps.some(step => step.expose.transform)) { - expose.transform = transformFn; - } - expose.compute = computeFn; - } else if (base.flags.update) { + if (baseComposes) { + if (anyStepsTransform) expose.transform = transformFn; + if (anyStepsCompute) expose.compute = computeFn; + } else if (baseUpdates) { expose.transform = transformFn; } else { expose.compute = computeFn; @@ -1229,7 +1287,7 @@ export default class Thing extends CacheableObject { switch (mode) { case 'null': return value !== null; case 'empty': return !empty(value); - case 'falsy': return !empty(value) && !!value; + case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); default: return false; } }; -- cgit 1.3.0-6-gf8a5 From c0bbd7e8fa6c76df4fa492e3a9d3b5e9ef42ec5c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 15:53:02 -0300 Subject: data: misc. utility additions * add earlyExitWithoutUpdateValue * add raiseWithoutDependency * add raiseWithoutUpdateValue * add earlyExitIfAvailabilityCheckFailed (internal) * refactor earlyExitWithoutDependency The "raise" utilities make use of the new `raiseAbove` continuation feature. --- src/data/things/thing.js | 100 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 782946ce..501286d7 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1384,6 +1384,36 @@ export default class Thing extends CacheableObject { ]); }, + // Early exits if an availability check fails. + // This is for internal use only - use `earlyExitWithoutDependency` or + // `earlyExitWIthoutUpdateValue` instead. + earlyExitIfAvailabilityCheckFailed({ + availability = '#availability', + value = null, + }) { + return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + options: {value}, + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), + }, + }, + ]); + }, + // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! earlyExitWithoutDependency(dependency, { @@ -1391,10 +1421,32 @@ export default class Thing extends CacheableObject { value = null, } = {}) { return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), + Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), + ]); + }, + + // Early exits if this property's update value isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + earlyExitWithoutUpdateValue({ + mode = 'null', + value = null, + } = {}) { + return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), + ]); + }, + + // Raises if a dependency isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + raiseWithoutDependency(dependency, { + mode = 'null', + map = {}, + raise = {}, + } = {}) { + return Thing.composite.from(`Thing.composite.raiseWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { flags: {expose: true, compose: true}, @@ -1410,11 +1462,43 @@ export default class Thing extends CacheableObject { { flags: {expose: true, compose: true}, expose: { - dependencies: ['#availability'], - options: {value}, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), + }, + }, + ]); + }, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), + // Raises if this property's update value isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + raiseWithoutUpdateValue({ + mode = 'null', + map = {}, + raise = {}, + } = {}) { + return Thing.composite.from(`Thing.composite.raiseWithoutUpdateValue`, [ + Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, }, ]); -- cgit 1.3.0-6-gf8a5 From 918fb043a640cf937de604fc74cb95566fa66459 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 15:56:34 -0300 Subject: data: refactor Thing.composite.withResolvedReference --- src/data/things/thing.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 501286d7..389b3845 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1542,30 +1542,8 @@ export default class Thing extends CacheableObject { earlyExitIfNotFound = false, }) { return Thing.composite.from(`Thing.composite.withResolvedReference`, [ - { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {ref}, - mapContinuation: {to}, - - compute: ({ref}, continuation) => - (ref - ? continuation() - : continuation.raise({to: null})), - }, - }, - - { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {data}, - - compute: ({data}, continuation) => - (data === null - ? continuation.exit(null) - : continuation()), - }, - }, + Thing.composite.raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + Thing.composite.earlyExitWithoutDependency(data), { flags: {expose: true, compose: true}, -- cgit 1.3.0-6-gf8a5 From 5a63b96cfd3d26e4b74ff4c6dfc793aef057f81b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 15:57:15 -0300 Subject: data: update Thing.common.dynamicThingsFromReferenceList Only the internal implementation. This should really be updated to take key/value-style parameters, and probably be renamed, but this helps to confirm a swathe of expected behavior continues to work with an existing `common` utility reimplemented compositionally. --- src/data/things/thing.js | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 389b3845..751e168f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -192,26 +192,29 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - dynamicThingsFromReferenceList: ( - referenceListProperty, - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, + dynamicThingsFromReferenceList( + refs, + data, + findFunction + ) { + return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ + Thing.composite.earlyExitWithoutDependency(refs, {value: []}), + Thing.composite.earlyExitWithoutDependency(data, {value: []}), - expose: { - dependencies: [referenceListProperty, thingDataProperty], - compute: ({ - [referenceListProperty]: refs, - [thingDataProperty]: thingData, - }) => - refs && thingData - ? refs - .map((ref) => findFn(ref, thingData, {mode: 'quiet'})) - .filter(Boolean) - : [], - }, - }), + { + flags: {expose: true}, + expose: { + mapDependencies: {refs, data}, + options: {findFunction}, + + compute: ({refs, data, '#options': {findFunction}}) => + refs + .map(ref => findFunction(ref, data, {mode: 'quiet'})) + .filter(Boolean), + }, + }, + ]); + }, // Corresponding function for a single reference. dynamicThingFromSingleReference: ( -- cgit 1.3.0-6-gf8a5 From f3162203ef1f758d500e065804f9dbe478d0481d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 16:05:53 -0300 Subject: data: Thing.composite.from: fix missed step.expose assumptions --- src/data/things/thing.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 751e168f..d4d7c850 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -993,7 +993,7 @@ export default class Thing extends CacheableObject { debug(() => color.bright(`begin composition - not transforming`)); } - stepLoop: for (let i = 0; i < steps.length; i++) { + for (let i = 0; i < steps.length; i++) { const step = steps[i]; const isBase = i === steps.length - 1; @@ -1021,8 +1021,8 @@ export default class Thing extends CacheableObject { const result = (callingTransformForThisStep - ? step.expose.transform(valueSoFar, filteredDependencies, continuation) - : step.expose.compute(filteredDependencies, continuation)); + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); -- cgit 1.3.0-6-gf8a5 From 56a8e47f0e5ad276baef9d27c16960e3ea2c583b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 16:06:58 -0300 Subject: data: remove lots of boilerplate {expose: true, compose: true} --- src/data/things/thing.js | 133 +++++++------------ src/data/things/track.js | 336 ++++++++++++++++++++--------------------------- 2 files changed, 195 insertions(+), 274 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d4d7c850..15ec62c3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1333,23 +1333,17 @@ export default class Thing extends CacheableObject { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), }, { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, + mapDependencies: {dependency}, + compute: ({dependency}, continuation) => + continuation.exit(dependency), }, ]); }, @@ -1367,22 +1361,16 @@ export default class Thing extends CacheableObject { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), }, { - flags: {expose: true, compose: true}, - expose: { - transform: (value, {}, continuation) => - continuation.exit(value), - }, + transform: (value, {}, continuation) => + continuation.exit(value), }, ]); }, @@ -1396,23 +1384,17 @@ export default class Thing extends CacheableObject { }) { return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, + options: {value}, + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), }, ]); }, @@ -1452,24 +1434,18 @@ export default class Thing extends CacheableObject { Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, ]); }, @@ -1485,24 +1461,18 @@ export default class Thing extends CacheableObject { Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, ]); }, @@ -1549,21 +1519,18 @@ export default class Thing extends CacheableObject { Thing.composite.earlyExitWithoutDependency(data), { - flags: {expose: true, compose: true}, - expose: { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {match: to}, + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) { - return continuation.exit(null); - } + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } - return continuation.raise({match}); - }, + return continuation.raise({match}); }, }, ]); diff --git a/src/data/things/track.js b/src/data/things/track.js index 74713a00..ad001445 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -49,18 +49,15 @@ export class Track extends Thing { Track.composite.withContainingTrackSection({earlyExitIfNotFound: false}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#trackSection'], - compute: ({'#trackSection': trackSection}, continuation) => - // Album.trackSections guarantees the track section will have a - // color property (inheriting from the album's own color), but only - // if it's actually present! Color will be inherited directly from - // album otherwise. - (trackSection - ? trackSection.color - : continuation()), - }, + dependencies: ['#trackSection'], + compute: ({'#trackSection': trackSection}, continuation) => + // Album.trackSections guarantees the track section will have a + // color property (inheriting from the album's own color), but only + // if it's actually present! Color will be inherited directly from + // album otherwise. + (trackSection + ? trackSection.color + : continuation()), }, Track.composite.withAlbumProperty('color'), @@ -225,14 +222,11 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {contribsFromTrack: '#artistContribs'}, - compute: ({contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : contribsFromTrack), - }, + mapDependencies: {contribsFromTrack: '#artistContribs'}, + compute: ({contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : contribsFromTrack), }, Track.composite.withAlbumProperty('artistContribs'), @@ -259,14 +253,11 @@ export class Track extends Thing { // of the track. coverArtistContribs: Thing.composite.from(`Track.coverArtistContribs`, [ { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['disableUniqueCoverArt'], - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? null - : continuation()), - }, + dependencies: ['disableUniqueCoverArt'], + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? null + : continuation()), }, Thing.composite.withResolvedContribs({ @@ -275,14 +266,11 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {contribsFromTrack: '#coverArtistContribs'}, - compute: ({contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : contribsFromTrack), - }, + mapDependencies: {contribsFromTrack: '#coverArtistContribs'}, + compute: ({contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : contribsFromTrack), }, Track.composite.withAlbumProperty('trackCoverArtistContribs'), @@ -357,21 +345,16 @@ export class Track extends Thing { Track.composite.withOriginalRelease(), { - flags: {expose: true, compose: true}, - - expose: { - dependencies: ['#originalRelease'], + dependencies: ['#originalRelease'], + compute({'#originalRelease': originalRelease}, continuation) { + if (!originalRelease) return continuation.raise(); - compute({'#originalRelease': originalRelease}, continuation) { - if (!originalRelease) return continuation.raise(); + const value = originalRelease[originalProperty]; + if (allowOverride && value === null) return continuation.raise(); - const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation.raise(); - - return continuation.exit(value); - }, + return continuation.exit(value); }, - } + }, ]); }, @@ -387,51 +370,43 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#albumDataAvailability'], - options: {earlyExitIfNotFound}, - mapContinuation: {to}, - compute: ({ - '#albumDataAvailability': albumDataAvailability, - '#options': {earlyExitIfNotFound}, - }, continuation) => - (albumDataAvailability - ? continuation() - : (earlyExitIfNotFound - ? continuation.exit(null) - : continuation.raise({to: null}))), - }, + dependencies: ['#albumDataAvailability'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + + compute: ({ + '#albumDataAvailability': albumDataAvailability, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (albumDataAvailability + ? continuation() + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: null}))), }, { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['this', 'albumData'], - compute: ({this: track, albumData}, continuation) => - continuation({ - '#album': - albumData.find(album => album.tracks.includes(track)), - }), - }, + dependencies: ['this', 'albumData'], + compute: ({this: track, albumData}, continuation) => + continuation({ + '#album': + albumData.find(album => album.tracks.includes(track)), + }), }, { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#album'], - options: {earlyExitIfNotFound}, - mapContinuation: {to}, - compute: ({ - '#album': album, - '#options': {earlyExitIfNotFound}, - }, continuation) => - (album - ? continuation.raise({to: album}) - : (earlyExitIfNotFound - ? continuation.exit(null) - : continuation.raise({to: album}))), - }, + dependencies: ['#album'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + compute: ({ + '#album': album, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (album + ? continuation.raise({to: album}) + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: album}))), }, ]); }, @@ -448,20 +423,17 @@ export class Track extends Thing { Track.composite.withAlbum({earlyExitIfNotFound}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#album'], - options: {property}, - mapContinuation: {to}, - - compute: ({ - '#album': album, - '#options': {property}, - }, continuation) => - (album - ? continuation.raise({to: album[property]}) - : continuation.raise({to: null})), - }, + dependencies: ['#album'], + options: {property}, + mapContinuation: {to}, + + compute: ({ + '#album': album, + '#options': {property}, + }, continuation) => + (album + ? continuation.raise({to: album[property]}) + : continuation.raise({to: null})), }, ]); }, @@ -479,29 +451,26 @@ export class Track extends Thing { Track.composite.withAlbum({earlyExitIfNotFound}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#album'], - options: {properties, prefix}, - - compute({ - '#album': album, - '#options': {properties, prefix}, - }, continuation) { - const raise = {}; - - if (album) { - for (const property of properties) { - raise[prefix + '.' + property] = album[property]; - } - } else { - for (const property of properties) { - raise[prefix + '.' + property] = null; - } + dependencies: ['#album'], + options: {properties, prefix}, + + compute({ + '#album': album, + '#options': {properties, prefix}, + }, continuation) { + const raise = {}; + + if (album) { + for (const property of properties) { + raise[prefix + '.' + property] = album[property]; } + } else { + for (const property of properties) { + raise[prefix + '.' + property] = null; + } + } - return continuation.raise(raise); - }, + return continuation.raise(raise); }, }, ]); @@ -519,30 +488,27 @@ export class Track extends Thing { Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['this', '#album.trackSections'], - mapContinuation: {to}, - - compute({ - this: track, - '#album.trackSections': trackSections, - }, continuation) { - if (!trackSections) { - return continuation.raise({to: null}); - } - - const trackSection = - trackSections.find(({tracks}) => tracks.includes(track)); - - if (trackSection) { - return continuation.raise({to: trackSection}); - } else if (earlyExitIfNotFound) { - return continuation.exit(null); - } else { - return continuation.raise({to: null}); - } - }, + dependencies: ['this', '#album.trackSections'], + mapContinuation: {to}, + + compute({ + this: track, + '#album.trackSections': trackSections, + }, continuation) { + if (!trackSections) { + return continuation.raise({to: null}); + } + + const trackSection = + trackSections.find(({tracks}) => tracks.includes(track)); + + if (trackSection) { + return continuation.raise({to: trackSection}); + } else if (earlyExitIfNotFound) { + return continuation.exit(null); + } else { + return continuation.raise({to: null}); + } }, }, ]); @@ -568,24 +534,21 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['this', '#originalRelease'], - options: {selfIfOriginal}, - mapContinuation: {to}, - compute: ({ - this: track, - '#originalRelease': originalRelease, - '#options': {selfIfOriginal}, - }, continuation) => - continuation.raise({ - to: - (originalRelease ?? - (selfIfOriginal - ? track - : null)), - }), - }, + dependencies: ['this', '#originalRelease'], + options: {selfIfOriginal}, + mapContinuation: {to}, + compute: ({ + this: track, + '#originalRelease': originalRelease, + '#options': {selfIfOriginal}, + }, continuation) => + continuation.raise({ + to: + (originalRelease ?? + (selfIfOriginal + ? track + : null)), + }), }, ]); }, @@ -597,15 +560,12 @@ export class Track extends Thing { } = {}) { return Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['disableUniqueCoverArt'], - mapContinuation: {to}, - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? continuation.raise({to: false}) - : continuation()), - }, + dependencies: ['disableUniqueCoverArt'], + mapContinuation: {to}, + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? continuation.raise({to: false}) + : continuation()), }, Thing.composite.withResolvedContribs({ @@ -614,29 +574,23 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#coverArtistContribs'], - mapContinuation: {to}, - compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raise({to: true})), - }, + dependencies: ['#coverArtistContribs'], + mapContinuation: {to}, + compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : continuation.raise({to: true})), }, Track.composite.withAlbumProperty('trackCoverArtistContribs'), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#album.trackCoverArtistContribs'], - mapContinuation: {to}, - compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => - (empty(contribsFromAlbum) - ? continuation.raise({to: false}) - : continuation.raise({to: true})), - }, + dependencies: ['#album.trackCoverArtistContribs'], + mapContinuation: {to}, + compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => + (empty(contribsFromAlbum) + ? continuation.raise({to: false}) + : continuation.raise({to: true})), }, ]); }, -- cgit 1.3.0-6-gf8a5 From f0a94d03d01220ff44c9c7cf610373781dd4c09d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 19:13:32 -0300 Subject: data: refactor Track.coverArtDate --- src/data/things/track.js | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index ad001445..ad90dd2c 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -98,36 +98,15 @@ export class Track extends Thing { // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. coverArtDate: Thing.composite.from(`Track.coverArtDate`, [ - Track.composite.withAlbumProperties({ - properties: [ - 'trackArtDate', - 'trackCoverArtistContribsByRef', - ], - }), + Track.composite.withHasUniqueCoverArt(), + Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), - { - flags: {update: true, expose: true}, + Thing.composite.exposeUpdateValueOrContinue(), + + Track.composite.withAlbumProperty('trackArtDate'), + Thing.composite.exposeDependency('#album.trackArtDate', { update: {validate: isDate}, - expose: { - dependencies: [ - 'coverArtistContribsByRef', - 'disableUniqueCoverArt', - '#album.trackArtDate', - '#album.trackCoverArtistContribsByRef', - ], - - transform(coverArtDate, { - coverArtistContribsByRef, - disableUniqueCoverArt, - '#album.trackArtDate': trackArtDate, - '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, - }) { - if (disableUniqueCoverArt) return null; - if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; - return coverArtDate ?? trackArtDate; - }, - }, - } + }), ]), originalReleaseTrackByRef: Thing.common.singleReference(Track), -- cgit 1.3.0-6-gf8a5 From 6f54b1211b5b07fe747ce4ebafdf917ce7851324 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 19:22:54 -0300 Subject: test: Track.coverArtFileExtension (unit) --- src/data/things/track.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index ad90dd2c..0b34de20 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -90,7 +90,9 @@ export class Track extends Thing { Thing.composite.exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), // Fallback to 'jpg'. - Thing.composite.exposeConstant('jpg'), + Thing.composite.exposeConstant('jpg', { + update: {validate: isFileExtension}, + }), ]), // Date of cover art release. Like coverArtFileExtension, this represents -- cgit 1.3.0-6-gf8a5 From 6325a70991396412eb8e93cee5f17bdb2859ae9d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 19:52:42 -0300 Subject: data, test: update & test misc. Track reverse reference lists * update & test Track.referencedByTracks * update & test Track.sampledByTracks * update & test Track.featuredInFlashes * update Thing.common.reverseReferenceList * add Thing.composite.withReverseReferenceList * add Track.composite.trackReverseReferenceList --- src/data/things/thing.js | 43 +++++++++++++++++++++++++++++--------- src/data/things/track.js | 54 ++++++++++++++++++++++-------------------------- 2 files changed, 58 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 15ec62c3..1c99a323 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -330,16 +330,15 @@ export default class Thing extends CacheableObject { // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. - reverseReferenceList: (thingDataProperty, referencerRefListProperty) => ({ - flags: {expose: true}, - - expose: { - dependencies: ['this', thingDataProperty], - - compute: ({this: thing, [thingDataProperty]: thingData}) => - thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [], - }, - }), + reverseReferenceList({ + data, + refList, + }) { + return Thing.composite.from(`Thing.common.reverseReferenceList`, [ + Thing.composite.withReverseReferenceList({data, refList}), + Thing.composite.exposeDependency('#reverseReferenceList'), + ]); + }, // Corresponding function for single references. Note that the return value // is still a list - this is for matching all the objects whose single @@ -1535,5 +1534,29 @@ export default class Thing extends CacheableObject { }, ]); }, + + // Check out the info on Thing.common.reverseReferenceList! + // This is its composable form. + withReverseReferenceList({ + data, + to = '#reverseReferenceList', + refList: refListProperty, + }) { + return Thing.composite.from(`Thing.common.reverseReferenceList`, [ + Thing.composite.earlyExitWithoutDependency(data, {value: []}), + + { + dependencies: ['this'], + mapDependencies: {data}, + mapContinuation: {to}, + options: {refListProperty}, + + compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => + continuation({ + to: data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ]); + }, }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 0b34de20..bc9affbe 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -278,38 +278,15 @@ export class Track extends Thing { // counting the number of times a track has been referenced, for use in // the "Tracks - by Times Referenced" listing page (or other data // processing). - referencedByTracks: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'trackData'], - - compute: ({this: track, trackData}) => - trackData - ? trackData - .filter((t) => !t.originalReleaseTrack) - .filter((t) => t.referencedTracks?.includes(track)) - : [], - }, - }, + referencedByTracks: Track.composite.trackReverseReferenceList('referencedTracks'), // For the same reasoning, exclude re-releases from sampled tracks too. - sampledByTracks: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'trackData'], - - compute: ({this: track, trackData}) => - trackData - ? trackData - .filter((t) => !t.originalReleaseTrack) - .filter((t) => t.sampledTracks?.includes(track)) - : [], - }, - }, + sampledByTracks: Track.composite.trackReverseReferenceList('sampledTracks'), - featuredInFlashes: Thing.common.reverseReferenceList('flashData', 'featuredTracks'), + featuredInFlashes: Thing.common.reverseReferenceList({ + data: 'flashData', + refList: 'featuredTracks', + }), }); static composite = { @@ -575,6 +552,25 @@ export class Track extends Thing { }, ]); }, + + trackReverseReferenceList(refListProperty) { + return Thing.composite.from(`Track.composite.trackReverseReferenceList`, [ + Thing.composite.withReverseReferenceList({ + data: 'trackData', + refList: refListProperty, + originalTracksOnly: true, + }), + + { + flags: {expose: true}, + expose: { + dependencies: ['#reverseReferenceList'], + compute: ({'#reverseReferenceList': reverseReferenceList}) => + reverseReferenceList.filter(track => !track.originalReleaseTrack), + }, + }, + ]); + }, }; [inspect.custom](depth) { -- cgit 1.3.0-6-gf8a5 From 8e783429194f58909f26c7b11d558d5b0a9b163f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 19:59:11 -0300 Subject: data: clean up bad mapDependencies usages --- src/data/things/track.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index bc9affbe..bf56a6dd 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -203,25 +203,15 @@ export class Track extends Thing { }), { - mapDependencies: {contribsFromTrack: '#artistContribs'}, - compute: ({contribsFromTrack}, continuation) => + dependencies: ['#artistContribs'], + compute: ({'#artistContribs': contribsFromTrack}, continuation) => (empty(contribsFromTrack) ? continuation() : contribsFromTrack), }, Track.composite.withAlbumProperty('artistContribs'), - - { - flags: {expose: true}, - expose: { - mapDependencies: {contribsFromAlbum: '#album.artistContribs'}, - compute: ({contribsFromAlbum}) => - (empty(contribsFromAlbum) - ? null - : contribsFromAlbum), - }, - }, + Thing.composite.exposeDependency('#album.artistContribs'), ]), contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ @@ -247,8 +237,8 @@ export class Track extends Thing { }), { - mapDependencies: {contribsFromTrack: '#coverArtistContribs'}, - compute: ({contribsFromTrack}, continuation) => + dependencies: ['#coverArtistContribs'], + compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => (empty(contribsFromTrack) ? continuation() : contribsFromTrack), -- cgit 1.3.0-6-gf8a5 From a3b80f08fc54cda6a6787bcd078059823026add6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 31 Aug 2023 20:19:22 -0300 Subject: data: update Thing.composition.from documentation --- src/data/things/thing.js | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1c99a323..19f5fb53 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -565,6 +565,14 @@ export default class Thing extends CacheableObject { // }, // ]); // + // One last note! A super common code pattern when creating more complex + // compositions is to have several steps which *only* expose and compose. + // As a syntax shortcut, you can skip the outer section. It's basically + // like writing out just the {expose: {...}} part. Remember that this + // indicates that the step you're defining is compositional, so you have + // to specify the flags manually for the base, even if this property isn't + // going to get an {update: true} flag. + // // == Cache-safe dependency names: == // // [Disclosure: The caching engine hasn't actually been implemented yet. @@ -705,7 +713,7 @@ export default class Thing extends CacheableObject { // expanded to the scope of the composition instead of following steps. // // For example, suppose your composition (which you expect to include in - // other compositions) brings about several internal, hash-prefixed + // other compositions) brings about several private, hash-prefixed // dependencies to contribute to its own results. Those dependencies won't // end up "bleeding" into the dependency list of whichever composition is // nesting this one - they will totally disappear once all the steps in @@ -718,6 +726,40 @@ export default class Thing extends CacheableObject { // a hash just like the exports from any other compositional step; they're // still dynamically provided dependencies!) // + // Another way to "export" dependencies is by using calling *any* step's + // `continuation.raise()` function. This is sort of like early exiting, + // but instead of quitting out the whole entire property, it will just + // break out of the current, nested composition's list of steps, acting + // as though the composition had finished naturally. The dependencies + // passed to `raise` will be the ones which get exported. + // + // Since `raise` is another way to export dependencies, if you're using + // dynamic export names, you should specify `mapContinuation` on the step + // calling `continuation.raise` as well. + // + // An important note on `mapDependencies` here: A nested composition gets + // free access to all the ordinary properties defined on the thing it's + // working on, but if you want it to depend on *private* dependencies - + // ones prefixed with '#' - which were provided by some other compositional + // step preceding wherever this one gets nested, then you *have* to use + // `mapDependencies` to gain access. Check out the section on "cache-safe + // dependency names" for information on this syntax! + // + // Also - on rare occasion - you might want to make a reusable composition + // that itself causes the composition *it's* nested in to raise. If that's + // the case, give `composition.raiseAbove()` a go! This effectively means + // kicking out of *two* layers of nested composition - the one including + // the step with the `raiseAbove` call, and the composition which that one + // is nested within. You don't need to use `raiseAbove` if the reusable + // utility function just returns a single compositional step, but if you + // want to make use of other compositional steps, it gives you access to + // the same conditional-raise capabilities. + // + // Have some syntax sugar! Since nested compositions are defined by having + // the base be {compose: true}, the composition will infer as much if you + // don't specifying the base's flags at all. Simply use the same shorthand + // syntax as for other compositional steps, and it'll work out cleanly! + // from(firstArg, secondArg) { const debug = fn => { if (Thing.composite.from.debug === true) { -- cgit 1.3.0-6-gf8a5 From 9d8616ced8f505b499780e859d96f288d67f2154 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 12:13:25 -0300 Subject: data: remove unused Thing.common utilities dynamicInheritContribs is replaced by more specialized behavior on tracks (which are the only thing that inherit contribs this way), and reverseSingleReference, introduced with reverseReferenceList, was never used anywhere. --- src/data/things/thing.js | 68 ------------------------------------------------ 1 file changed, 68 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19f5fb53..ad27ca55 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -257,60 +257,6 @@ export default class Thing extends CacheableObject { }, }), - // Dynamically inherit a contribution list from some other object, if it - // hasn't been overridden on this object. This is handy for solo albums - // where all tracks have the same artist, for example. - dynamicInheritContribs: ( - // If this property is explicitly false, the contribution list returned - // will always be empty. - nullerProperty, - - // Property holding contributions on the current object. - contribsByRefProperty, - - // Property holding corresponding "default" contributions on the parent - // object, which will fallen back to if the object doesn't have its own - // contribs. - parentContribsByRefProperty, - - // Data array to search in and "find" function to locate parent object - // (which will be passed the child object and the wiki data array). - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, - expose: { - dependencies: [ - 'this', - contribsByRefProperty, - thingDataProperty, - nullerProperty, - 'artistData', - ].filter(Boolean), - - compute({ - this: thing, - [nullerProperty]: nuller, - [contribsByRefProperty]: contribsByRef, - [thingDataProperty]: thingData, - artistData, - }) { - if (!artistData) return []; - if (nuller === false) return []; - const refs = - contribsByRef ?? - findFn(thing, thingData, {mode: 'quiet'})?.[parentContribsByRefProperty]; - if (!refs) return []; - return refs - .map(({who: ref, what}) => ({ - who: find.artist(ref, artistData), - what, - })) - .filter(({who}) => who); - }, - }, - }), - // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. contribsPresent: (contribsByRefProperty) => ({ @@ -340,20 +286,6 @@ export default class Thing extends CacheableObject { ]); }, - // Corresponding function for single references. Note that the return value - // is still a list - this is for matching all the objects whose single - // reference (in the given property) matches this Thing. - reverseSingleReference: (thingDataProperty, referencerRefListProperty) => ({ - flags: {expose: true}, - - expose: { - dependencies: ['this', thingDataProperty], - - compute: ({this: thing, [thingDataProperty]: thingData}) => - thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [], - }, - }), - // General purpose wiki data constructor, for properties like artistData, // trackData, etc. wikiData: (thingClass) => ({ -- cgit 1.3.0-6-gf8a5 From 703f065560e71ec7f750ea8a9dfdff2c71e0fde8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 12:27:50 -0300 Subject: data: move Thing.composite definition into dedicated file --- src/data/things/composite.js | 1179 ++++++++++++++++++++++++++++++++++++++++++ src/data/things/thing.js | 1176 +---------------------------------------- 2 files changed, 1181 insertions(+), 1174 deletions(-) create mode 100644 src/data/things/composite.js (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js new file mode 100644 index 00000000..1be60cd1 --- /dev/null +++ b/src/data/things/composite.js @@ -0,0 +1,1179 @@ +// Composes multiple compositional "steps" and a "base" to form a property +// descriptor out of modular building blocks. This is an extension to the +// more general-purpose CacheableObject property descriptor syntax, and +// aims to make modular data processing - which lends to declarativity - +// much easier, without fundamentally altering much of the typical syntax +// or terminology, nor building on it to an excessive degree. +// +// Think of a composition as being a chain of steps which lead into a final +// base property, which is usually responsible for returning the value that +// will actually get exposed when the property being described is accessed. +// +// == The compositional base: == +// +// The final item in a compositional list is its base, and it identifies +// the essential qualities of the property descriptor. The compositional +// steps preceding it may exit early, in which case the expose function +// defined on the base won't be called; or they will provide dependencies +// that the base may use to compute the final value that gets exposed for +// this property. +// +// The base indicates the capabilities of the composition as a whole. +// It should be {expose: true}, since that's the only area that preceding +// compositional steps (currently) can actually influence. If it's also +// {update: true}, then the composition as a whole accepts an update value +// just like normal update-flag property descriptors - meaning it can be +// set with `thing.someProperty = value` and that value will be paseed +// into each (implementing) step's transform() function, as well as the +// base. Bases usually aren't {compose: true}, but can be - check out the +// section on "nesting compositions" for details about that. +// +// Every composition always has exactly one compositional base, and it's +// always the last item in the composition list. All items preceding it +// are compositional steps, described below. +// +// == Compositional steps: == +// +// Compositional steps are, in essence, typical property descriptors with +// the extra flag {compose: true}. They operate on existing dependencies, +// and are typically dynamically constructed by "utility" functions (but +// can also be manually declared within the step list of a composition). +// Compositional steps serve two purposes: +// +// 1. exit early, if some condition is matched, returning and exposing +// some value directly from that step instead of continuing further +// down the step list; +// +// 2. and/or provide new, dynamically created "private" dependencies which +// can be accessed by further steps down the list, or at the base at +// the bottom, modularly supplying information that will contribute to +// the final value exposed for this property. +// +// Usually it's just one of those two, but it's fine for a step to perform +// both jobs if the situation benefits. +// +// Compositional steps are the real "modular" or "compositional" part of +// this data processing style - they're designed to be combined together +// in dynamic, versatile ways, as each property demands it. You usually +// define a compositional step to be returned by some ordinary static +// property-descriptor-returning function (customarily namespaced under +// the relevant Thing class's static `composite` field) - that lets you +// reuse it in multiple compositions later on. +// +// Compositional steps are implemented with "continuation passing style", +// meaning the connection to the next link on the chain is passed right to +// each step's compute (or transform) function, and the implementation gets +// to decide whether to continue on that chain or exit early by returning +// some other value. +// +// Every step along the chain, apart from the base at the bottom, has to +// have the {compose: true} step. That means its compute() or transform() +// function will be passed an extra argument at the end, `continuation`. +// To provide new dependencies to items further down the chain, just pass +// them directly to this continuation() function, customarily with a hash +// ('#') prefixing each name - for example: +// +// compute({..some dependencies..}, continuation) { +// return continuation({ +// '#excitingProperty': (..a value made from dependencies..), +// }); +// } +// +// Performing an early exit is as simple as returning some other value, +// instead of the continuation. You may also use `continuation.exit(value)` +// to perform the exact same kind of early exit - it's just a different +// syntax that might fit in better in certain longer compositions. +// +// It may be fine to simply provide new dependencies under a hard-coded +// name, such as '#excitingProperty' above, but if you're writing a utility +// that dynamically returns the compositional step and you suspect you +// might want to use this step multiple times in a single composition, +// it's customary to accept a name for the result. +// +// Here's a detailed example showing off early exit, dynamically operating +// on a provided dependency name, and then providing a result in another +// also-provided dependency name: +// +// static Thing.composite.withResolvedContribs = ({ +// from: contribsByRefDependency, +// to: outputDependency, +// }) => ({ +// flags: {expose: true, compose: true}, +// expose: { +// dependencies: [contribsByRefDependency, 'artistData'], +// compute({ +// [contribsByRefDependency]: contribsByRef, +// artistData, +// }, continuation) { +// if (!artistData) return null; /* early exit! */ +// return continuation({ +// [outputDependency]: /* this is the important part */ +// (..resolve contributions one way or another..), +// }); +// }, +// }, +// }); +// +// And how you might work that into a composition: +// +// static Track[Thing.getPropertyDescriptors].coverArtists = +// Thing.composite.from([ +// Track.composite.doSomethingWhichMightEarlyExit(), +// Thing.composite.withResolvedContribs({ +// from: 'coverArtistContribsByRef', +// to: '#coverArtistContribs', +// }), +// +// { +// flags: {expose: true}, +// expose: { +// dependencies: ['#coverArtistContribs'], +// compute({'#coverArtistContribs': coverArtistContribs}) { +// return coverArtistContribs.map(({who}) => who); +// }, +// }, +// }, +// ]); +// +// One last note! A super common code pattern when creating more complex +// compositions is to have several steps which *only* expose and compose. +// As a syntax shortcut, you can skip the outer section. It's basically +// like writing out just the {expose: {...}} part. Remember that this +// indicates that the step you're defining is compositional, so you have +// to specify the flags manually for the base, even if this property isn't +// going to get an {update: true} flag. +// +// == Cache-safe dependency names: == +// +// [Disclosure: The caching engine hasn't actually been implemented yet. +// As such, this section is subject to change, and simply provides sound +// forward-facing advice and interfaces.] +// +// It's a good idea to write individual compositional steps in such a way +// that they're "cache-safe" - meaning the same input (dependency) values +// will always result in the same output (continuation or early exit). +// +// In order to facilitate this, compositional step descriptors may specify +// unique `mapDependencies`, `mapContinuation`, and `options` values. +// +// Consider the `withResolvedContribs` example adjusted to make use of +// two of these options below: +// +// static Thing.composite.withResolvedContribs = ({ +// from: contribsByRefDependency, +// to: outputDependency, +// }) => ({ +// flags: {expose: true, compose: true}, +// expose: { +// dependencies: ['artistData'], +// mapDependencies: {contribsByRef: contribsByRefDependency}, +// mapContinuation: {outputDependency}, +// compute({ +// contribsByRef, /* no longer in square brackets */ +// artistData, +// }, continuation) { +// if (!artistData) return null; +// return continuation({ +// outputDependency: /* no longer in square brackets */ +// (..resolve contributions one way or another..), +// }); +// }, +// }, +// }); +// +// With a little destructuring and restructuring JavaScript sugar, the +// above can be simplified some more: +// +// static Thing.composite.withResolvedContribs = ({from, to}) => ({ +// flags: {expose: true, compose: true}, +// expose: { +// dependencies: ['artistData'], +// mapDependencies: {from}, +// mapContinuation: {to}, +// compute({artistData, from: contribsByRef}, continuation) { +// if (!artistData) return null; +// return continuation({ +// to: (..resolve contributions one way or another..), +// }); +// }, +// }, +// }); +// +// These two properties let you separate the name-mapping behavior (for +// dependencies and the continuation) from the main body of the compute +// function. That means the compute function will *always* get inputs in +// the same form (dependencies 'artistData' and 'from' above), and will +// *always* provide its output in the same form (early return or 'to'). +// +// Thanks to that, this `compute` function is cache-safe! Its outputs can +// be cached corresponding to each set of mapped inputs. So it won't matter +// whether the `from` dependency is named `coverArtistContribsByRef` or +// `contributorContribsByRef` or something else - the compute function +// doesn't care, and only expects that value to be provided via its `from` +// argument. Likewise, it doesn't matter if the output should be sent to +// '#coverArtistContribs` or `#contributorContribs` or some other name; +// the mapping is handled automatically outside, and compute will always +// output its value to the continuation's `to`. +// +// Note that `mapDependencies` and `mapContinuation` should be objects of +// the same "shape" each run - that is, the values will change depending on +// outside context, but the keys are always the same. You shouldn't use +// `mapDependencies` to dynamically select more or fewer dependencies. +// If you need to dynamically select a range of dependencies, just specify +// them in the `dependencies` array like usual. The caching engine will +// understand that differently named `dependencies` indicate separate +// input-output caches should be used. +// +// The 'options' property makes it possible to specify external arguments +// that fundamentally change the behavior of the `compute` function, while +// still remaining cache-safe. It indicates that the caching engine should +// use a completely different input-to-output cache for each permutation +// of the 'options' values. This way, those functions are still cacheable +// at all; they'll just be cached separately for each set of option values. +// Values on the 'options' property will always be provided in compute's +// dependencies under '#options' (to avoid name conflicts with other +// dependencies). +// +// == To compute or to transform: == +// +// A compositional step can work directly on a property's stored update +// value, transforming it in place and either early exiting with it or +// passing it on (via continuation) to the next item(s) in the +// compositional step list. (If needed, these can provide dependencies +// the same way as compute functions too - just pass that object after +// the updated (or same) transform value in your call to continuation().) +// +// But in order to make them more versatile, compositional steps have an +// extra trick up their sleeve. If a compositional step implements compute +// and *not* transform, it can still be used in a composition targeting a +// property which updates! These retain their full dependency-providing and +// early exit functionality - they just won't be provided the update value. +// If a compute-implementing step returns its continuation, then whichever +// later step (or the base) next implements transform() will receive the +// update value that had so far been running - as well as any dependencies +// the compute() step returned, of course! +// +// Please note that a compositional step which transforms *should not* +// specify, in its flags, {update: true}. Just provide the transform() +// function in its expose descriptor; it will be automatically detected +// and used when appropriate. +// +// It's actually possible for a step to specify both transform and compute, +// in which case the transform() implementation will only be selected if +// the composition's base is {update: true}. It's not exactly known why you +// would want to specify unique-but-related transform and compute behavior, +// but the basic possibility was too cool to skip out on. +// +// == Nesting compositions: == +// +// Compositional steps are so convenient that you just might want to bundle +// them together, and form a whole new step-shaped unit of its own! +// +// In order to allow for this while helping to ensure internal dependencies +// remain neatly isolated from the composition which nests your bundle, +// the Thing.composite.from() function will accept and adapt to a base that +// specifies the {compose: true} flag, just like the steps preceding it. +// +// The continuation function that gets provided to the base will be mildly +// special - after all, nothing follows the base within the composition's +// own list! Instead of appending dependencies alongside any previously +// provided ones to be available to the next step, the base's continuation +// function should be used to define "exports" of the composition as a +// whole. It's similar to the usual behavior of the continuation, just +// expanded to the scope of the composition instead of following steps. +// +// For example, suppose your composition (which you expect to include in +// other compositions) brings about several private, hash-prefixed +// dependencies to contribute to its own results. Those dependencies won't +// end up "bleeding" into the dependency list of whichever composition is +// nesting this one - they will totally disappear once all the steps in +// the nested composition have finished up. +// +// To "export" the results of processing all those dependencies (provided +// that's something you want to do and this composition isn't used purely +// for a conditional early-exit), you'll want to define them in the +// continuation passed to the base. (Customarily, those should start with +// a hash just like the exports from any other compositional step; they're +// still dynamically provided dependencies!) +// +// Another way to "export" dependencies is by using calling *any* step's +// `continuation.raise()` function. This is sort of like early exiting, +// but instead of quitting out the whole entire property, it will just +// break out of the current, nested composition's list of steps, acting +// as though the composition had finished naturally. The dependencies +// passed to `raise` will be the ones which get exported. +// +// Since `raise` is another way to export dependencies, if you're using +// dynamic export names, you should specify `mapContinuation` on the step +// calling `continuation.raise` as well. +// +// An important note on `mapDependencies` here: A nested composition gets +// free access to all the ordinary properties defined on the thing it's +// working on, but if you want it to depend on *private* dependencies - +// ones prefixed with '#' - which were provided by some other compositional +// step preceding wherever this one gets nested, then you *have* to use +// `mapDependencies` to gain access. Check out the section on "cache-safe +// dependency names" for information on this syntax! +// +// Also - on rare occasion - you might want to make a reusable composition +// that itself causes the composition *it's* nested in to raise. If that's +// the case, give `composition.raiseAbove()` a go! This effectively means +// kicking out of *two* layers of nested composition - the one including +// the step with the `raiseAbove` call, and the composition which that one +// is nested within. You don't need to use `raiseAbove` if the reusable +// utility function just returns a single compositional step, but if you +// want to make use of other compositional steps, it gives you access to +// the same conditional-raise capabilities. +// +// Have some syntax sugar! Since nested compositions are defined by having +// the base be {compose: true}, the composition will infer as much if you +// don't specifying the base's flags at all. Simply use the same shorthand +// syntax as for other compositional steps, and it'll work out cleanly! +// + +import {empty, filterProperties, openAggregate} from '#sugar'; + +import Thing from './thing.js'; + +export {compositeFrom as from}; +function compositeFrom(firstArg, secondArg) { + const debug = fn => { + if (compositeFrom.debug === true) { + const label = + (annotation + ? color.dim(`[composite: ${annotation}]`) + : color.dim(`[composite]`)); + const result = fn(); + if (Array.isArray(result)) { + console.log(label, ...result.map(value => + (typeof value === 'object' + ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) + : value))); + } else { + console.log(label, result); + } + } + }; + + let annotation, composition; + if (typeof firstArg === 'string') { + [annotation, composition] = [firstArg, secondArg]; + } else { + [annotation, composition] = [null, firstArg]; + } + + const base = composition.at(-1); + const steps = composition.slice(); + + const aggregate = openAggregate({ + message: + `Errors preparing Thing.composite.from() composition` + + (annotation ? ` (${annotation})` : ''), + }); + + const baseExposes = + (base.flags + ? base.flags.expose + : true); + + const baseUpdates = + (base.flags + ? base.flags.update + : false); + + const baseComposes = + (base.flags + ? base.flags.compose + : true); + + if (!baseExposes) { + aggregate.push(new TypeError(`All steps, including base, must expose`)); + } + + const exposeDependencies = new Set(); + + let anyStepsCompute = false; + let anyStepsTransform = false; + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; + const message = + `Errors in step #${i + 1}` + + (isBase ? ` (base)` : ``) + + (step.annotation ? ` (${step.annotation})` : ``); + + aggregate.nest({message}, ({push}) => { + if (step.flags) { + let flagsErrored = false; + + if (!step.flags.compose && !isBase) { + push(new TypeError(`All steps but base must compose`)); + flagsErrored = true; + } + + if (!step.flags.expose) { + push(new TypeError(`All steps must expose`)); + flagsErrored = true; + } + + if (flagsErrored) { + return; + } + } + + const expose = + (step.flags + ? step.expose + : step); + + const stepComputes = !!expose.compute; + const stepTransforms = !!expose.transform; + + if (!stepComputes && !stepTransforms) { + push(new TypeError(`Steps must provide compute or transform (or both)`)); + return; + } + + if ( + stepTransforms && !stepComputes && + !baseUpdates && !baseComposes + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + return; + } + + if (stepComputes) { + anyStepsCompute = true; + } + + if (stepTransforms) { + anyStepsTransform = true; + } + + // Unmapped dependencies are exposed on the final composition only if + // they're "public", i.e. pointing to update values of other properties + // on the CacheableObject. + for (const dependency of expose.dependencies ?? []) { + if (typeof dependency === 'string' && dependency.startsWith('#')) { + continue; + } + + exposeDependencies.add(dependency); + } + + // Mapped dependencies are always exposed on the final composition. + // These are explicitly for reading values which are named outside of + // the current compositional step. + for (const dependency of Object.values(expose.mapDependencies ?? {})) { + exposeDependencies.add(dependency); + } + }); + } + + if (!baseComposes) { + if (baseUpdates) { + if (!anyStepsTransform) { + push(new TypeError(`Expected at least one step to transform`)); + } + } else { + if (!anyStepsCompute) { + push(new TypeError(`Expected at least one step to compute`)); + } + } + } + + aggregate.close(); + + const constructedDescriptor = {}; + + if (annotation) { + constructedDescriptor.annotation = annotation; + } + + constructedDescriptor.flags = { + update: baseUpdates, + expose: baseExposes, + compose: baseComposes, + }; + + if (baseUpdates) { + constructedDescriptor.update = base.update; + } + + if (baseExposes) { + const expose = constructedDescriptor.expose = {}; + expose.dependencies = Array.from(exposeDependencies); + + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); + + function _filterDependencies(availableDependencies, { + dependencies, + mapDependencies, + options, + }) { + const filteredDependencies = + (dependencies + ? filterProperties(availableDependencies, dependencies) + : {}); + + if (mapDependencies) { + for (const [to, from] of Object.entries(mapDependencies)) { + filteredDependencies[to] = availableDependencies[from] ?? null; + } + } + + if (options) { + filteredDependencies['#options'] = options; + } + + return filteredDependencies; + } + + function _assignDependencies(continuationAssignment, {mapContinuation}) { + if (!mapContinuation) { + return continuationAssignment; + } + + const assignDependencies = {}; + + for (const [from, to] of Object.entries(mapContinuation)) { + assignDependencies[to] = continuationAssignment[from] ?? null; + } + + return assignDependencies; + } + + function _prepareContinuation(callingTransformForThisStep) { + const continuationStorage = { + returnedWith: null, + providedDependencies: undefined, + providedValue: undefined, + }; + + const continuation = + (callingTransformForThisStep + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.exit = (providedValue) => { + continuationStorage.returnedWith = 'exit'; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + }; + + if (baseComposes) { + const makeRaiseLike = returnWith => + (callingTransformForThisStep + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = returnWith; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = returnWith; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); + } + + return {continuation, continuationStorage}; + } + + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; + + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); + + const availableDependencies = {...initialDependencies}; + + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; + + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); + + const expose = + (step.flags + ? step.expose + : step); + + const callingTransformForThisStep = + expectingTransform && expose.transform; + + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); + + debug(() => [ + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); + + const result = + (callingTransformForThisStep + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.compute(filteredDependencies, continuation)); + + if (result !== continuationSymbol) { + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); + } + + debug(() => color.bright(`end composition - exit (inferred)`)); + + return result; + } + + const {returnedWith} = continuationStorage; + + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; + + debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); + debug(() => color.bright(`end composition - exit (explicit)`)); + + if (baseComposes) { + return continuationIfApplicable.exit(providedValue); + } else { + return providedValue; + } + } + + const {providedValue, providedDependencies} = continuationStorage; + + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); + + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); + + const continuationArgs = []; + if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); + if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); + + debug(() => { + const base = `step #${i+1} - result: ` + returnedWith; + const parts = []; + + if (callingTransformForThisStep) { + if (continuingWithValue === undefined) { + parts.push(`(no value)`); + } else { + parts.push(`value:`, providedValue); + } + } + + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } + + if (empty(parts)) { + return base; + } else { + return [base + ' ->', ...parts]; + } + }); + + switch (returnedWith) { + case 'raise': + debug(() => + (isBase + ? color.bright(`end composition - raise (base: explicit)`) + : color.bright(`end composition - raise`))); + return continuationIfApplicable(...continuationArgs); + + case 'raiseAbove': + debug(() => color.bright(`end composition - raiseAbove`)); + return continuationIfApplicable.raise(...continuationArgs); + + case 'continuation': + if (isBase) { + debug(() => color.bright(`end composition - raise (inferred)`)); + return continuationIfApplicable(...continuationArgs); + } else { + Object.assign(availableDependencies, continuingWithDependencies); + break; + } + } + } + } + + const transformFn = + (value, initialDependencies, continuationIfApplicable) => + _computeOrTransform(value, initialDependencies, continuationIfApplicable); + + const computeFn = + (initialDependencies, continuationIfApplicable) => + _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); + + if (baseComposes) { + if (anyStepsTransform) expose.transform = transformFn; + if (anyStepsCompute) expose.compute = computeFn; + } else if (baseUpdates) { + expose.transform = transformFn; + } else { + expose.compute = computeFn; + } + } + + return constructedDescriptor; +} + +// Evaluates a function with composite debugging enabled, turns debugging +// off again, and returns the result of the function. This is mostly syntax +// sugar, but also helps avoid unit tests avoid accidentally printing debug +// info for a bunch of unrelated composites (due to property enumeration +// when displaying an unexpected result). Use as so: +// +// Without debugging: +// t.same(thing.someProp, value) +// +// With debugging: +// t.same(Thing.composite.debug(() => thing.someProp), value) +// +export function debug(fn) { + compositeFrom.debug = true; + const value = fn(); + compositeFrom.debug = false; + return value; +} + +// -- Compositional steps for compositions to nest -- + +// Provides dependencies exactly as they are (or null if not defined) to the +// continuation. Although this can *technically* be used to alias existing +// dependencies to some other name within the middle of a composition, it's +// intended to be used only as a composition's base - doing so makes the +// composition as a whole suitable as a step in some other composition, +// providing the listed (internal) dependencies to later steps just like +// other compositional steps. +export {_export as export}; +function _export(mapping) { + const mappingEntries = Object.entries(mapping); + + return { + annotation: `Thing.composite.export`, + flags: {expose: true, compose: true}, + + expose: { + options: {mappingEntries}, + dependencies: Object.values(mapping), + + compute({'#options': {mappingEntries}, ...dependencies}, continuation) { + const exports = {}; + + // Note: This is slightly different behavior from filterProperties, + // as defined in sugar.js, which doesn't fall back to null for + // properties which don't exist on the original object. + for (const [exportKey, dependencyKey] of mappingEntries) { + exports[exportKey] = + (Object.hasOwn(dependencies, dependencyKey) + ? dependencies[dependencyKey] + : null); + } + + return continuation.raise(exports); + } + }, + }; +} + +// -- Compositional steps for top-level property descriptors -- + +// Exposes a dependency exactly as it is; this is typically the base of a +// composition which was created to serve as one property's descriptor. +// Since this serves as a base, specify a value for {update} to indicate +// that the property as a whole updates (and some previous compositional +// step works with that update value). Set {update: true} to only enable +// the update flag, or set update to an object to specify a descriptor +// (e.g. for custom value validation). +// +// Please note that this *doesn't* verify that the dependency exists, so +// if you provide the wrong name or it hasn't been set by a previous +// compositional step, the property will be exposed as undefined instead +// of null. +// +export function exposeDependency(dependency, { + update = false, +} = {}) { + return { + annotation: `Thing.composite.exposeDependency`, + flags: {expose: true, update: !!update}, + + expose: { + mapDependencies: {dependency}, + compute: ({dependency}) => dependency, + }, + + update: + (typeof update === 'object' + ? update + : null), + }; +} + +// Exposes a constant value exactly as it is; like exposeDependency, this +// is typically the base of a composition serving as a particular property +// descriptor. It generally follows steps which will conditionally early +// exit with some other value, with the exposeConstant base serving as the +// fallback default value. Like exposeDependency, set {update} to true or +// an object to indicate that the property as a whole updates. +export function exposeConstant(value, { + update = false, +} = {}) { + return { + annotation: `Thing.composite.exposeConstant`, + flags: {expose: true, update: !!update}, + + expose: { + options: {value}, + compute: ({'#options': {value}}) => value, + }, + + update: + (typeof update === 'object' + ? update + : null), + }; +} + +// Checks the availability of a dependency or the update value and provides +// the result to later steps under '#availability' (by default). This is +// mainly intended for use by the more specific utilities, which you should +// consider using instead. Customize {mode} to select one of these modes, +// or leave unset and default to 'null': +// +// * 'null': Check that the value isn't null. +// * 'empty': Check that the value is neither null nor an empty array. +// * 'falsy': Check that the value isn't false when treated as a boolean +// (nor an empty array). Keep in mind this will also be false +// for values like zero and the empty string! +// +export function withResultOfAvailabilityCheck({ + fromUpdateValue, + fromDependency, + mode = 'null', + to = '#availability', +}) { + if (!['null', 'empty', 'falsy'].includes(mode)) { + throw new TypeError(`Expected mode to be null, empty, or falsy`); + } + + if (fromUpdateValue && fromDependency) { + throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); + } + + if (!fromUpdateValue && !fromDependency) { + throw new TypeError(`Missing dependency name (or fromUpdateValue)`); + } + + const checkAvailability = (value, mode) => { + switch (mode) { + case 'null': return value !== null; + case 'empty': return !empty(value); + case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); + default: return false; + } + }; + + if (fromDependency) { + return { + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {from: fromDependency}, + mapContinuation: {to}, + options: {mode}, + compute: ({from, '#options': {mode}}, continuation) => + continuation({to: checkAvailability(from, mode)}), + }, + }; + } else { + return { + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, + flags: {expose: true, compose: true}, + expose: { + mapContinuation: {to}, + options: {mode}, + transform: (value, {'#options': {mode}}, continuation) => + continuation(value, {to: checkAvailability(value, mode)}), + }, + }; + } +} + +// Exposes a dependency as it is, or continues if it's unavailable. +// See withResultOfAvailabilityCheck for {mode} options! +export function exposeDependencyOrContinue(dependency, { + mode = 'null', +} = {}) { + return compositeFrom(`Thing.composite.exposeDependencyOrContinue`, [ + withResultOfAvailabilityCheck({ + fromDependency: dependency, + mode, + }), + + { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, + + { + mapDependencies: {dependency}, + compute: ({dependency}, continuation) => + continuation.exit(dependency), + }, + ]); +} + +// Exposes the update value of an {update: true} property as it is, +// or continues if it's unavailable. See withResultOfAvailabilityCheck +// for {mode} options! +export function exposeUpdateValueOrContinue({ + mode = 'null', +} = {}) { + return compositeFrom(`Thing.composite.exposeUpdateValueOrContinue`, [ + withResultOfAvailabilityCheck({ + fromUpdateValue: true, + mode, + }), + + { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, + + { + transform: (value, {}, continuation) => + continuation.exit(value), + }, + ]); +} + +// Early exits if an availability check has failed. +// This is for internal use only - use `earlyExitWithoutDependency` or +// `earlyExitWIthoutUpdateValue` instead. +export function earlyExitIfAvailabilityCheckFailed({ + availability = '#availability', + value = null, +} = {}) { + return compositeFrom(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ + { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + + { + options: {value}, + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), + }, + ]); +} + +// Early exits if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function earlyExitWithoutDependency(dependency, { + mode = 'null', + value = null, +} = {}) { + return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + earlyExitIfAvailabilityCheckFailed({value}), + ]); +} + +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function earlyExitWithoutUpdateValue({ + mode = 'null', + value = null, +} = {}) { + return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + earlyExitIfAvailabilityCheckFailed({value}), + ]); +} + +// Raises if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function raiseWithoutDependency(dependency, { + mode = 'null', + map = {}, + raise = {}, +} = {}) { + return compositeFrom(`Thing.composite.raiseWithoutDependency`, [ + withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + + { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + + { + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), + }, + ]); +} + +// Raises if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function raiseWithoutUpdateValue({ + mode = 'null', + map = {}, + raise = {}, +} = {}) { + return compositeFrom(`Thing.composite.raiseWithoutUpdateValue`, [ + withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + + { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + + { + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), + }, + ]); +} + +// -- Compositional steps for processing data -- + +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. +export function withResolvedContribs({from, to}) { + return { + annotation: `Thing.composite.withResolvedContribs`, + flags: {expose: true, compose: true}, + + expose: { + dependencies: ['artistData'], + mapDependencies: {from}, + mapContinuation: {to}, + compute: ({artistData, from}, continuation) => + continuation({ + to: Thing.findArtistsFromContribs(from, artistData), + }), + }, + }; +} + +// Resolves a reference by using the provided find function to match it +// within the provided thingData dependency. This will early exit if the +// data dependency is null, or, if earlyExitIfNotFound is set to true, +// if the find function doesn't match anything for the reference. +// Otherwise, the data object is provided on the output dependency; +// or null, if the reference doesn't match anything or itself was null +// to begin with. +export function withResolvedReference({ + ref, + data, + to, + find: findFunction, + earlyExitIfNotFound = false, +}) { + return compositeFrom(`Thing.composite.withResolvedReference`, [ + raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + earlyExitWithoutDependency(data), + + { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, + + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } + + return continuation.raise({match}); + }, + }, + ]); +} + +// Check out the info on Thing.common.reverseReferenceList! +// This is its composable form. +export function withReverseReferenceList({ + data, + to = '#reverseReferenceList', + refList: refListProperty, +}) { + return compositeFrom(`Thing.common.reverseReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + { + dependencies: ['this'], + mapDependencies: {data}, + mapContinuation: {to}, + options: {refListProperty}, + + compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => + continuation({ + to: data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ]); +} diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ad27ca55..01aa8b1b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -27,6 +27,7 @@ import { } from '#validators'; import CacheableObject from './cacheable-object.js'; +import * as composite from './composite.js'; export default class Thing extends CacheableObject { static referenceType = Symbol('Thing.referenceType'); @@ -359,1178 +360,5 @@ export default class Thing extends CacheableObject { .filter(({who}) => who)); } - static composite = { - // Composes multiple compositional "steps" and a "base" to form a property - // descriptor out of modular building blocks. This is an extension to the - // more general-purpose CacheableObject property descriptor syntax, and - // aims to make modular data processing - which lends to declarativity - - // much easier, without fundamentally altering much of the typical syntax - // or terminology, nor building on it to an excessive degree. - // - // Think of a composition as being a chain of steps which lead into a final - // base property, which is usually responsible for returning the value that - // will actually get exposed when the property being described is accessed. - // - // == The compositional base: == - // - // The final item in a compositional list is its base, and it identifies - // the essential qualities of the property descriptor. The compositional - // steps preceding it may exit early, in which case the expose function - // defined on the base won't be called; or they will provide dependencies - // that the base may use to compute the final value that gets exposed for - // this property. - // - // The base indicates the capabilities of the composition as a whole. - // It should be {expose: true}, since that's the only area that preceding - // compositional steps (currently) can actually influence. If it's also - // {update: true}, then the composition as a whole accepts an update value - // just like normal update-flag property descriptors - meaning it can be - // set with `thing.someProperty = value` and that value will be paseed - // into each (implementing) step's transform() function, as well as the - // base. Bases usually aren't {compose: true}, but can be - check out the - // section on "nesting compositions" for details about that. - // - // Every composition always has exactly one compositional base, and it's - // always the last item in the composition list. All items preceding it - // are compositional steps, described below. - // - // == Compositional steps: == - // - // Compositional steps are, in essence, typical property descriptors with - // the extra flag {compose: true}. They operate on existing dependencies, - // and are typically dynamically constructed by "utility" functions (but - // can also be manually declared within the step list of a composition). - // Compositional steps serve two purposes: - // - // 1. exit early, if some condition is matched, returning and exposing - // some value directly from that step instead of continuing further - // down the step list; - // - // 2. and/or provide new, dynamically created "private" dependencies which - // can be accessed by further steps down the list, or at the base at - // the bottom, modularly supplying information that will contribute to - // the final value exposed for this property. - // - // Usually it's just one of those two, but it's fine for a step to perform - // both jobs if the situation benefits. - // - // Compositional steps are the real "modular" or "compositional" part of - // this data processing style - they're designed to be combined together - // in dynamic, versatile ways, as each property demands it. You usually - // define a compositional step to be returned by some ordinary static - // property-descriptor-returning function (customarily namespaced under - // the relevant Thing class's static `composite` field) - that lets you - // reuse it in multiple compositions later on. - // - // Compositional steps are implemented with "continuation passing style", - // meaning the connection to the next link on the chain is passed right to - // each step's compute (or transform) function, and the implementation gets - // to decide whether to continue on that chain or exit early by returning - // some other value. - // - // Every step along the chain, apart from the base at the bottom, has to - // have the {compose: true} step. That means its compute() or transform() - // function will be passed an extra argument at the end, `continuation`. - // To provide new dependencies to items further down the chain, just pass - // them directly to this continuation() function, customarily with a hash - // ('#') prefixing each name - for example: - // - // compute({..some dependencies..}, continuation) { - // return continuation({ - // '#excitingProperty': (..a value made from dependencies..), - // }); - // } - // - // Performing an early exit is as simple as returning some other value, - // instead of the continuation. You may also use `continuation.exit(value)` - // to perform the exact same kind of early exit - it's just a different - // syntax that might fit in better in certain longer compositions. - // - // It may be fine to simply provide new dependencies under a hard-coded - // name, such as '#excitingProperty' above, but if you're writing a utility - // that dynamically returns the compositional step and you suspect you - // might want to use this step multiple times in a single composition, - // it's customary to accept a name for the result. - // - // Here's a detailed example showing off early exit, dynamically operating - // on a provided dependency name, and then providing a result in another - // also-provided dependency name: - // - // static Thing.composite.withResolvedContribs = ({ - // from: contribsByRefDependency, - // to: outputDependency, - // }) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: [contribsByRefDependency, 'artistData'], - // compute({ - // [contribsByRefDependency]: contribsByRef, - // artistData, - // }, continuation) { - // if (!artistData) return null; /* early exit! */ - // return continuation({ - // [outputDependency]: /* this is the important part */ - // (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // And how you might work that into a composition: - // - // static Track[Thing.getPropertyDescriptors].coverArtists = - // Thing.composite.from([ - // Track.composite.doSomethingWhichMightEarlyExit(), - // Thing.composite.withResolvedContribs({ - // from: 'coverArtistContribsByRef', - // to: '#coverArtistContribs', - // }), - // - // { - // flags: {expose: true}, - // expose: { - // dependencies: ['#coverArtistContribs'], - // compute({'#coverArtistContribs': coverArtistContribs}) { - // return coverArtistContribs.map(({who}) => who); - // }, - // }, - // }, - // ]); - // - // One last note! A super common code pattern when creating more complex - // compositions is to have several steps which *only* expose and compose. - // As a syntax shortcut, you can skip the outer section. It's basically - // like writing out just the {expose: {...}} part. Remember that this - // indicates that the step you're defining is compositional, so you have - // to specify the flags manually for the base, even if this property isn't - // going to get an {update: true} flag. - // - // == Cache-safe dependency names: == - // - // [Disclosure: The caching engine hasn't actually been implemented yet. - // As such, this section is subject to change, and simply provides sound - // forward-facing advice and interfaces.] - // - // It's a good idea to write individual compositional steps in such a way - // that they're "cache-safe" - meaning the same input (dependency) values - // will always result in the same output (continuation or early exit). - // - // In order to facilitate this, compositional step descriptors may specify - // unique `mapDependencies`, `mapContinuation`, and `options` values. - // - // Consider the `withResolvedContribs` example adjusted to make use of - // two of these options below: - // - // static Thing.composite.withResolvedContribs = ({ - // from: contribsByRefDependency, - // to: outputDependency, - // }) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: ['artistData'], - // mapDependencies: {contribsByRef: contribsByRefDependency}, - // mapContinuation: {outputDependency}, - // compute({ - // contribsByRef, /* no longer in square brackets */ - // artistData, - // }, continuation) { - // if (!artistData) return null; - // return continuation({ - // outputDependency: /* no longer in square brackets */ - // (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // With a little destructuring and restructuring JavaScript sugar, the - // above can be simplified some more: - // - // static Thing.composite.withResolvedContribs = ({from, to}) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: ['artistData'], - // mapDependencies: {from}, - // mapContinuation: {to}, - // compute({artistData, from: contribsByRef}, continuation) { - // if (!artistData) return null; - // return continuation({ - // to: (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // These two properties let you separate the name-mapping behavior (for - // dependencies and the continuation) from the main body of the compute - // function. That means the compute function will *always* get inputs in - // the same form (dependencies 'artistData' and 'from' above), and will - // *always* provide its output in the same form (early return or 'to'). - // - // Thanks to that, this `compute` function is cache-safe! Its outputs can - // be cached corresponding to each set of mapped inputs. So it won't matter - // whether the `from` dependency is named `coverArtistContribsByRef` or - // `contributorContribsByRef` or something else - the compute function - // doesn't care, and only expects that value to be provided via its `from` - // argument. Likewise, it doesn't matter if the output should be sent to - // '#coverArtistContribs` or `#contributorContribs` or some other name; - // the mapping is handled automatically outside, and compute will always - // output its value to the continuation's `to`. - // - // Note that `mapDependencies` and `mapContinuation` should be objects of - // the same "shape" each run - that is, the values will change depending on - // outside context, but the keys are always the same. You shouldn't use - // `mapDependencies` to dynamically select more or fewer dependencies. - // If you need to dynamically select a range of dependencies, just specify - // them in the `dependencies` array like usual. The caching engine will - // understand that differently named `dependencies` indicate separate - // input-output caches should be used. - // - // The 'options' property makes it possible to specify external arguments - // that fundamentally change the behavior of the `compute` function, while - // still remaining cache-safe. It indicates that the caching engine should - // use a completely different input-to-output cache for each permutation - // of the 'options' values. This way, those functions are still cacheable - // at all; they'll just be cached separately for each set of option values. - // Values on the 'options' property will always be provided in compute's - // dependencies under '#options' (to avoid name conflicts with other - // dependencies). - // - // == To compute or to transform: == - // - // A compositional step can work directly on a property's stored update - // value, transforming it in place and either early exiting with it or - // passing it on (via continuation) to the next item(s) in the - // compositional step list. (If needed, these can provide dependencies - // the same way as compute functions too - just pass that object after - // the updated (or same) transform value in your call to continuation().) - // - // But in order to make them more versatile, compositional steps have an - // extra trick up their sleeve. If a compositional step implements compute - // and *not* transform, it can still be used in a composition targeting a - // property which updates! These retain their full dependency-providing and - // early exit functionality - they just won't be provided the update value. - // If a compute-implementing step returns its continuation, then whichever - // later step (or the base) next implements transform() will receive the - // update value that had so far been running - as well as any dependencies - // the compute() step returned, of course! - // - // Please note that a compositional step which transforms *should not* - // specify, in its flags, {update: true}. Just provide the transform() - // function in its expose descriptor; it will be automatically detected - // and used when appropriate. - // - // It's actually possible for a step to specify both transform and compute, - // in which case the transform() implementation will only be selected if - // the composition's base is {update: true}. It's not exactly known why you - // would want to specify unique-but-related transform and compute behavior, - // but the basic possibility was too cool to skip out on. - // - // == Nesting compositions: == - // - // Compositional steps are so convenient that you just might want to bundle - // them together, and form a whole new step-shaped unit of its own! - // - // In order to allow for this while helping to ensure internal dependencies - // remain neatly isolated from the composition which nests your bundle, - // the Thing.composite.from() function will accept and adapt to a base that - // specifies the {compose: true} flag, just like the steps preceding it. - // - // The continuation function that gets provided to the base will be mildly - // special - after all, nothing follows the base within the composition's - // own list! Instead of appending dependencies alongside any previously - // provided ones to be available to the next step, the base's continuation - // function should be used to define "exports" of the composition as a - // whole. It's similar to the usual behavior of the continuation, just - // expanded to the scope of the composition instead of following steps. - // - // For example, suppose your composition (which you expect to include in - // other compositions) brings about several private, hash-prefixed - // dependencies to contribute to its own results. Those dependencies won't - // end up "bleeding" into the dependency list of whichever composition is - // nesting this one - they will totally disappear once all the steps in - // the nested composition have finished up. - // - // To "export" the results of processing all those dependencies (provided - // that's something you want to do and this composition isn't used purely - // for a conditional early-exit), you'll want to define them in the - // continuation passed to the base. (Customarily, those should start with - // a hash just like the exports from any other compositional step; they're - // still dynamically provided dependencies!) - // - // Another way to "export" dependencies is by using calling *any* step's - // `continuation.raise()` function. This is sort of like early exiting, - // but instead of quitting out the whole entire property, it will just - // break out of the current, nested composition's list of steps, acting - // as though the composition had finished naturally. The dependencies - // passed to `raise` will be the ones which get exported. - // - // Since `raise` is another way to export dependencies, if you're using - // dynamic export names, you should specify `mapContinuation` on the step - // calling `continuation.raise` as well. - // - // An important note on `mapDependencies` here: A nested composition gets - // free access to all the ordinary properties defined on the thing it's - // working on, but if you want it to depend on *private* dependencies - - // ones prefixed with '#' - which were provided by some other compositional - // step preceding wherever this one gets nested, then you *have* to use - // `mapDependencies` to gain access. Check out the section on "cache-safe - // dependency names" for information on this syntax! - // - // Also - on rare occasion - you might want to make a reusable composition - // that itself causes the composition *it's* nested in to raise. If that's - // the case, give `composition.raiseAbove()` a go! This effectively means - // kicking out of *two* layers of nested composition - the one including - // the step with the `raiseAbove` call, and the composition which that one - // is nested within. You don't need to use `raiseAbove` if the reusable - // utility function just returns a single compositional step, but if you - // want to make use of other compositional steps, it gives you access to - // the same conditional-raise capabilities. - // - // Have some syntax sugar! Since nested compositions are defined by having - // the base be {compose: true}, the composition will infer as much if you - // don't specifying the base's flags at all. Simply use the same shorthand - // syntax as for other compositional steps, and it'll work out cleanly! - // - from(firstArg, secondArg) { - const debug = fn => { - if (Thing.composite.from.debug === true) { - const label = - (annotation - ? color.dim(`[composite: ${annotation}]`) - : color.dim(`[composite]`)); - const result = fn(); - if (Array.isArray(result)) { - console.log(label, ...result.map(value => - (typeof value === 'object' - ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) - : value))); - } else { - console.log(label, result); - } - } - }; - - let annotation, composition; - if (typeof firstArg === 'string') { - [annotation, composition] = [firstArg, secondArg]; - } else { - [annotation, composition] = [null, firstArg]; - } - - const base = composition.at(-1); - const steps = composition.slice(); - - const aggregate = openAggregate({ - message: - `Errors preparing Thing.composite.from() composition` + - (annotation ? ` (${annotation})` : ''), - }); - - const baseExposes = - (base.flags - ? base.flags.expose - : true); - - const baseUpdates = - (base.flags - ? base.flags.update - : false); - - const baseComposes = - (base.flags - ? base.flags.compose - : true); - - if (!baseExposes) { - aggregate.push(new TypeError(`All steps, including base, must expose`)); - } - - const exposeDependencies = new Set(); - - let anyStepsCompute = false; - let anyStepsTransform = false; - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; - const message = - `Errors in step #${i + 1}` + - (isBase ? ` (base)` : ``) + - (step.annotation ? ` (${step.annotation})` : ``); - - aggregate.nest({message}, ({push}) => { - if (step.flags) { - let flagsErrored = false; - - if (!step.flags.compose && !isBase) { - push(new TypeError(`All steps but base must compose`)); - flagsErrored = true; - } - - if (!step.flags.expose) { - push(new TypeError(`All steps must expose`)); - flagsErrored = true; - } - - if (flagsErrored) { - return; - } - } - - const expose = - (step.flags - ? step.expose - : step); - - const stepComputes = !!expose.compute; - const stepTransforms = !!expose.transform; - - if (!stepComputes && !stepTransforms) { - push(new TypeError(`Steps must provide compute or transform (or both)`)); - return; - } - - if ( - stepTransforms && !stepComputes && - !baseUpdates && !baseComposes - ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - return; - } - - if (stepComputes) { - anyStepsCompute = true; - } - - if (stepTransforms) { - anyStepsTransform = true; - } - - // Unmapped dependencies are exposed on the final composition only if - // they're "public", i.e. pointing to update values of other properties - // on the CacheableObject. - for (const dependency of expose.dependencies ?? []) { - if (typeof dependency === 'string' && dependency.startsWith('#')) { - continue; - } - - exposeDependencies.add(dependency); - } - - // Mapped dependencies are always exposed on the final composition. - // These are explicitly for reading values which are named outside of - // the current compositional step. - for (const dependency of Object.values(expose.mapDependencies ?? {})) { - exposeDependencies.add(dependency); - } - }); - } - - if (!baseComposes) { - if (baseUpdates) { - if (!anyStepsTransform) { - push(new TypeError(`Expected at least one step to transform`)); - } - } else { - if (!anyStepsCompute) { - push(new TypeError(`Expected at least one step to compute`)); - } - } - } - - aggregate.close(); - - const constructedDescriptor = {}; - - if (annotation) { - constructedDescriptor.annotation = annotation; - } - - constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, - }; - - if (baseUpdates) { - constructedDescriptor.update = base.update; - } - - if (baseExposes) { - const expose = constructedDescriptor.expose = {}; - expose.dependencies = Array.from(exposeDependencies); - - const continuationSymbol = Symbol('continuation symbol'); - const noTransformSymbol = Symbol('no-transform symbol'); - - function _filterDependencies(availableDependencies, { - dependencies, - mapDependencies, - options, - }) { - const filteredDependencies = - (dependencies - ? filterProperties(availableDependencies, dependencies) - : {}); - - if (mapDependencies) { - for (const [to, from] of Object.entries(mapDependencies)) { - filteredDependencies[to] = availableDependencies[from] ?? null; - } - } - - if (options) { - filteredDependencies['#options'] = options; - } - - return filteredDependencies; - } - - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } - - const assignDependencies = {}; - - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; - } - - return assignDependencies; - } - - function _prepareContinuation(callingTransformForThisStep) { - const continuationStorage = { - returnedWith: null, - providedDependencies: undefined, - providedValue: undefined, - }; - - const continuation = - (callingTransformForThisStep - ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; - continuationStorage.providedDependencies = providedDependencies; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - } - : (providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; - continuationStorage.providedDependencies = providedDependencies; - return continuationSymbol; - }); - - continuation.exit = (providedValue) => { - continuationStorage.returnedWith = 'exit'; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - }; - - if (baseComposes) { - const makeRaiseLike = returnWith => - (callingTransformForThisStep - ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - } - : (providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - return continuationSymbol; - }); - - continuation.raise = makeRaiseLike('raise'); - continuation.raiseAbove = makeRaiseLike('raiseAbove'); - } - - return {continuation, continuationStorage}; - } - - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { - const expectingTransform = initialValue !== noTransformSymbol; - - let valueSoFar = - (expectingTransform - ? initialValue - : undefined); - - const availableDependencies = {...initialDependencies}; - - if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); - } else { - debug(() => color.bright(`begin composition - not transforming`)); - } - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; - - debug(() => [ - `step #${i+1}` + - (isBase - ? ` (base):` - : ` of ${steps.length}:`), - step]); - - const expose = - (step.flags - ? step.expose - : step); - - const callingTransformForThisStep = - expectingTransform && expose.transform; - - const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); - - debug(() => [ - `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); - - const result = - (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); - - if (result !== continuationSymbol) { - debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - - if (baseComposes) { - throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); - } - - debug(() => color.bright(`end composition - exit (inferred)`)); - - return result; - } - - const {returnedWith} = continuationStorage; - - if (returnedWith === 'exit') { - const {providedValue} = continuationStorage; - - debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); - - if (baseComposes) { - return continuationIfApplicable.exit(providedValue); - } else { - return providedValue; - } - } - - const {providedValue, providedDependencies} = continuationStorage; - - const continuingWithValue = - (expectingTransform - ? (callingTransformForThisStep - ? providedValue ?? null - : valueSoFar ?? null) - : undefined); - - const continuingWithDependencies = - (providedDependencies - ? _assignDependencies(providedDependencies, expose) - : null); - - const continuationArgs = []; - if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); - if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - - debug(() => { - const base = `step #${i+1} - result: ` + returnedWith; - const parts = []; - - if (callingTransformForThisStep) { - if (continuingWithValue === undefined) { - parts.push(`(no value)`); - } else { - parts.push(`value:`, providedValue); - } - } - - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); - } else { - parts.push(`(no deps)`); - } - - if (empty(parts)) { - return base; - } else { - return [base + ' ->', ...parts]; - } - }); - - switch (returnedWith) { - case 'raise': - debug(() => - (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); - return continuationIfApplicable(...continuationArgs); - - case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); - return continuationIfApplicable.raise(...continuationArgs); - - case 'continuation': - if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); - return continuationIfApplicable(...continuationArgs); - } else { - Object.assign(availableDependencies, continuingWithDependencies); - break; - } - } - } - } - - const transformFn = - (value, initialDependencies, continuationIfApplicable) => - _computeOrTransform(value, initialDependencies, continuationIfApplicable); - - const computeFn = - (initialDependencies, continuationIfApplicable) => - _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - - if (baseComposes) { - if (anyStepsTransform) expose.transform = transformFn; - if (anyStepsCompute) expose.compute = computeFn; - } else if (baseUpdates) { - expose.transform = transformFn; - } else { - expose.compute = computeFn; - } - } - - return constructedDescriptor; - }, - - // Evaluates a function with composite debugging enabled, turns debugging - // off again, and returns the result of the function. This is mostly syntax - // sugar, but also helps avoid unit tests avoid accidentally printing debug - // info for a bunch of unrelated composites (due to property enumeration - // when displaying an unexpected result). Use as so: - // - // Without debugging: - // t.same(thing.someProp, value) - // - // With debugging: - // t.same(Thing.composite.debug(() => thing.someProp), value) - // - debug(fn) { - Thing.composite.from.debug = true; - const value = fn(); - Thing.composite.from.debug = false; - return value; - }, - - // -- Compositional steps for compositions to nest -- - - // Provides dependencies exactly as they are (or null if not defined) to the - // continuation. Although this can *technically* be used to alias existing - // dependencies to some other name within the middle of a composition, it's - // intended to be used only as a composition's base - doing so makes the - // composition as a whole suitable as a step in some other composition, - // providing the listed (internal) dependencies to later steps just like - // other compositional steps. - export(mapping) { - const mappingEntries = Object.entries(mapping); - - return { - annotation: `Thing.composite.export`, - flags: {expose: true, compose: true}, - - expose: { - options: {mappingEntries}, - dependencies: Object.values(mapping), - - compute({'#options': {mappingEntries}, ...dependencies}, continuation) { - const exports = {}; - - // Note: This is slightly different behavior from filterProperties, - // as defined in sugar.js, which doesn't fall back to null for - // properties which don't exist on the original object. - for (const [exportKey, dependencyKey] of mappingEntries) { - exports[exportKey] = - (Object.hasOwn(dependencies, dependencyKey) - ? dependencies[dependencyKey] - : null); - } - - return continuation.raise(exports); - } - }, - }; - }, - - // -- Compositional steps for top-level property descriptors -- - - // Exposes a dependency exactly as it is; this is typically the base of a - // composition which was created to serve as one property's descriptor. - // Since this serves as a base, specify a value for {update} to indicate - // that the property as a whole updates (and some previous compositional - // step works with that update value). Set {update: true} to only enable - // the update flag, or set update to an object to specify a descriptor - // (e.g. for custom value validation). - // - // Please note that this *doesn't* verify that the dependency exists, so - // if you provide the wrong name or it hasn't been set by a previous - // compositional step, the property will be exposed as undefined instead - // of null. - // - exposeDependency(dependency, { - update = false, - } = {}) { - return { - annotation: `Thing.composite.exposeDependency`, - flags: {expose: true, update: !!update}, - - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, - }, - - update: - (typeof update === 'object' - ? update - : null), - }; - }, - - // Exposes a constant value exactly as it is; like exposeDependency, this - // is typically the base of a composition serving as a particular property - // descriptor. It generally follows steps which will conditionally early - // exit with some other value, with the exposeConstant base serving as the - // fallback default value. Like exposeDependency, set {update} to true or - // an object to indicate that the property as a whole updates. - exposeConstant(value, { - update = false, - } = {}) { - return { - annotation: `Thing.composite.exposeConstant`, - flags: {expose: true, update: !!update}, - - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, - }, - - update: - (typeof update === 'object' - ? update - : null), - }; - }, - - // Checks the availability of a dependency or the update value and provides - // the result to later steps under '#availability' (by default). This is - // mainly intended for use by the more specific utilities, which you should - // consider using instead. Customize {mode} to select one of these modes, - // or leave unset and default to 'null': - // - // * 'null': Check that the value isn't null. - // * 'empty': Check that the value is neither null nor an empty array. - // * 'falsy': Check that the value isn't false when treated as a boolean - // (nor an empty array). Keep in mind this will also be false - // for values like zero and the empty string! - // - withResultOfAvailabilityCheck({ - fromUpdateValue, - fromDependency, - mode = 'null', - to = '#availability', - }) { - if (!['null', 'empty', 'falsy'].includes(mode)) { - throw new TypeError(`Expected mode to be null, empty, or falsy`); - } - - if (fromUpdateValue && fromDependency) { - throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); - } - - if (!fromUpdateValue && !fromDependency) { - throw new TypeError(`Missing dependency name (or fromUpdateValue)`); - } - - const checkAvailability = (value, mode) => { - switch (mode) { - case 'null': return value !== null; - case 'empty': return !empty(value); - case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); - default: return false; - } - }; - - if (fromDependency) { - return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {from: fromDependency}, - mapContinuation: {to}, - options: {mode}, - compute: ({from, '#options': {mode}}, continuation) => - continuation({to: checkAvailability(from, mode)}), - }, - }; - } else { - return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, - flags: {expose: true, compose: true}, - expose: { - mapContinuation: {to}, - options: {mode}, - transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {to: checkAvailability(value, mode)}), - }, - }; - } - }, - - // Exposes a dependency as it is, or continues if it's unavailable. - // See withResultOfAvailabilityCheck for {mode} options! - exposeDependencyOrContinue(dependency, { - mode = 'null', - } = {}) { - return Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, - - { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, - ]); - }, - - // Exposes the update value of an {update: true} property as it is, - // or continues if it's unavailable. See withResultOfAvailabilityCheck - // for {mode} options! - exposeUpdateValueOrContinue({ - mode = 'null', - } = {}) { - return Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromUpdateValue: true, - mode, - }), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, - - { - transform: (value, {}, continuation) => - continuation.exit(value), - }, - ]); - }, - - // Early exits if an availability check fails. - // This is for internal use only - use `earlyExitWithoutDependency` or - // `earlyExitWIthoutUpdateValue` instead. - earlyExitIfAvailabilityCheckFailed({ - availability = '#availability', - value = null, - }) { - return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, - ]); - }, - - // Early exits if a dependency isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutDependency(dependency, { - mode = 'null', - value = null, - } = {}) { - return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), - ]); - }, - - // Early exits if this property's update value isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutUpdateValue({ - mode = 'null', - value = null, - } = {}) { - return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), - ]); - }, - - // Raises if a dependency isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - raiseWithoutDependency(dependency, { - mode = 'null', - map = {}, - raise = {}, - } = {}) { - return Thing.composite.from(`Thing.composite.raiseWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); - }, - - // Raises if this property's update value isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - raiseWithoutUpdateValue({ - mode = 'null', - map = {}, - raise = {}, - } = {}) { - return Thing.composite.from(`Thing.composite.raiseWithoutUpdateValue`, [ - Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); - }, - - // -- Compositional steps for processing data -- - - // Resolves the contribsByRef contained in the provided dependency, - // providing (named by the second argument) the result. "Resolving" - // means mapping the "who" reference of each contribution to an artist - // object, and filtering out those whose "who" doesn't match any artist. - withResolvedContribs({from, to}) { - return { - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, - - expose: { - dependencies: ['artistData'], - mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => - continuation({ - to: Thing.findArtistsFromContribs(from, artistData), - }), - }, - }; - }, - - // Resolves a reference by using the provided find function to match it - // within the provided thingData dependency. This will early exit if the - // data dependency is null, or, if earlyExitIfNotFound is set to true, - // if the find function doesn't match anything for the reference. - // Otherwise, the data object is provided on the output dependency; - // or null, if the reference doesn't match anything or itself was null - // to begin with. - withResolvedReference({ - ref, - data, - to, - find: findFunction, - earlyExitIfNotFound = false, - }) { - return Thing.composite.from(`Thing.composite.withResolvedReference`, [ - Thing.composite.raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), - Thing.composite.earlyExitWithoutDependency(data), - - { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {match: to}, - - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && earlyExitIfNotFound) { - return continuation.exit(null); - } - - return continuation.raise({match}); - }, - }, - ]); - }, - - // Check out the info on Thing.common.reverseReferenceList! - // This is its composable form. - withReverseReferenceList({ - data, - to = '#reverseReferenceList', - refList: refListProperty, - }) { - return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.earlyExitWithoutDependency(data, {value: []}), - - { - dependencies: ['this'], - mapDependencies: {data}, - mapContinuation: {to}, - options: {refListProperty}, - - compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => - continuation({ - to: data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ]); - }, - }; + static composite = composite; } -- cgit 1.3.0-6-gf8a5 From 6f242fc864028a12321255ba04a88c6190801510 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 14:34:12 -0300 Subject: data: isolate withResolvedContribs internal behavior --- src/data/things/composite.js | 26 ++++++++++++++++++++------ src/data/things/thing.js | 30 ++++++++++-------------------- 2 files changed, 30 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1be60cd1..bf2d11ea 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -331,6 +331,7 @@ // syntax as for other compositional steps, and it'll work out cleanly! // +import find from '#find'; import {empty, filterProperties, openAggregate} from '#sugar'; import Thing from './thing.js'; @@ -1102,20 +1103,33 @@ export function raiseWithoutUpdateValue({ // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, to}) { - return { - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, + return Thing.composite.from(`Thing.composite.withResolvedContribs`, [ + Thing.composite.earlyExitWithoutDependency('artistData', { + value: [], + }), - expose: { + Thing.composite.raiseWithoutDependency(from, { + mode: 'empty', + map: {to}, + raise: {to: []}, + }), + + { dependencies: ['artistData'], mapDependencies: {from}, mapContinuation: {to}, compute: ({artistData, from}, continuation) => continuation({ - to: Thing.findArtistsFromContribs(from, artistData), + to: + from + .map(({who, what}) => ({ + who: find.artist(who, artistData, {mode: 'quiet'}), + what, + })) + .filter(({who}) => who), }), }, - }; + ]); } // Resolves a reference by using the provided find function to match it diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 01aa8b1b..9bfed080 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -249,14 +249,16 @@ export default class Thing extends CacheableObject { // filtered out. (So if the list is all empty, chances are that either the // reference list is somehow messed up, or artistData isn't being provided // properly.) - dynamicContribs: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: ['artistData', contribsByRefProperty], - compute: ({artistData, [contribsByRefProperty]: contribsByRef}) => - Thing.findArtistsFromContribs(contribsByRef, artistData), - }, - }), + dynamicContribs(contribsByRefProperty) { + return Thing.composite.from(`Thing.common.dynamicContribs`, [ + Thing.composite.withResolvedContribs({ + from: contribsByRefProperty, + to: '#contribs', + }), + + Thing.composite.exposeDependency('#contribs'), + ]); + }, // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. @@ -348,17 +350,5 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } - static findArtistsFromContribs(contribsByRef, artistData) { - if (empty(contribsByRef)) return null; - - return ( - contribsByRef - .map(({who, what}) => ({ - who: find.artist(who, artistData, {mode: 'quiet'}), - what, - })) - .filter(({who}) => who)); - } - static composite = composite; } -- cgit 1.3.0-6-gf8a5 From 117f1e6b707dfe102b968e421b21906d03100dc8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 14:57:16 -0300 Subject: data: new withResolvedReferenceList utility --- src/data/things/composite.js | 107 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index bf2d11ea..18a5f434 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,3 +1,15 @@ +import find from '#find'; +import {filterMultipleArrays} from '#wiki-data'; + +import { + empty, + filterProperties, + openAggregate, + stitchArrays, +} from '#sugar'; + +import Thing from './thing.js'; + // Composes multiple compositional "steps" and a "base" to form a property // descriptor out of modular building blocks. This is an extension to the // more general-purpose CacheableObject property descriptor syntax, and @@ -331,11 +343,6 @@ // syntax as for other compositional steps, and it'll work out cleanly! // -import find from '#find'; -import {empty, filterProperties, openAggregate} from '#sugar'; - -import Thing from './thing.js'; - export {compositeFrom as from}; function compositeFrom(firstArg, secondArg) { const debug = fn => { @@ -1104,10 +1111,6 @@ export function raiseWithoutUpdateValue({ // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, to}) { return Thing.composite.from(`Thing.composite.withResolvedContribs`, [ - Thing.composite.earlyExitWithoutDependency('artistData', { - value: [], - }), - Thing.composite.raiseWithoutDependency(from, { mode: 'empty', map: {to}, @@ -1115,20 +1118,32 @@ export function withResolvedContribs({from, to}) { }), { - dependencies: ['artistData'], mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => + compute: ({from}, continuation) => continuation({ - to: - from - .map(({who, what}) => ({ - who: find.artist(who, artistData, {mode: 'quiet'}), - what, - })) - .filter(({who}) => who), + '#whoByRef': from.map(({who}) => who), + '#what': from.map(({what}) => what), }), }, + + withResolvedReferenceList({ + refList: '#whoByRef', + data: 'artistData', + to: '#who', + find: find.artist, + notFoundMode: 'null', + }), + + { + dependencies: ['#who', '#what'], + mapContinuation: {to}, + compute({'#who': who, '#what': what}, continuation) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + to: stitchArrays({who, what}), + }); + }, + }, ]); } @@ -1168,6 +1183,60 @@ export function withResolvedReference({ ]); } +// Resolves a list of references, with each reference matched with provided +// data in the same way as withResolvedReference. This will early exit if the +// data dependency is null (even if the reference list is empty). By default +// it will filter out references which don't match, but this can be changed +// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). +export function withResolvedReferenceList({ + refList, + data, + to, + find: findFunction, + notFoundMode = 'filter', +}) { + if (!['filter', 'exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); + } + + return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + raiseWithoutDependency(refList, { + map: {to}, + raise: [], + mode: 'empty', + }), + + { + options: {findFunction, notFoundMode}, + mapDependencies: {refList, data}, + mapContinuation: {matches: to}, + + compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { + const matches = + refList.map(ref => findFunction(ref, data, {mode: 'quiet'})); + + if (!matches.includes(null)) { + return continuation.raise({matches}); + } + + switch (notFoundMode) { + case 'filter': + matches = matches.filter(value => value !== null); + return contination.raise({matches}); + + case 'exit': + return continuation.exit([]); + + case 'null': + return continuation.raise({matches}); + } + }, + }, + ]); +} + // Check out the info on Thing.common.reverseReferenceList! // This is its composable form. export function withReverseReferenceList({ -- cgit 1.3.0-6-gf8a5 From 91624e5d61a1473e143bad8860c8c2ccffec38fe Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 15:10:48 -0300 Subject: data: misc. eslint-caught fixes in composite.js --- src/data/things/composite.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 18a5f434..4f1abdb4 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,3 +1,6 @@ +import {inspect} from 'node:util'; + +import {color} from '#cli'; import find from '#find'; import {filterMultipleArrays} from '#wiki-data'; @@ -482,11 +485,11 @@ function compositeFrom(firstArg, secondArg) { if (!baseComposes) { if (baseUpdates) { if (!anyStepsTransform) { - push(new TypeError(`Expected at least one step to transform`)); + aggregate.push(new TypeError(`Expected at least one step to transform`)); } } else { if (!anyStepsCompute) { - push(new TypeError(`Expected at least one step to compute`)); + aggregate.push(new TypeError(`Expected at least one step to compute`)); } } } @@ -1087,8 +1090,8 @@ export function raiseWithoutUpdateValue({ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { - mapDependencies: {availability}, - compute: ({availability}, continuation) => + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => (availability ? continuation.raise() : continuation()), @@ -1214,7 +1217,7 @@ export function withResolvedReferenceList({ mapContinuation: {matches: to}, compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { - const matches = + let matches = refList.map(ref => findFunction(ref, data, {mode: 'quiet'})); if (!matches.includes(null)) { @@ -1224,7 +1227,7 @@ export function withResolvedReferenceList({ switch (notFoundMode) { case 'filter': matches = matches.filter(value => value !== null); - return contination.raise({matches}); + return continuation.raise({matches}); case 'exit': return continuation.exit([]); -- cgit 1.3.0-6-gf8a5 From 137bd813980d77441a86303ac6c04b61d9ccb8da Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 15:14:37 -0300 Subject: data: isolate internals of dynamicThingsFromReferenceList --- src/data/things/composite.js | 2 +- src/data/things/thing.js | 26 ++++++++++---------------- 2 files changed, 11 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4f1abdb4..e930e228 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1207,7 +1207,7 @@ export function withResolvedReferenceList({ raiseWithoutDependency(refList, { map: {to}, - raise: [], + raise: {to: []}, mode: 'empty', }), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9bfed080..9f77c3fc 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -194,26 +194,20 @@ export default class Thing extends CacheableObject { // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. dynamicThingsFromReferenceList( - refs, + refList, data, findFunction ) { return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ - Thing.composite.earlyExitWithoutDependency(refs, {value: []}), - Thing.composite.earlyExitWithoutDependency(data, {value: []}), - - { - flags: {expose: true}, - expose: { - mapDependencies: {refs, data}, - options: {findFunction}, - - compute: ({refs, data, '#options': {findFunction}}) => - refs - .map(ref => findFunction(ref, data, {mode: 'quiet'})) - .filter(Boolean), - }, - }, + Thing.composite.withResolvedReferenceList({ + refList, + data, + to: '#things', + find: findFunction, + notFoundMode: 'filter', + }), + + Thing.composite.exposeDependency('#things'), ]); }, -- cgit 1.3.0-6-gf8a5 From 2d7c536ee91a8f5bf8f16db1fc2d0a4d8bb4fc85 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 15:22:58 -0300 Subject: data: dynamicThingsFromReferenceList -> resolvedReferenceList --- src/data/things/album.js | 13 +++++++++++-- src/data/things/composite.js | 12 ++++++------ src/data/things/flash.js | 16 ++++++++++------ src/data/things/group.js | 16 ++++++++++------ src/data/things/homepage-layout.js | 10 +++++----- src/data/things/thing.js | 12 ++++++------ src/data/things/track.js | 18 +++++++++++++++--- src/data/things/wiki-info.js | 6 +++++- 8 files changed, 68 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 06982903..81f04f70 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -128,8 +128,17 @@ export class Album extends Thing { commentatorArtists: Thing.common.commentatorArtists(), - groups: Thing.common.dynamicThingsFromReferenceList('groupsByRef', 'groupData', find.group), - artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag), + groups: Thing.common.resolvedReferenceList({ + list: 'groupsByRef', + data: 'groupData', + find: find.group, + }), + + artTags: Thing.common.resolvedReferenceList({ + list: 'artTagsByRef', + data: 'artTagData', + find: find.artTag, + }), hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'), hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'), diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e930e228..7f3463cf 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1130,7 +1130,7 @@ export function withResolvedContribs({from, to}) { }, withResolvedReferenceList({ - refList: '#whoByRef', + list: '#whoByRef', data: 'artistData', to: '#who', find: find.artist, @@ -1192,7 +1192,7 @@ export function withResolvedReference({ // it will filter out references which don't match, but this can be changed // to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). export function withResolvedReferenceList({ - refList, + list, data, to, find: findFunction, @@ -1205,7 +1205,7 @@ export function withResolvedReferenceList({ return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), - raiseWithoutDependency(refList, { + raiseWithoutDependency(list, { map: {to}, raise: {to: []}, mode: 'empty', @@ -1213,12 +1213,12 @@ export function withResolvedReferenceList({ { options: {findFunction, notFoundMode}, - mapDependencies: {refList, data}, + mapDependencies: {list, data}, mapContinuation: {matches: to}, - compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { + compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { let matches = - refList.map(ref => findFunction(ref, data, {mode: 'quiet'})); + list.map(ref => findFunction(ref, data, {mode: 'quiet'})); if (!matches.includes(null)) { return continuation.raise({matches}); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 3f870c51..baef23d8 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -67,11 +67,11 @@ export class Flash extends Thing { contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'), - featuredTracks: Thing.common.dynamicThingsFromReferenceList( - 'featuredTracksByRef', - 'trackData', - find.track - ), + featuredTracks: Thing.common.resolvedReferenceList({ + list: 'featuredTracksByRef', + data: 'trackData', + find: find.track, + }), act: { flags: {expose: true}, @@ -141,6 +141,10 @@ export class FlashAct extends Thing { // Expose only - flashes: Thing.common.dynamicThingsFromReferenceList('flashesByRef', 'flashData', find.flash), + flashes: Thing.common.resolvedReferenceList({ + list: 'flashesByRef', + data: 'flashData', + find: find.flash, + }), }) } diff --git a/src/data/things/group.js b/src/data/things/group.js index f552b8f3..d04fcf56 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -26,7 +26,11 @@ export class Group extends Thing { // Expose only - featuredAlbums: Thing.common.dynamicThingsFromReferenceList('featuredAlbumsByRef', 'albumData', find.album), + featuredAlbums: Thing.common.resolvedReferenceList({ + list: 'featuredAlbumsByRef', + data: 'albumData', + find: find.album, + }), descriptionShort: { flags: {expose: true}, @@ -88,10 +92,10 @@ export class GroupCategory extends Thing { // Expose only - groups: Thing.common.dynamicThingsFromReferenceList( - 'groupsByRef', - 'groupData', - find.group - ), + groups: Thing.common.resolvedReferenceList({ + list: 'groupsByRef', + data: 'groupData', + find: find.group, + }), }); } diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index ec9e9556..cbdcb99a 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -125,10 +125,10 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { find.group ), - sourceAlbums: Thing.common.dynamicThingsFromReferenceList( - 'sourceAlbumsByRef', - 'albumData', - find.album - ), + sourceAlbums: Thing.common.resolvedReferenceList({ + list: 'sourceAlbumsByRef', + data: 'albumData', + find: find.album, + }), }); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9f77c3fc..f36b08bc 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -193,14 +193,14 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - dynamicThingsFromReferenceList( - refList, + resolvedReferenceList({ + list, data, - findFunction - ) { - return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ + find: findFunction, + }) { + return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ Thing.composite.withResolvedReferenceList({ - refList, + list, data, to: '#things', find: findFunction, diff --git a/src/data/things/track.js b/src/data/things/track.js index bf56a6dd..733c81c9 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -250,15 +250,27 @@ export class Track extends Thing { referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ Track.composite.inheritFromOriginalRelease({property: 'referencedTracks'}), - Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), + Thing.common.resolvedReferenceList({ + list: 'referencedTracksByRef', + data: 'trackData', + find: find.track, + }), ]), sampledTracks: Thing.composite.from(`Track.sampledTracks`, [ Track.composite.inheritFromOriginalRelease({property: 'sampledTracks'}), - Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), + Thing.common.resolvedReferenceList({ + list: 'sampledTracksByRef', + data: 'trackData', + find: find.track, + }), ]), - artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag), + artTags: Thing.common.resolvedReferenceList({ + list: 'artTagsByRef', + data: 'artTagData', + find: find.artTag, + }), // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index e8279987..d6790c55 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -59,6 +59,10 @@ export class WikiInfo extends Thing { // Expose only - divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList('divideTrackListsByGroupsByRef', 'groupData', find.group), + divideTrackListsByGroups: Thing.common.resolvedReferenceList({ + list: 'divideTrackListsByGroupsByRef', + data: 'groupData', + find: find.group, + }), }); } -- cgit 1.3.0-6-gf8a5 From 007c70642a60ed83bd840f550aa06563d4ba6a99 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 15:31:41 -0300 Subject: data: reverseReferenceList refList -> list --- src/data/things/composite.js | 2 +- src/data/things/thing.js | 4 ++-- src/data/things/track.js | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7f3463cf..138814d9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1244,8 +1244,8 @@ export function withResolvedReferenceList({ // This is its composable form. export function withReverseReferenceList({ data, + list: refListProperty, to = '#reverseReferenceList', - refList: refListProperty, }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f36b08bc..915474d4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -275,10 +275,10 @@ export default class Thing extends CacheableObject { // wiki data provided, not the requesting Thing itself. reverseReferenceList({ data, - refList, + list, }) { return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.withReverseReferenceList({data, refList}), + Thing.composite.withReverseReferenceList({data, list}), Thing.composite.exposeDependency('#reverseReferenceList'), ]); }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 733c81c9..87e796b9 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -287,7 +287,7 @@ export class Track extends Thing { featuredInFlashes: Thing.common.reverseReferenceList({ data: 'flashData', - refList: 'featuredTracks', + list: 'featuredTracks', }), }); @@ -559,8 +559,7 @@ export class Track extends Thing { return Thing.composite.from(`Track.composite.trackReverseReferenceList`, [ Thing.composite.withReverseReferenceList({ data: 'trackData', - refList: refListProperty, - originalTracksOnly: true, + list: refListProperty, }), { -- cgit 1.3.0-6-gf8a5 From 2437ac322a4c44f2fd9f6a77ac7a65bbb3afc2c0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 15:42:47 -0300 Subject: data: dynamicThingFromSingleReference -> resolvedReference --- src/data/things/composite.js | 4 ++-- src/data/things/homepage-layout.js | 10 +++++----- src/data/things/thing.js | 40 ++++++++++---------------------------- src/data/things/track.js | 6 +++++- 4 files changed, 22 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 138814d9..d3f76b11 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1160,8 +1160,8 @@ export function withResolvedContribs({from, to}) { export function withResolvedReference({ ref, data, - to, find: findFunction, + to = '#resolvedReference', earlyExitIfNotFound = false, }) { return compositeFrom(`Thing.composite.withResolvedReference`, [ @@ -1194,8 +1194,8 @@ export function withResolvedReference({ export function withResolvedReferenceList({ list, data, - to, find: findFunction, + to = '#resolvedReferenceList', notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index cbdcb99a..c478bc41 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -119,11 +119,11 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { // Expose only - sourceGroup: Thing.common.dynamicThingFromSingleReference( - 'sourceGroupByRef', - 'groupData', - find.group - ), + sourceGroup: Thing.common.resolvedReference({ + ref: 'sourceGroupByRef', + data: 'groupData', + find: find.group, + }), sourceAlbums: Thing.common.resolvedReferenceList({ list: 'sourceAlbumsByRef', diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 915474d4..36a1f58a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -193,40 +193,23 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - resolvedReferenceList({ - list, - data, - find: findFunction, - }) { + resolvedReferenceList({list, data, find}) { return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ Thing.composite.withResolvedReferenceList({ - list, - data, - to: '#things', - find: findFunction, + list, data, find, notFoundMode: 'filter', }), - - Thing.composite.exposeDependency('#things'), + Thing.composite.exposeDependency('#resolvedReferenceList'), ]); }, // Corresponding function for a single reference. - dynamicThingFromSingleReference: ( - singleReferenceProperty, - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, - - expose: { - dependencies: [singleReferenceProperty, thingDataProperty], - compute: ({ - [singleReferenceProperty]: ref, - [thingDataProperty]: thingData, - }) => (ref && thingData ? findFn(ref, thingData, {mode: 'quiet'}) : null), - }, - }), + resolvedReference({ref, data, find}) { + return Thing.composite.from(`Thing.common.resolvedReference`, [ + Thing.composite.withResolvedReference({ref, data, find}), + Thing.composite.exposeDependency('#resolvedReference'), + ]); + }, // Corresponding dynamic property to contribsByRef, which takes the values // in the provided property and searches the object's artistData for @@ -273,10 +256,7 @@ export default class Thing extends CacheableObject { // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. - reverseReferenceList({ - data, - list, - }) { + reverseReferenceList({data, list}) { return Thing.composite.from(`Thing.common.reverseReferenceList`, [ Thing.composite.withReverseReferenceList({data, list}), Thing.composite.exposeDependency('#reverseReferenceList'), diff --git a/src/data/things/track.js b/src/data/things/track.js index 87e796b9..2b628b66 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -145,7 +145,11 @@ export class Track extends Thing { // not generally relevant information). It's also not guaranteed that // dataSourceAlbum is available (depending on the Track creator to optionally // provide dataSourceAlbumByRef). - dataSourceAlbum: Thing.common.dynamicThingFromSingleReference('dataSourceAlbumByRef', 'albumData', find.album), + dataSourceAlbum: Thing.common.resolvedReference({ + ref: 'dataSourceAlbumByRef', + data: 'albumData', + find: find.album, + }), date: Thing.composite.from(`Track.date`, [ Thing.composite.exposeDependencyOrContinue('dateFirstReleased'), -- cgit 1.3.0-6-gf8a5 From 659620b7522d0e36ca15a54716b46d83f0bfc4f3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 17:45:56 -0300 Subject: data: move composite helper functions to top function scope --- src/data/things/composite.js | 390 +++++++++++++++++++++---------------------- 1 file changed, 195 insertions(+), 195 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index d3f76b11..805331a9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -496,255 +496,255 @@ function compositeFrom(firstArg, secondArg) { aggregate.close(); - const constructedDescriptor = {}; - - if (annotation) { - constructedDescriptor.annotation = annotation; - } + function _filterDependencies(availableDependencies, { + dependencies, + mapDependencies, + options, + }) { + const filteredDependencies = + (dependencies + ? filterProperties(availableDependencies, dependencies) + : {}); + + if (mapDependencies) { + for (const [to, from] of Object.entries(mapDependencies)) { + filteredDependencies[to] = availableDependencies[from] ?? null; + } + } - constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, - }; + if (options) { + filteredDependencies['#options'] = options; + } - if (baseUpdates) { - constructedDescriptor.update = base.update; + return filteredDependencies; } - if (baseExposes) { - const expose = constructedDescriptor.expose = {}; - expose.dependencies = Array.from(exposeDependencies); - - const continuationSymbol = Symbol('continuation symbol'); - const noTransformSymbol = Symbol('no-transform symbol'); - - function _filterDependencies(availableDependencies, { - dependencies, - mapDependencies, - options, - }) { - const filteredDependencies = - (dependencies - ? filterProperties(availableDependencies, dependencies) - : {}); - - if (mapDependencies) { - for (const [to, from] of Object.entries(mapDependencies)) { - filteredDependencies[to] = availableDependencies[from] ?? null; - } - } - - if (options) { - filteredDependencies['#options'] = options; - } - - return filteredDependencies; + function _assignDependencies(continuationAssignment, {mapContinuation}) { + if (!mapContinuation) { + return continuationAssignment; } - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } + const assignDependencies = {}; - const assignDependencies = {}; + for (const [from, to] of Object.entries(mapContinuation)) { + assignDependencies[to] = continuationAssignment[from] ?? null; + } - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; - } + return assignDependencies; + } - return assignDependencies; - } + function _prepareContinuation(callingTransformForThisStep) { + const continuationStorage = { + returnedWith: null, + providedDependencies: undefined, + providedValue: undefined, + }; - function _prepareContinuation(callingTransformForThisStep) { - const continuationStorage = { - returnedWith: null, - providedDependencies: undefined, - providedValue: undefined, - }; + const continuation = + (callingTransformForThisStep + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.exit = (providedValue) => { + continuationStorage.returnedWith = 'exit'; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + }; - const continuation = + if (baseComposes) { + const makeRaiseLike = returnWith => (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; continuationStorage.providedValue = providedValue; return continuationSymbol; } : (providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; return continuationSymbol; }); - continuation.exit = (providedValue) => { - continuationStorage.returnedWith = 'exit'; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - }; - - if (baseComposes) { - const makeRaiseLike = returnWith => - (callingTransformForThisStep - ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - } - : (providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - return continuationSymbol; - }); - - continuation.raise = makeRaiseLike('raise'); - continuation.raiseAbove = makeRaiseLike('raiseAbove'); - } - - return {continuation, continuationStorage}; + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); } - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { - const expectingTransform = initialValue !== noTransformSymbol; + return {continuation, continuationStorage}; + } - let valueSoFar = - (expectingTransform - ? initialValue - : undefined); + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); - const availableDependencies = {...initialDependencies}; + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; - if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); - } else { - debug(() => color.bright(`begin composition - not transforming`)); - } + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; + const availableDependencies = {...initialDependencies}; - debug(() => [ - `step #${i+1}` + - (isBase - ? ` (base):` - : ` of ${steps.length}:`), - step]); + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } - const expose = - (step.flags - ? step.expose - : step); + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; - const callingTransformForThisStep = - expectingTransform && expose.transform; + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); - const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); + const expose = + (step.flags + ? step.expose + : step); - debug(() => [ - `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + const callingTransformForThisStep = + expectingTransform && expose.transform; - const result = - (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); - if (result !== continuationSymbol) { - debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + debug(() => [ + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); - if (baseComposes) { - throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); - } + const result = + (callingTransformForThisStep + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.compute(filteredDependencies, continuation)); - debug(() => color.bright(`end composition - exit (inferred)`)); + if (result !== continuationSymbol) { + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - return result; + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - const {returnedWith} = continuationStorage; + debug(() => color.bright(`end composition - exit (inferred)`)); + + return result; + } - if (returnedWith === 'exit') { - const {providedValue} = continuationStorage; + const {returnedWith} = continuationStorage; - debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; - if (baseComposes) { - return continuationIfApplicable.exit(providedValue); - } else { - return providedValue; - } + debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); + debug(() => color.bright(`end composition - exit (explicit)`)); + + if (baseComposes) { + return continuationIfApplicable.exit(providedValue); + } else { + return providedValue; } + } - const {providedValue, providedDependencies} = continuationStorage; - - const continuingWithValue = - (expectingTransform - ? (callingTransformForThisStep - ? providedValue ?? null - : valueSoFar ?? null) - : undefined); - - const continuingWithDependencies = - (providedDependencies - ? _assignDependencies(providedDependencies, expose) - : null); - - const continuationArgs = []; - if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); - if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - - debug(() => { - const base = `step #${i+1} - result: ` + returnedWith; - const parts = []; - - if (callingTransformForThisStep) { - if (continuingWithValue === undefined) { - parts.push(`(no value)`); - } else { - parts.push(`value:`, providedValue); - } - } + const {providedValue, providedDependencies} = continuationStorage; - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); - } else { - parts.push(`(no deps)`); - } + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); + + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); - if (empty(parts)) { - return base; + const continuationArgs = []; + if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); + if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); + + debug(() => { + const base = `step #${i+1} - result: ` + returnedWith; + const parts = []; + + if (callingTransformForThisStep) { + if (continuingWithValue === undefined) { + parts.push(`(no value)`); } else { - return [base + ' ->', ...parts]; + parts.push(`value:`, providedValue); } - }); + } - switch (returnedWith) { - case 'raise': - debug(() => - (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); - return continuationIfApplicable(...continuationArgs); + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } - case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); - return continuationIfApplicable.raise(...continuationArgs); - - case 'continuation': - if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); - return continuationIfApplicable(...continuationArgs); - } else { - Object.assign(availableDependencies, continuingWithDependencies); - break; - } + if (empty(parts)) { + return base; + } else { + return [base + ' ->', ...parts]; } + }); + + switch (returnedWith) { + case 'raise': + debug(() => + (isBase + ? color.bright(`end composition - raise (base: explicit)`) + : color.bright(`end composition - raise`))); + return continuationIfApplicable(...continuationArgs); + + case 'raiseAbove': + debug(() => color.bright(`end composition - raiseAbove`)); + return continuationIfApplicable.raise(...continuationArgs); + + case 'continuation': + if (isBase) { + debug(() => color.bright(`end composition - raise (inferred)`)); + return continuationIfApplicable(...continuationArgs); + } else { + Object.assign(availableDependencies, continuingWithDependencies); + break; + } } } + } + + const constructedDescriptor = {}; + + if (annotation) { + constructedDescriptor.annotation = annotation; + } + + constructedDescriptor.flags = { + update: baseUpdates, + expose: baseExposes, + compose: baseComposes, + }; + + if (baseUpdates) { + constructedDescriptor.update = base.update; + } + + if (baseExposes) { + const expose = constructedDescriptor.expose = {}; + expose.dependencies = Array.from(exposeDependencies); const transformFn = (value, initialDependencies, continuationIfApplicable) => -- cgit 1.3.0-6-gf8a5 From 1594885c506ed76c0f4f1dc58ab14a4fabba6be5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 17:46:36 -0300 Subject: data: don't pass dependencies without expose properties --- src/data/things/composite.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 805331a9..21cf365f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -501,6 +501,10 @@ function compositeFrom(firstArg, secondArg) { mapDependencies, options, }) { + if (!dependencies && !mapDependencies && !options) { + return null; + } + const filteredDependencies = (dependencies ? filterProperties(availableDependencies, dependencies) @@ -629,8 +633,12 @@ function compositeFrom(firstArg, secondArg) { const result = (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); + ? (filteredDependencies + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.transform(valueSoFar, continuation)) + : (filteredDependencies + ? expose.compute(filteredDependencies, continuation) + : expose.compute(continuation))); if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); @@ -998,7 +1006,7 @@ export function exposeUpdateValueOrContinue({ }, { - transform: (value, {}, continuation) => + transform: (value, continuation) => continuation.exit(value), }, ]); -- cgit 1.3.0-6-gf8a5 From c6ba294c4fef425074f2352b640cc02c4768ee6e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 6 Sep 2023 17:49:56 -0300 Subject: data: unused import fixes --- src/data/things/thing.js | 2 +- src/data/things/track.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 36a1f58a..968dd102 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty, filterProperties, openAggregate} from '#sugar'; +import {empty} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { diff --git a/src/data/things/track.js b/src/data/things/track.js index 2b628b66..7d7e8a68 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -16,7 +16,6 @@ export class Track extends Thing { Flash, validators: { - isBoolean, isColor, isDate, isDuration, -- cgit 1.3.0-6-gf8a5 From ff6d14354612a9da430d523fa9dbc237cae3a6e2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 09:38:26 -0300 Subject: infra, data: allow exporting non-classes from things/ files --- src/data/things/index.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/data/things/index.js b/src/data/things/index.js index 591cdc3b..2d4f77d7 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -82,6 +82,8 @@ function errorDuplicateClassNames() { function flattenClassLists() { for (const classes of Object.values(allClassLists)) { for (const [name, constructor] of Object.entries(classes)) { + if (typeof constructor !== 'function') continue; + if (!(constructor.prototype instanceof Thing)) continue; allClasses[name] = constructor; } } -- cgit 1.3.0-6-gf8a5 From 78d293d5f4eea7ed6ee6f3cddd3ffcf73c5056a0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 09:40:55 -0300 Subject: data: directly import from #composite; define own utils at module --- src/data/things/composite.js | 32 +-- src/data/things/thing.js | 36 ++- src/data/things/track.js | 670 ++++++++++++++++++++++--------------------- 3 files changed, 379 insertions(+), 359 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 21cf365f..bcc52a2a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -808,7 +808,7 @@ function _export(mapping) { const mappingEntries = Object.entries(mapping); return { - annotation: `Thing.composite.export`, + annotation: `export`, flags: {expose: true, compose: true}, expose: { @@ -853,7 +853,7 @@ export function exposeDependency(dependency, { update = false, } = {}) { return { - annotation: `Thing.composite.exposeDependency`, + annotation: `exposeDependency`, flags: {expose: true, update: !!update}, expose: { @@ -878,7 +878,7 @@ export function exposeConstant(value, { update = false, } = {}) { return { - annotation: `Thing.composite.exposeConstant`, + annotation: `exposeConstant`, flags: {expose: true, update: !!update}, expose: { @@ -934,7 +934,7 @@ export function withResultOfAvailabilityCheck({ if (fromDependency) { return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, + annotation: `withResultOfAvailabilityCheck.fromDependency`, flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, @@ -946,7 +946,7 @@ export function withResultOfAvailabilityCheck({ }; } else { return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, + annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { mapContinuation: {to}, @@ -963,7 +963,7 @@ export function withResultOfAvailabilityCheck({ export function exposeDependencyOrContinue(dependency, { mode = 'null', } = {}) { - return compositeFrom(`Thing.composite.exposeDependencyOrContinue`, [ + return compositeFrom(`exposeDependencyOrContinue`, [ withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -991,7 +991,7 @@ export function exposeDependencyOrContinue(dependency, { export function exposeUpdateValueOrContinue({ mode = 'null', } = {}) { - return compositeFrom(`Thing.composite.exposeUpdateValueOrContinue`, [ + return compositeFrom(`exposeUpdateValueOrContinue`, [ withResultOfAvailabilityCheck({ fromUpdateValue: true, mode, @@ -1019,7 +1019,7 @@ export function earlyExitIfAvailabilityCheckFailed({ availability = '#availability', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ + return compositeFrom(`earlyExitIfAvailabilityCheckFailed`, [ { mapDependencies: {availability}, compute: ({availability}, continuation) => @@ -1042,7 +1042,7 @@ export function earlyExitWithoutDependency(dependency, { mode = 'null', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + return compositeFrom(`earlyExitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), earlyExitIfAvailabilityCheckFailed({value}), ]); @@ -1054,7 +1054,7 @@ export function earlyExitWithoutUpdateValue({ mode = 'null', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + return compositeFrom(`earlyExitWithoutDependency`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), earlyExitIfAvailabilityCheckFailed({value}), ]); @@ -1067,7 +1067,7 @@ export function raiseWithoutDependency(dependency, { map = {}, raise = {}, } = {}) { - return compositeFrom(`Thing.composite.raiseWithoutDependency`, [ + return compositeFrom(`raiseWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { @@ -1094,7 +1094,7 @@ export function raiseWithoutUpdateValue({ map = {}, raise = {}, } = {}) { - return compositeFrom(`Thing.composite.raiseWithoutUpdateValue`, [ + return compositeFrom(`raiseWithoutUpdateValue`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { @@ -1121,8 +1121,8 @@ export function raiseWithoutUpdateValue({ // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, to}) { - return Thing.composite.from(`Thing.composite.withResolvedContribs`, [ - Thing.composite.raiseWithoutDependency(from, { + return compositeFrom(`withResolvedContribs`, [ + raiseWithoutDependency(from, { mode: 'empty', map: {to}, raise: {to: []}, @@ -1172,7 +1172,7 @@ export function withResolvedReference({ to = '#resolvedReference', earlyExitIfNotFound = false, }) { - return compositeFrom(`Thing.composite.withResolvedReference`, [ + return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), earlyExitWithoutDependency(data), @@ -1210,7 +1210,7 @@ export function withResolvedReferenceList({ throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ + return compositeFrom(`withResolvedReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 968dd102..0716931a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -8,6 +8,15 @@ import find from '#find'; import {empty} from '#sugar'; import {getKebabCase} from '#wiki-data'; +import { + from as compositeFrom, + exposeDependency, + withReverseReferenceList, + withResolvedContribs, + withResolvedReference, + withResolvedReferenceList, +} from '#composite'; + import { isAdditionalFileList, isBoolean, @@ -27,7 +36,6 @@ import { } from '#validators'; import CacheableObject from './cacheable-object.js'; -import * as composite from './composite.js'; export default class Thing extends CacheableObject { static referenceType = Symbol('Thing.referenceType'); @@ -194,20 +202,20 @@ export default class Thing extends CacheableObject { // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. resolvedReferenceList({list, data, find}) { - return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ - Thing.composite.withResolvedReferenceList({ + return compositeFrom(`Thing.common.resolvedReferenceList`, [ + withResolvedReferenceList({ list, data, find, notFoundMode: 'filter', }), - Thing.composite.exposeDependency('#resolvedReferenceList'), + exposeDependency('#resolvedReferenceList'), ]); }, // Corresponding function for a single reference. resolvedReference({ref, data, find}) { - return Thing.composite.from(`Thing.common.resolvedReference`, [ - Thing.composite.withResolvedReference({ref, data, find}), - Thing.composite.exposeDependency('#resolvedReference'), + return compositeFrom(`Thing.common.resolvedReference`, [ + withResolvedReference({ref, data, find}), + exposeDependency('#resolvedReference'), ]); }, @@ -227,13 +235,13 @@ export default class Thing extends CacheableObject { // reference list is somehow messed up, or artistData isn't being provided // properly.) dynamicContribs(contribsByRefProperty) { - return Thing.composite.from(`Thing.common.dynamicContribs`, [ - Thing.composite.withResolvedContribs({ + return compositeFrom(`Thing.common.dynamicContribs`, [ + withResolvedContribs({ from: contribsByRefProperty, to: '#contribs', }), - Thing.composite.exposeDependency('#contribs'), + exposeDependency('#contribs'), ]); }, @@ -257,9 +265,9 @@ export default class Thing extends CacheableObject { // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. reverseReferenceList({data, list}) { - return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.withReverseReferenceList({data, list}), - Thing.composite.exposeDependency('#reverseReferenceList'), + return compositeFrom(`Thing.common.reverseReferenceList`, [ + withReverseReferenceList({data, list}), + exposeDependency('#reverseReferenceList'), ]); }, @@ -323,6 +331,4 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } - - static composite = composite; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 7d7e8a68..c5e6ff34 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -4,6 +4,19 @@ import {color} from '#cli'; import find from '#find'; import {empty} from '#sugar'; +import { + from as compositeFrom, + earlyExitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, + withResolvedContribs, + withResolvedReference, + withResultOfAvailabilityCheck, + withReverseReferenceList, +} from '#composite'; + import Thing from './thing.js'; export class Track extends Thing { @@ -43,9 +56,9 @@ export class Track extends Thing { sampledTracksByRef: Thing.common.referenceList(Track), artTagsByRef: Thing.common.referenceList(ArtTag), - color: Thing.composite.from(`Track.color`, [ - Thing.composite.exposeUpdateValueOrContinue(), - Track.composite.withContainingTrackSection({earlyExitIfNotFound: false}), + color: compositeFrom(`Track.color`, [ + exposeUpdateValueOrContinue(), + withContainingTrackSection({earlyExitIfNotFound: false}), { dependencies: ['#trackSection'], @@ -59,8 +72,8 @@ export class Track extends Thing { : continuation()), }, - Track.composite.withAlbumProperty('color'), - Thing.composite.exposeDependency('#album.color', { + withAlbumProperty('color'), + exposeDependency('#album.color', { update: {validate: isColor}, }), ]), @@ -75,21 +88,21 @@ export class Track extends Thing { // 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: Thing.composite.from(`Track.coverArtFileExtension`, [ + coverArtFileExtension: compositeFrom(`Track.coverArtFileExtension`, [ // No cover art file extension if the track doesn't have unique artwork // in the first place. - Track.composite.withHasUniqueCoverArt(), - Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + withHasUniqueCoverArt(), + earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), // Expose custom coverArtFileExtension update value first. - Thing.composite.exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue(), // Expose album's trackCoverArtFileExtension if no update value set. - Track.composite.withAlbumProperty('trackCoverArtFileExtension'), - Thing.composite.exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), + withAlbumProperty('trackCoverArtFileExtension'), + exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), // Fallback to 'jpg'. - Thing.composite.exposeConstant('jpg', { + exposeConstant('jpg', { update: {validate: isFileExtension}, }), ]), @@ -98,14 +111,14 @@ export class Track extends Thing { // only the track's own unique cover artwork, if any. This exposes only as // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. - coverArtDate: Thing.composite.from(`Track.coverArtDate`, [ - Track.composite.withHasUniqueCoverArt(), - Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + coverArtDate: compositeFrom(`Track.coverArtDate`, [ + withHasUniqueCoverArt(), + earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), - Thing.composite.exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue(), - Track.composite.withAlbumProperty('trackArtDate'), - Thing.composite.exposeDependency('#album.trackArtDate', { + withAlbumProperty('trackArtDate'), + exposeDependency('#album.trackArtDate', { update: {validate: isDate}, }), ]), @@ -132,9 +145,9 @@ export class Track extends Thing { commentatorArtists: Thing.common.commentatorArtists(), - album: Thing.composite.from(`Track.album`, [ - Track.composite.withAlbum(), - Thing.composite.exposeDependency('#album'), + album: compositeFrom(`Track.album`, [ + withAlbum(), + exposeDependency('#album'), ]), // Note - this is an internal property used only to help identify a track. @@ -150,10 +163,10 @@ export class Track extends Thing { find: find.album, }), - date: Thing.composite.from(`Track.date`, [ - Thing.composite.exposeDependencyOrContinue('dateFirstReleased'), - Track.composite.withAlbumProperty('date'), - Thing.composite.exposeDependency('#album.date'), + date: compositeFrom(`Track.date`, [ + exposeDependencyOrContinue('dateFirstReleased'), + withAlbumProperty('date'), + exposeDependency('#album.date'), ]), // Whether or not the track has "unique" cover artwork - a cover which is @@ -163,19 +176,19 @@ export class Track extends Thing { // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) - hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [ - Track.composite.withHasUniqueCoverArt(), - Thing.composite.exposeDependency('#hasUniqueCoverArt'), + hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ + withHasUniqueCoverArt(), + exposeDependency('#hasUniqueCoverArt'), ]), - originalReleaseTrack: Thing.composite.from(`Track.originalReleaseTrack`, [ - Track.composite.withOriginalRelease(), - Thing.composite.exposeDependency('#originalRelease'), + originalReleaseTrack: compositeFrom(`Track.originalReleaseTrack`, [ + withOriginalRelease(), + exposeDependency('#originalRelease'), ]), - otherReleases: Thing.composite.from(`Track.otherReleases`, [ - Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}), - Track.composite.withOriginalRelease({selfIfOriginal: true}), + otherReleases: compositeFrom(`Track.otherReleases`, [ + earlyExitWithoutDependency('trackData', {mode: 'empty'}), + withOriginalRelease({selfIfOriginal: true}), { flags: {expose: true}, @@ -197,10 +210,10 @@ export class Track extends Thing { }, ]), - artistContribs: Thing.composite.from(`Track.artistContribs`, [ - Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), + artistContribs: compositeFrom(`Track.artistContribs`, [ + inheritFromOriginalRelease({property: 'artistContribs'}), - Thing.composite.withResolvedContribs({ + withResolvedContribs({ from: 'artistContribsByRef', to: '#artistContribs', }), @@ -213,19 +226,19 @@ export class Track extends Thing { : contribsFromTrack), }, - Track.composite.withAlbumProperty('artistContribs'), - Thing.composite.exposeDependency('#album.artistContribs'), + withAlbumProperty('artistContribs'), + exposeDependency('#album.artistContribs'), ]), - contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ - Track.composite.inheritFromOriginalRelease({property: 'contributorContribs'}), + contributorContribs: compositeFrom(`Track.contributorContribs`, [ + inheritFromOriginalRelease({property: 'contributorContribs'}), Thing.common.dynamicContribs('contributorContribsByRef'), ]), // Cover artists aren't inherited from the original release, since it // typically varies by release and isn't defined by the musical qualities // of the track. - coverArtistContribs: Thing.composite.from(`Track.coverArtistContribs`, [ + coverArtistContribs: compositeFrom(`Track.coverArtistContribs`, [ { dependencies: ['disableUniqueCoverArt'], compute: ({disableUniqueCoverArt}, continuation) => @@ -234,7 +247,7 @@ export class Track extends Thing { : continuation()), }, - Thing.composite.withResolvedContribs({ + withResolvedContribs({ from: 'coverArtistContribsByRef', to: '#coverArtistContribs', }), @@ -247,12 +260,12 @@ export class Track extends Thing { : contribsFromTrack), }, - Track.composite.withAlbumProperty('trackCoverArtistContribs'), - Thing.composite.exposeDependency('#album.trackCoverArtistContribs'), + withAlbumProperty('trackCoverArtistContribs'), + exposeDependency('#album.trackCoverArtistContribs'), ]), - referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ - Track.composite.inheritFromOriginalRelease({property: 'referencedTracks'}), + referencedTracks: compositeFrom(`Track.referencedTracks`, [ + inheritFromOriginalRelease({property: 'referencedTracks'}), Thing.common.resolvedReferenceList({ list: 'referencedTracksByRef', data: 'trackData', @@ -260,8 +273,8 @@ export class Track extends Thing { }), ]), - sampledTracks: Thing.composite.from(`Track.sampledTracks`, [ - Track.composite.inheritFromOriginalRelease({property: 'sampledTracks'}), + sampledTracks: compositeFrom(`Track.sampledTracks`, [ + inheritFromOriginalRelease({property: 'sampledTracks'}), Thing.common.resolvedReferenceList({ list: 'sampledTracksByRef', data: 'trackData', @@ -283,10 +296,10 @@ export class Track extends Thing { // counting the number of times a track has been referenced, for use in // the "Tracks - by Times Referenced" listing page (or other data // processing). - referencedByTracks: Track.composite.trackReverseReferenceList('referencedTracks'), + referencedByTracks: trackReverseReferenceList('referencedTracks'), // For the same reasoning, exclude re-releases from sampled tracks too. - sampledByTracks: Track.composite.trackReverseReferenceList('sampledTracks'), + sampledByTracks: trackReverseReferenceList('sampledTracks'), featuredInFlashes: Thing.common.reverseReferenceList({ data: 'flashData', @@ -294,309 +307,310 @@ export class Track extends Thing { }), }); - static composite = { - // Early exits with a value inherited from the original release, if - // this track is a rerelease, and otherwise continues with no further - // dependencies provided. If allowOverride is true, then the continuation - // will also be called if the original release exposed the requested - // property as null. - inheritFromOriginalRelease({ - property: originalProperty, - allowOverride = false, - }) { - return Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ - Track.composite.withOriginalRelease(), - - { - dependencies: ['#originalRelease'], - compute({'#originalRelease': originalRelease}, continuation) { - if (!originalRelease) return continuation.raise(); - - const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation.raise(); - - return continuation.exit(value); - }, - }, - ]); - }, + [inspect.custom](depth) { + const parts = []; - // Gets the track's album. Unless earlyExitIfNotFound is overridden false, - // this will early exit with null in two cases - albumData being missing, - // or not including an album whose .tracks array includes this track. - withAlbum({to = '#album', earlyExitIfNotFound = true} = {}) { - return Thing.composite.from(`Track.composite.withAlbum`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromDependency: 'albumData', - mode: 'empty', - to: '#albumDataAvailability', - }), + parts.push(Thing.prototype[inspect.custom].apply(this)); - { - dependencies: ['#albumDataAvailability'], - options: {earlyExitIfNotFound}, - mapContinuation: {to}, + if (this.originalReleaseTrackByRef) { + parts.unshift(`${color.yellow('[rerelease]')} `); + } - compute: ({ - '#albumDataAvailability': albumDataAvailability, - '#options': {earlyExitIfNotFound}, - }, continuation) => - (albumDataAvailability - ? continuation() - : (earlyExitIfNotFound - ? continuation.exit(null) - : continuation.raise({to: null}))), - }, + let album; + if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) { + const albumName = album.name; + const albumIndex = album.tracks.indexOf(this); + const trackNum = + (albumIndex === -1 + ? '#?' + : `#${albumIndex + 1}`); + parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); + } - { - dependencies: ['this', 'albumData'], - compute: ({this: track, albumData}, continuation) => - continuation({ - '#album': - albumData.find(album => album.tracks.includes(track)), - }), - }, + return parts.join(''); + } +} - { - dependencies: ['#album'], - options: {earlyExitIfNotFound}, - mapContinuation: {to}, - compute: ({ - '#album': album, - '#options': {earlyExitIfNotFound}, - }, continuation) => - (album - ? continuation.raise({to: album}) - : (earlyExitIfNotFound - ? continuation.exit(null) - : continuation.raise({to: album}))), - }, - ]); +// Early exits with a value inherited from the original release, if +// this track is a rerelease, and otherwise continues with no further +// dependencies provided. If allowOverride is true, then the continuation +// will also be called if the original release exposed the requested +// property as null. +function inheritFromOriginalRelease({ + property: originalProperty, + allowOverride = false, +}) { + return compositeFrom(`inheritFromOriginalRelease`, [ + withOriginalRelease(), + + { + dependencies: ['#originalRelease'], + compute({'#originalRelease': originalRelease}, continuation) { + if (!originalRelease) return continuation.raise(); + + const value = originalRelease[originalProperty]; + if (allowOverride && value === null) return continuation.raise(); + + return continuation.exit(value); + }, }, + ]); +} - // Gets a single property from this track's album, providing it as the same - // property name prefixed with '#album.' (by default). If the track's album - // isn't available, and earlyExitIfNotFound hasn't been set, the property - // will be provided as null. - withAlbumProperty(property, { - to = '#album.' + property, - earlyExitIfNotFound = false, - } = {}) { - return Thing.composite.from(`Track.composite.withAlbumProperty`, [ - Track.composite.withAlbum({earlyExitIfNotFound}), - - { - dependencies: ['#album'], - options: {property}, - mapContinuation: {to}, +// Gets the track's album. Unless earlyExitIfNotFound is overridden false, +// this will early exit with null in two cases - albumData being missing, +// or not including an album whose .tracks array includes this track. +function withAlbum({ + to = '#album', + earlyExitIfNotFound = true, +} = {}) { + return compositeFrom(`withAlbum`, [ + withResultOfAvailabilityCheck({ + fromDependency: 'albumData', + mode: 'empty', + to: '#albumDataAvailability', + }), - compute: ({ - '#album': album, - '#options': {property}, - }, continuation) => - (album - ? continuation.raise({to: album[property]}) - : continuation.raise({to: null})), - }, - ]); + { + dependencies: ['#albumDataAvailability'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + + compute: ({ + '#albumDataAvailability': albumDataAvailability, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (albumDataAvailability + ? continuation() + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: null}))), }, - // Gets the listed properties from this track's album, providing them as - // dependencies (by default) with '#album.' prefixed before each property - // name. If the track's album isn't available, and earlyExitIfNotFound - // hasn't been set, the same dependency names will be provided as null. - withAlbumProperties({ - properties, - prefix = '#album', - earlyExitIfNotFound = false, - }) { - return Thing.composite.from(`Track.composite.withAlbumProperties`, [ - Track.composite.withAlbum({earlyExitIfNotFound}), - - { - dependencies: ['#album'], - options: {properties, prefix}, - - compute({ - '#album': album, - '#options': {properties, prefix}, - }, continuation) { - const raise = {}; - - if (album) { - for (const property of properties) { - raise[prefix + '.' + property] = album[property]; - } - } else { - for (const property of properties) { - raise[prefix + '.' + property] = null; - } - } - - return continuation.raise(raise); - }, - }, - ]); + { + dependencies: ['this', 'albumData'], + compute: ({this: track, albumData}, continuation) => + continuation({ + '#album': + albumData.find(album => album.tracks.includes(track)), + }), }, - // Gets the track section containing this track from its album's track list. - // Unless earlyExitIfNotFound is overridden false, this will early exit if - // the album can't be found or if none of its trackSections includes the - // track for some reason. - withContainingTrackSection({ - to = '#trackSection', - earlyExitIfNotFound = true, - } = {}) { - return Thing.composite.from(`Track.composite.withContainingTrackSection`, [ - Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}), - - { - dependencies: ['this', '#album.trackSections'], - mapContinuation: {to}, - - compute({ - this: track, - '#album.trackSections': trackSections, - }, continuation) { - if (!trackSections) { - return continuation.raise({to: null}); - } - - const trackSection = - trackSections.find(({tracks}) => tracks.includes(track)); - - if (trackSection) { - return continuation.raise({to: trackSection}); - } else if (earlyExitIfNotFound) { - return continuation.exit(null); - } else { - return continuation.raise({to: null}); - } - }, - }, - ]); + { + dependencies: ['#album'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + compute: ({ + '#album': album, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (album + ? continuation.raise({to: album}) + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: album}))), }, + ]); +} - // Just includes the original release of this track as a dependency. - // If this track isn't a rerelease, then it'll provide null, unless the - // {selfIfOriginal} option is set, in which case it'll provide this track - // itself. Note that this will early exit if the original release is - // specified by reference and that reference doesn't resolve to anything. - // Outputs to '#originalRelease' by default. - withOriginalRelease({ - to = '#originalRelease', - selfIfOriginal = false, - } = {}) { - return Thing.composite.from(`Track.composite.withOriginalRelease`, [ - Thing.composite.withResolvedReference({ - ref: 'originalReleaseTrackByRef', - data: 'trackData', - to: '#originalRelease', - find: find.track, - earlyExitIfNotFound: true, - }), - - { - dependencies: ['this', '#originalRelease'], - options: {selfIfOriginal}, - mapContinuation: {to}, - compute: ({ - this: track, - '#originalRelease': originalRelease, - '#options': {selfIfOriginal}, - }, continuation) => - continuation.raise({ - to: - (originalRelease ?? - (selfIfOriginal - ? track - : null)), - }), - }, - ]); +// Gets a single property from this track's album, providing it as the same +// property name prefixed with '#album.' (by default). If the track's album +// isn't available, and earlyExitIfNotFound hasn't been set, the property +// will be provided as null. +function withAlbumProperty(property, { + to = '#album.' + property, + earlyExitIfNotFound = false, +} = {}) { + return compositeFrom(`withAlbumProperty`, [ + withAlbum({earlyExitIfNotFound}), + + { + dependencies: ['#album'], + options: {property}, + mapContinuation: {to}, + + compute: ({ + '#album': album, + '#options': {property}, + }, continuation) => + (album + ? continuation.raise({to: album[property]}) + : continuation.raise({to: null})), }, + ]); +} - // The algorithm for checking if a track has unique cover art is used in a - // couple places, so it's defined in full as a compositional step. - withHasUniqueCoverArt({ - to = '#hasUniqueCoverArt', - } = {}) { - return Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ - { - dependencies: ['disableUniqueCoverArt'], - mapContinuation: {to}, - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? continuation.raise({to: false}) - : continuation()), - }, +// Gets the listed properties from this track's album, providing them as +// dependencies (by default) with '#album.' prefixed before each property +// name. If the track's album isn't available, and earlyExitIfNotFound +// hasn't been set, the same dependency names will be provided as null. +function withAlbumProperties({ + properties, + prefix = '#album', + earlyExitIfNotFound = false, +}) { + return compositeFrom(`withAlbumProperties`, [ + withAlbum({earlyExitIfNotFound}), + + { + dependencies: ['#album'], + options: {properties, prefix}, + + compute({ + '#album': album, + '#options': {properties, prefix}, + }, continuation) { + const raise = {}; + + if (album) { + for (const property of properties) { + raise[prefix + '.' + property] = album[property]; + } + } else { + for (const property of properties) { + raise[prefix + '.' + property] = null; + } + } + + return continuation.raise(raise); + }, + }, + ]); +} - Thing.composite.withResolvedContribs({ - from: 'coverArtistContribsByRef', - to: '#coverArtistContribs', - }), +// Gets the track section containing this track from its album's track list. +// Unless earlyExitIfNotFound is overridden false, this will early exit if +// the album can't be found or if none of its trackSections includes the +// track for some reason. +function withContainingTrackSection({ + to = '#trackSection', + earlyExitIfNotFound = true, +} = {}) { + return compositeFrom(`withContainingTrackSection`, [ + withAlbumProperty('trackSections', {earlyExitIfNotFound}), + + { + dependencies: ['this', '#album.trackSections'], + mapContinuation: {to}, + + compute({ + this: track, + '#album.trackSections': trackSections, + }, continuation) { + if (!trackSections) { + return continuation.raise({to: null}); + } + + const trackSection = + trackSections.find(({tracks}) => tracks.includes(track)); + + if (trackSection) { + return continuation.raise({to: trackSection}); + } else if (earlyExitIfNotFound) { + return continuation.exit(null); + } else { + return continuation.raise({to: null}); + } + }, + }, + ]); +} - { - dependencies: ['#coverArtistContribs'], - mapContinuation: {to}, - compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raise({to: true})), - }, +// Just includes the original release of this track as a dependency. +// If this track isn't a rerelease, then it'll provide null, unless the +// {selfIfOriginal} option is set, in which case it'll provide this track +// itself. Note that this will early exit if the original release is +// specified by reference and that reference doesn't resolve to anything. +// Outputs to '#originalRelease' by default. +function withOriginalRelease({ + to = '#originalRelease', + selfIfOriginal = false, +} = {}) { + return compositeFrom(`withOriginalRelease`, [ + withResolvedReference({ + ref: 'originalReleaseTrackByRef', + data: 'trackData', + to: '#originalRelease', + find: find.track, + earlyExitIfNotFound: true, + }), - Track.composite.withAlbumProperty('trackCoverArtistContribs'), + { + dependencies: ['this', '#originalRelease'], + options: {selfIfOriginal}, + mapContinuation: {to}, + compute: ({ + this: track, + '#originalRelease': originalRelease, + '#options': {selfIfOriginal}, + }, continuation) => + continuation.raise({ + to: + (originalRelease ?? + (selfIfOriginal + ? track + : null)), + }), + }, + ]); +} - { - dependencies: ['#album.trackCoverArtistContribs'], - mapContinuation: {to}, - compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => - (empty(contribsFromAlbum) - ? continuation.raise({to: false}) - : continuation.raise({to: true})), - }, - ]); +// The algorithm for checking if a track has unique cover art is used in a +// couple places, so it's defined in full as a compositional step. +function withHasUniqueCoverArt({ + to = '#hasUniqueCoverArt', +} = {}) { + return compositeFrom(`withHasUniqueCoverArt`, [ + { + dependencies: ['disableUniqueCoverArt'], + mapContinuation: {to}, + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? continuation.raise({to: false}) + : continuation()), }, - trackReverseReferenceList(refListProperty) { - return Thing.composite.from(`Track.composite.trackReverseReferenceList`, [ - Thing.composite.withReverseReferenceList({ - data: 'trackData', - list: refListProperty, - }), + withResolvedContribs({ + from: 'coverArtistContribsByRef', + to: '#coverArtistContribs', + }), - { - flags: {expose: true}, - expose: { - dependencies: ['#reverseReferenceList'], - compute: ({'#reverseReferenceList': reverseReferenceList}) => - reverseReferenceList.filter(track => !track.originalReleaseTrack), - }, - }, - ]); + { + dependencies: ['#coverArtistContribs'], + mapContinuation: {to}, + compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : continuation.raise({to: true})), }, - }; - - [inspect.custom](depth) { - const parts = []; - parts.push(Thing.prototype[inspect.custom].apply(this)); + withAlbumProperty('trackCoverArtistContribs'), - if (this.originalReleaseTrackByRef) { - parts.unshift(`${color.yellow('[rerelease]')} `); - } + { + dependencies: ['#album.trackCoverArtistContribs'], + mapContinuation: {to}, + compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => + (empty(contribsFromAlbum) + ? continuation.raise({to: false}) + : continuation.raise({to: true})), + }, + ]); +} - let album; - if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) { - const albumName = album.name; - const albumIndex = album.tracks.indexOf(this); - const trackNum = - (albumIndex === -1 - ? '#?' - : `#${albumIndex + 1}`); - parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); - } +function trackReverseReferenceList(refListProperty) { + return compositeFrom(`trackReverseReferenceList`, [ + withReverseReferenceList({ + data: 'trackData', + list: refListProperty, + }), - return parts.join(''); - } + { + flags: {expose: true}, + expose: { + dependencies: ['#reverseReferenceList'], + compute: ({'#reverseReferenceList': reverseReferenceList}) => + reverseReferenceList.filter(track => !track.originalReleaseTrack), + }, + }, + ]); } -- cgit 1.3.0-6-gf8a5 From c86de8a2be3867c14ca92c8e6799fd9b325305ec Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 09:55:29 -0300 Subject: data: move composite utilities related to wiki data into thing.js --- src/data/things/composite.js | 167 ------------------------------------------- src/data/things/thing.js | 166 ++++++++++++++++++++++++++++++++++++++++-- src/data/things/track.js | 9 +-- 3 files changed, 165 insertions(+), 177 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index bcc52a2a..7cba1e97 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,18 +1,13 @@ import {inspect} from 'node:util'; import {color} from '#cli'; -import find from '#find'; -import {filterMultipleArrays} from '#wiki-data'; import { empty, filterProperties, openAggregate, - stitchArrays, } from '#sugar'; -import Thing from './thing.js'; - // Composes multiple compositional "steps" and a "base" to form a property // descriptor out of modular building blocks. This is an extension to the // more general-purpose CacheableObject property descriptor syntax, and @@ -794,8 +789,6 @@ export function debug(fn) { return value; } -// -- Compositional steps for compositions to nest -- - // Provides dependencies exactly as they are (or null if not defined) to the // continuation. Although this can *technically* be used to alias existing // dependencies to some other name within the middle of a composition, it's @@ -834,8 +827,6 @@ function _export(mapping) { }; } -// -- Compositional steps for top-level property descriptors -- - // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. // Since this serves as a base, specify a value for {update} to indicate @@ -1113,161 +1104,3 @@ export function raiseWithoutUpdateValue({ }, ]); } - -// -- Compositional steps for processing data -- - -// Resolves the contribsByRef contained in the provided dependency, -// providing (named by the second argument) the result. "Resolving" -// means mapping the "who" reference of each contribution to an artist -// object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({from, to}) { - return compositeFrom(`withResolvedContribs`, [ - raiseWithoutDependency(from, { - mode: 'empty', - map: {to}, - raise: {to: []}, - }), - - { - mapDependencies: {from}, - compute: ({from}, continuation) => - continuation({ - '#whoByRef': from.map(({who}) => who), - '#what': from.map(({what}) => what), - }), - }, - - withResolvedReferenceList({ - list: '#whoByRef', - data: 'artistData', - to: '#who', - find: find.artist, - notFoundMode: 'null', - }), - - { - dependencies: ['#who', '#what'], - mapContinuation: {to}, - compute({'#who': who, '#what': what}, continuation) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - to: stitchArrays({who, what}), - }); - }, - }, - ]); -} - -// Resolves a reference by using the provided find function to match it -// within the provided thingData dependency. This will early exit if the -// data dependency is null, or, if earlyExitIfNotFound is set to true, -// if the find function doesn't match anything for the reference. -// Otherwise, the data object is provided on the output dependency; -// or null, if the reference doesn't match anything or itself was null -// to begin with. -export function withResolvedReference({ - ref, - data, - find: findFunction, - to = '#resolvedReference', - earlyExitIfNotFound = false, -}) { - return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), - earlyExitWithoutDependency(data), - - { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {match: to}, - - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && earlyExitIfNotFound) { - return continuation.exit(null); - } - - return continuation.raise({match}); - }, - }, - ]); -} - -// Resolves a list of references, with each reference matched with provided -// data in the same way as withResolvedReference. This will early exit if the -// data dependency is null (even if the reference list is empty). By default -// it will filter out references which don't match, but this can be changed -// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). -export function withResolvedReferenceList({ - list, - data, - find: findFunction, - to = '#resolvedReferenceList', - notFoundMode = 'filter', -}) { - if (!['filter', 'exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); - } - - return compositeFrom(`withResolvedReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), - - raiseWithoutDependency(list, { - map: {to}, - raise: {to: []}, - mode: 'empty', - }), - - { - options: {findFunction, notFoundMode}, - mapDependencies: {list, data}, - mapContinuation: {matches: to}, - - compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { - let matches = - list.map(ref => findFunction(ref, data, {mode: 'quiet'})); - - if (!matches.includes(null)) { - return continuation.raise({matches}); - } - - switch (notFoundMode) { - case 'filter': - matches = matches.filter(value => value !== null); - return continuation.raise({matches}); - - case 'exit': - return continuation.exit([]); - - case 'null': - return continuation.raise({matches}); - } - }, - }, - ]); -} - -// Check out the info on Thing.common.reverseReferenceList! -// This is its composable form. -export function withReverseReferenceList({ - data, - list: refListProperty, - to = '#reverseReferenceList', -}) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), - - { - dependencies: ['this'], - mapDependencies: {data}, - mapContinuation: {to}, - options: {refListProperty}, - - compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => - continuation({ - to: data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ]); -} diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0716931a..1077a652 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,16 +5,14 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty} from '#sugar'; -import {getKebabCase} from '#wiki-data'; +import {empty, stitchArrays} from '#sugar'; +import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { from as compositeFrom, + earlyExitWithoutDependency, exposeDependency, - withReverseReferenceList, - withResolvedContribs, - withResolvedReference, - withResolvedReferenceList, + raiseWithoutDependency, } from '#composite'; import { @@ -332,3 +330,159 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } } + +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. +export function withResolvedContribs({from, to}) { + return compositeFrom(`withResolvedContribs`, [ + raiseWithoutDependency(from, { + mode: 'empty', + map: {to}, + raise: {to: []}, + }), + + { + mapDependencies: {from}, + compute: ({from}, continuation) => + continuation({ + '#whoByRef': from.map(({who}) => who), + '#what': from.map(({what}) => what), + }), + }, + + withResolvedReferenceList({ + list: '#whoByRef', + data: 'artistData', + to: '#who', + find: find.artist, + notFoundMode: 'null', + }), + + { + dependencies: ['#who', '#what'], + mapContinuation: {to}, + compute({'#who': who, '#what': what}, continuation) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + to: stitchArrays({who, what}), + }); + }, + }, + ]); +} + +// Resolves a reference by using the provided find function to match it +// within the provided thingData dependency. This will early exit if the +// data dependency is null, or, if earlyExitIfNotFound is set to true, +// if the find function doesn't match anything for the reference. +// Otherwise, the data object is provided on the output dependency; +// or null, if the reference doesn't match anything or itself was null +// to begin with. +export function withResolvedReference({ + ref, + data, + find: findFunction, + to = '#resolvedReference', + earlyExitIfNotFound = false, +}) { + return compositeFrom(`withResolvedReference`, [ + raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + earlyExitWithoutDependency(data), + + { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, + + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } + + return continuation.raise({match}); + }, + }, + ]); +} + +// Resolves a list of references, with each reference matched with provided +// data in the same way as withResolvedReference. This will early exit if the +// data dependency is null (even if the reference list is empty). By default +// it will filter out references which don't match, but this can be changed +// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). +export function withResolvedReferenceList({ + list, + data, + find: findFunction, + to = '#resolvedReferenceList', + notFoundMode = 'filter', +}) { + if (!['filter', 'exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); + } + + return compositeFrom(`withResolvedReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + raiseWithoutDependency(list, { + map: {to}, + raise: {to: []}, + mode: 'empty', + }), + + { + options: {findFunction, notFoundMode}, + mapDependencies: {list, data}, + mapContinuation: {matches: to}, + + compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { + let matches = + list.map(ref => findFunction(ref, data, {mode: 'quiet'})); + + if (!matches.includes(null)) { + return continuation.raise({matches}); + } + + switch (notFoundMode) { + case 'filter': + matches = matches.filter(value => value !== null); + return continuation.raise({matches}); + + case 'exit': + return continuation.exit([]); + + case 'null': + return continuation.raise({matches}); + } + }, + }, + ]); +} + +// Check out the info on Thing.common.reverseReferenceList! +// This is its composable form. +export function withReverseReferenceList({ + data, + list: refListProperty, + to = '#reverseReferenceList', +}) { + return compositeFrom(`Thing.common.reverseReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + { + dependencies: ['this'], + mapDependencies: {data}, + mapContinuation: {to}, + options: {refListProperty}, + + compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => + continuation({ + to: data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ]); +} diff --git a/src/data/things/track.js b/src/data/things/track.js index c5e6ff34..0d7803bd 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -11,13 +11,14 @@ import { exposeDependency, exposeDependencyOrContinue, exposeUpdateValueOrContinue, - withResolvedContribs, - withResolvedReference, withResultOfAvailabilityCheck, - withReverseReferenceList, } from '#composite'; -import Thing from './thing.js'; +import Thing, { + withResolvedContribs, + withResolvedReference, + withReverseReferenceList, +} from './thing.js'; export class Track extends Thing { static [Thing.referenceType] = 'track'; -- cgit 1.3.0-6-gf8a5 From 3437936e6127192d30a308b68731cd4aa33555e7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 10:10:44 -0300 Subject: data: earlyExit -> exit in misc. utility names --- src/data/things/composite.js | 20 ++++++++++---------- src/data/things/thing.js | 9 ++++----- src/data/things/track.js | 8 ++++---- 3 files changed, 18 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7cba1e97..84a98290 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1004,13 +1004,13 @@ export function exposeUpdateValueOrContinue({ } // Early exits if an availability check has failed. -// This is for internal use only - use `earlyExitWithoutDependency` or -// `earlyExitWIthoutUpdateValue` instead. -export function earlyExitIfAvailabilityCheckFailed({ +// This is for internal use only - use `exitWithoutDependency` or +// `exitWithoutUpdateValue` instead. +export function exitIfAvailabilityCheckFailed({ availability = '#availability', value = null, } = {}) { - return compositeFrom(`earlyExitIfAvailabilityCheckFailed`, [ + return compositeFrom(`exitIfAvailabilityCheckFailed`, [ { mapDependencies: {availability}, compute: ({availability}, continuation) => @@ -1029,25 +1029,25 @@ export function earlyExitIfAvailabilityCheckFailed({ // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function earlyExitWithoutDependency(dependency, { +export function exitWithoutDependency(dependency, { mode = 'null', value = null, } = {}) { - return compositeFrom(`earlyExitWithoutDependency`, [ + return compositeFrom(`exitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - earlyExitIfAvailabilityCheckFailed({value}), + exitIfAvailabilityCheckFailed({value}), ]); } // Early exits if this property's update value isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function earlyExitWithoutUpdateValue({ +export function exitWithoutUpdateValue({ mode = 'null', value = null, } = {}) { - return compositeFrom(`earlyExitWithoutDependency`, [ + return compositeFrom(`exitWithoutUpdateValue`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - earlyExitIfAvailabilityCheckFailed({value}), + exitIfAvailabilityCheckFailed({value}), ]); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1077a652..5d407153 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -10,7 +10,7 @@ import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { from as compositeFrom, - earlyExitWithoutDependency, + exitWithoutDependency, exposeDependency, raiseWithoutDependency, } from '#composite'; @@ -389,7 +389,7 @@ export function withResolvedReference({ }) { return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), - earlyExitWithoutDependency(data), + exitWithoutDependency(data), { options: {findFunction, earlyExitIfNotFound}, @@ -426,8 +426,7 @@ export function withResolvedReferenceList({ } return compositeFrom(`withResolvedReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), - + exitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { map: {to}, raise: {to: []}, @@ -471,7 +470,7 @@ export function withReverseReferenceList({ to = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), + exitWithoutDependency(data, {value: []}), { dependencies: ['this'], diff --git a/src/data/things/track.js b/src/data/things/track.js index 0d7803bd..a7f96e42 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -6,7 +6,7 @@ import {empty} from '#sugar'; import { from as compositeFrom, - earlyExitWithoutDependency, + exitWithoutDependency, exposeConstant, exposeDependency, exposeDependencyOrContinue, @@ -93,7 +93,7 @@ export class Track extends Thing { // No cover art file extension if the track doesn't have unique artwork // in the first place. withHasUniqueCoverArt(), - earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + exitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), // Expose custom coverArtFileExtension update value first. exposeUpdateValueOrContinue(), @@ -114,7 +114,7 @@ export class Track extends Thing { // is specified, this value is null. coverArtDate: compositeFrom(`Track.coverArtDate`, [ withHasUniqueCoverArt(), - earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + exitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), exposeUpdateValueOrContinue(), @@ -188,7 +188,7 @@ export class Track extends Thing { ]), otherReleases: compositeFrom(`Track.otherReleases`, [ - earlyExitWithoutDependency('trackData', {mode: 'empty'}), + exitWithoutDependency('trackData', {mode: 'empty'}), withOriginalRelease({selfIfOriginal: true}), { -- cgit 1.3.0-6-gf8a5 From b076c87e435bbe2403122158ee03e4934c220c6c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 10:32:41 -0300 Subject: data: earlyExitIfNotFound -> notFoundMode --- src/data/things/thing.js | 21 +++++++++------- src/data/things/track.js | 65 +++++++++++++++++++++++++++--------------------- 2 files changed, 48 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5d407153..93f19799 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -375,31 +375,34 @@ export function withResolvedContribs({from, to}) { // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the -// data dependency is null, or, if earlyExitIfNotFound is set to true, -// if the find function doesn't match anything for the reference. -// Otherwise, the data object is provided on the output dependency; -// or null, if the reference doesn't match anything or itself was null -// to begin with. +// data dependency is null, or, if notFoundMode is set to 'exit', if the find +// function doesn't match anything for the reference. Otherwise, the data +// object is provided on the output dependency; or null, if the reference +// doesn't match anything or itself was null to begin with. export function withResolvedReference({ ref, data, find: findFunction, to = '#resolvedReference', - earlyExitIfNotFound = false, + notFoundMode = 'null', }) { + if (!['exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be exit or null`); + } + return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), exitWithoutDependency(data), { - options: {findFunction, earlyExitIfNotFound}, + options: {findFunction, notFoundMode}, mapDependencies: {ref, data}, mapContinuation: {match: to}, - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) { + if (match === null && notFoundMode === 'exit') { return continuation.exit(null); } diff --git a/src/data/things/track.js b/src/data/things/track.js index a7f96e42..9e1942e3 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -59,7 +59,7 @@ export class Track extends Thing { color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), - withContainingTrackSection({earlyExitIfNotFound: false}), + withContainingTrackSection(), { dependencies: ['#trackSection'], @@ -358,12 +358,13 @@ function inheritFromOriginalRelease({ ]); } -// Gets the track's album. Unless earlyExitIfNotFound is overridden false, -// this will early exit with null in two cases - albumData being missing, -// or not including an album whose .tracks array includes this track. +// Gets the track's album. This will early exit if albumData is missing. +// By default, if there's no album whose list of tracks includes this track, +// the output dependency will be null; set {notFoundMode: 'exit'} to early +// exit instead. function withAlbum({ to = '#album', - earlyExitIfNotFound = true, + notFoundMode = 'null', } = {}) { return compositeFrom(`withAlbum`, [ withResultOfAvailabilityCheck({ @@ -374,16 +375,16 @@ function withAlbum({ { dependencies: ['#albumDataAvailability'], - options: {earlyExitIfNotFound}, + options: {notFoundMode}, mapContinuation: {to}, compute: ({ '#albumDataAvailability': albumDataAvailability, - '#options': {earlyExitIfNotFound}, + '#options': {notFoundMode}, }, continuation) => (albumDataAvailability ? continuation() - : (earlyExitIfNotFound + : (notFoundMode === 'exit' ? continuation.exit(null) : continuation.raise({to: null}))), }, @@ -392,38 +393,38 @@ function withAlbum({ dependencies: ['this', 'albumData'], compute: ({this: track, albumData}, continuation) => continuation({ - '#album': - albumData.find(album => album.tracks.includes(track)), + '#album': albumData.find(album => album.tracks.includes(track)), }), }, { dependencies: ['#album'], - options: {earlyExitIfNotFound}, + options: {notFoundMode}, mapContinuation: {to}, + compute: ({ '#album': album, - '#options': {earlyExitIfNotFound}, + '#options': {notFoundMode}, }, continuation) => (album ? continuation.raise({to: album}) - : (earlyExitIfNotFound + : (notFoundMode === 'exit' ? continuation.exit(null) - : continuation.raise({to: album}))), + : continuation.raise({to: null}))), }, ]); } // Gets a single property from this track's album, providing it as the same // property name prefixed with '#album.' (by default). If the track's album -// isn't available, and earlyExitIfNotFound hasn't been set, the property -// will be provided as null. +// isn't available, then by default, the property will be provided as null; +// set {notFoundMode: 'exit'} to early exit instead. function withAlbumProperty(property, { to = '#album.' + property, - earlyExitIfNotFound = false, + notFoundMode = 'null', } = {}) { return compositeFrom(`withAlbumProperty`, [ - withAlbum({earlyExitIfNotFound}), + withAlbum({notFoundMode}), { dependencies: ['#album'], @@ -443,15 +444,16 @@ function withAlbumProperty(property, { // Gets the listed properties from this track's album, providing them as // dependencies (by default) with '#album.' prefixed before each property -// name. If the track's album isn't available, and earlyExitIfNotFound -// hasn't been set, the same dependency names will be provided as null. +// name. If the track's album isn't available, then by default, the same +// dependency names will be provided as null; set {notFoundMode: 'exit'} +// to early exit instead. function withAlbumProperties({ properties, prefix = '#album', - earlyExitIfNotFound = false, + notFoundMode = 'null', }) { return compositeFrom(`withAlbumProperties`, [ - withAlbum({earlyExitIfNotFound}), + withAlbum({notFoundMode}), { dependencies: ['#album'], @@ -480,23 +482,28 @@ function withAlbumProperties({ } // Gets the track section containing this track from its album's track list. -// Unless earlyExitIfNotFound is overridden false, this will early exit if -// the album can't be found or if none of its trackSections includes the -// track for some reason. +// If notFoundMode is set to 'exit', this will early exit if the album can't be +// found or if none of its trackSections includes the track for some reason. function withContainingTrackSection({ to = '#trackSection', - earlyExitIfNotFound = true, + notFoundMode = 'null', } = {}) { + if (!['exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be exit or null`); + } + return compositeFrom(`withContainingTrackSection`, [ - withAlbumProperty('trackSections', {earlyExitIfNotFound}), + withAlbumProperty('trackSections', {notFoundMode}), { dependencies: ['this', '#album.trackSections'], + options: {notFoundMode}, mapContinuation: {to}, compute({ this: track, '#album.trackSections': trackSections, + '#options': {notFoundMode}, }, continuation) { if (!trackSections) { return continuation.raise({to: null}); @@ -507,7 +514,7 @@ function withContainingTrackSection({ if (trackSection) { return continuation.raise({to: trackSection}); - } else if (earlyExitIfNotFound) { + } else if (notFoundMode === 'exit') { return continuation.exit(null); } else { return continuation.raise({to: null}); @@ -533,7 +540,7 @@ function withOriginalRelease({ data: 'trackData', to: '#originalRelease', find: find.track, - earlyExitIfNotFound: true, + notFoundMode: 'exit', }), { -- cgit 1.3.0-6-gf8a5 From fcdc788a3b9efe308518ccdce89f8db0dd5618f6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 10:36:32 -0300 Subject: data: composite docs update --- src/data/things/composite.js | 31 +++++++++++++++---------------- src/data/things/thing.js | 2 +- src/data/things/track.js | 2 +- 3 files changed, 17 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 84a98290..b9cd6bfb 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -104,7 +104,7 @@ import { // on a provided dependency name, and then providing a result in another // also-provided dependency name: // -// static Thing.composite.withResolvedContribs = ({ +// withResolvedContribs = ({ // from: contribsByRefDependency, // to: outputDependency, // }) => ({ @@ -126,10 +126,11 @@ import { // // And how you might work that into a composition: // -// static Track[Thing.getPropertyDescriptors].coverArtists = -// Thing.composite.from([ -// Track.composite.doSomethingWhichMightEarlyExit(), -// Thing.composite.withResolvedContribs({ +// Track.coverArtists = +// compositeFrom([ +// doSomethingWhichMightEarlyExit(), +// +// withResolvedContribs({ // from: 'coverArtistContribsByRef', // to: '#coverArtistContribs', // }), @@ -138,9 +139,8 @@ import { // flags: {expose: true}, // expose: { // dependencies: ['#coverArtistContribs'], -// compute({'#coverArtistContribs': coverArtistContribs}) { -// return coverArtistContribs.map(({who}) => who); -// }, +// compute: ({'#coverArtistContribs': coverArtistContribs}) => +// coverArtistContribs.map(({who}) => who), // }, // }, // ]); @@ -169,7 +169,7 @@ import { // Consider the `withResolvedContribs` example adjusted to make use of // two of these options below: // -// static Thing.composite.withResolvedContribs = ({ +// withResolvedContribs = ({ // from: contribsByRefDependency, // to: outputDependency, // }) => ({ @@ -194,7 +194,7 @@ import { // With a little destructuring and restructuring JavaScript sugar, the // above can be simplified some more: // -// static Thing.composite.withResolvedContribs = ({from, to}) => ({ +// withResolvedContribs = ({from, to}) => ({ // flags: {expose: true, compose: true}, // expose: { // dependencies: ['artistData'], @@ -281,7 +281,7 @@ import { // // In order to allow for this while helping to ensure internal dependencies // remain neatly isolated from the composition which nests your bundle, -// the Thing.composite.from() function will accept and adapt to a base that +// the compositeFrom() function will accept and adapt to a base that // specifies the {compose: true} flag, just like the steps preceding it. // // The continuation function that gets provided to the base will be mildly @@ -341,8 +341,7 @@ import { // syntax as for other compositional steps, and it'll work out cleanly! // -export {compositeFrom as from}; -function compositeFrom(firstArg, secondArg) { +export function compositeFrom(firstArg, secondArg) { const debug = fn => { if (compositeFrom.debug === true) { const label = @@ -373,7 +372,7 @@ function compositeFrom(firstArg, secondArg) { const aggregate = openAggregate({ message: - `Errors preparing Thing.composite.from() composition` + + `Errors preparing composition` + (annotation ? ` (${annotation})` : ''), }); @@ -780,9 +779,9 @@ function compositeFrom(firstArg, secondArg) { // t.same(thing.someProp, value) // // With debugging: -// t.same(Thing.composite.debug(() => thing.someProp), value) +// t.same(debugComposite(() => thing.someProp), value) // -export function debug(fn) { +export function debugComposite(fn) { compositeFrom.debug = true; const value = fn(); compositeFrom.debug = false; diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 93f19799..9b564ee9 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -9,7 +9,7 @@ import {empty, stitchArrays} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { - from as compositeFrom, + compositeFrom, exitWithoutDependency, exposeDependency, raiseWithoutDependency, diff --git a/src/data/things/track.js b/src/data/things/track.js index 9e1942e3..1818e003 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -5,7 +5,7 @@ import find from '#find'; import {empty} from '#sugar'; import { - from as compositeFrom, + compositeFrom, exitWithoutDependency, exposeConstant, exposeDependency, -- cgit 1.3.0-6-gf8a5 From ba04498715423c165cdb254676cc211c48b7c8ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 10:38:16 -0300 Subject: data: remove unused export() raising utility --- src/data/things/composite.js | 38 -------------------------------------- 1 file changed, 38 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b9cd6bfb..f59e7d75 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -788,44 +788,6 @@ export function debugComposite(fn) { return value; } -// Provides dependencies exactly as they are (or null if not defined) to the -// continuation. Although this can *technically* be used to alias existing -// dependencies to some other name within the middle of a composition, it's -// intended to be used only as a composition's base - doing so makes the -// composition as a whole suitable as a step in some other composition, -// providing the listed (internal) dependencies to later steps just like -// other compositional steps. -export {_export as export}; -function _export(mapping) { - const mappingEntries = Object.entries(mapping); - - return { - annotation: `export`, - flags: {expose: true, compose: true}, - - expose: { - options: {mappingEntries}, - dependencies: Object.values(mapping), - - compute({'#options': {mappingEntries}, ...dependencies}, continuation) { - const exports = {}; - - // Note: This is slightly different behavior from filterProperties, - // as defined in sugar.js, which doesn't fall back to null for - // properties which don't exist on the original object. - for (const [exportKey, dependencyKey] of mappingEntries) { - exports[exportKey] = - (Object.hasOwn(dependencies, dependencyKey) - ? dependencies[dependencyKey] - : null); - } - - return continuation.raise(exports); - } - }, - }; -} - // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. // Since this serves as a base, specify a value for {update} to indicate -- cgit 1.3.0-6-gf8a5 From 4541b2aa65a2f5ccfb7f9a13d5605311fd8ef801 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 11:37:58 -0300 Subject: data: composite "to" -> "into" --- src/data/things/composite.js | 28 ++++++++++---------- src/data/things/thing.js | 34 ++++++++++++------------ src/data/things/track.js | 62 ++++++++++++++++++++++---------------------- 3 files changed, 62 insertions(+), 62 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f59e7d75..976f7804 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -106,7 +106,7 @@ import { // // withResolvedContribs = ({ // from: contribsByRefDependency, -// to: outputDependency, +// into: outputDependency, // }) => ({ // flags: {expose: true, compose: true}, // expose: { @@ -132,7 +132,7 @@ import { // // withResolvedContribs({ // from: 'coverArtistContribsByRef', -// to: '#coverArtistContribs', +// into: '#coverArtistContribs', // }), // // { @@ -171,7 +171,7 @@ import { // // withResolvedContribs = ({ // from: contribsByRefDependency, -// to: outputDependency, +// into: outputDependency, // }) => ({ // flags: {expose: true, compose: true}, // expose: { @@ -199,11 +199,11 @@ import { // expose: { // dependencies: ['artistData'], // mapDependencies: {from}, -// mapContinuation: {to}, +// mapContinuation: {into}, // compute({artistData, from: contribsByRef}, continuation) { // if (!artistData) return null; // return continuation({ -// to: (..resolve contributions one way or another..), +// into: (..resolve contributions one way or another..), // }); // }, // }, @@ -505,8 +505,8 @@ export function compositeFrom(firstArg, secondArg) { : {}); if (mapDependencies) { - for (const [to, from] of Object.entries(mapDependencies)) { - filteredDependencies[to] = availableDependencies[from] ?? null; + for (const [into, from] of Object.entries(mapDependencies)) { + filteredDependencies[into] = availableDependencies[from] ?? null; } } @@ -524,8 +524,8 @@ export function compositeFrom(firstArg, secondArg) { const assignDependencies = {}; - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; + for (const [from, into] of Object.entries(mapContinuation)) { + assignDependencies[into] = continuationAssignment[from] ?? null; } return assignDependencies; @@ -861,7 +861,7 @@ export function withResultOfAvailabilityCheck({ fromUpdateValue, fromDependency, mode = 'null', - to = '#availability', + into = '#availability', }) { if (!['null', 'empty', 'falsy'].includes(mode)) { throw new TypeError(`Expected mode to be null, empty, or falsy`); @@ -890,10 +890,10 @@ export function withResultOfAvailabilityCheck({ flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, - mapContinuation: {to}, + mapContinuation: {into}, options: {mode}, compute: ({from, '#options': {mode}}, continuation) => - continuation({to: checkAvailability(from, mode)}), + continuation({into: checkAvailability(from, mode)}), }, }; } else { @@ -901,10 +901,10 @@ export function withResultOfAvailabilityCheck({ annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { - mapContinuation: {to}, + mapContinuation: {into}, options: {mode}, transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {to: checkAvailability(value, mode)}), + continuation(value, {into: checkAvailability(value, mode)}), }, }; } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9b564ee9..16003b00 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -236,7 +236,7 @@ export default class Thing extends CacheableObject { return compositeFrom(`Thing.common.dynamicContribs`, [ withResolvedContribs({ from: contribsByRefProperty, - to: '#contribs', + into: '#contribs', }), exposeDependency('#contribs'), @@ -335,12 +335,12 @@ export default class Thing extends CacheableObject { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({from, to}) { +export function withResolvedContribs({from, into}) { return compositeFrom(`withResolvedContribs`, [ raiseWithoutDependency(from, { mode: 'empty', - map: {to}, - raise: {to: []}, + map: {into}, + raise: {into: []}, }), { @@ -355,18 +355,18 @@ export function withResolvedContribs({from, to}) { withResolvedReferenceList({ list: '#whoByRef', data: 'artistData', - to: '#who', + into: '#who', find: find.artist, notFoundMode: 'null', }), { dependencies: ['#who', '#what'], - mapContinuation: {to}, + mapContinuation: {into}, compute({'#who': who, '#what': what}, continuation) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ - to: stitchArrays({who, what}), + into: stitchArrays({who, what}), }); }, }, @@ -383,7 +383,7 @@ export function withResolvedReference({ ref, data, find: findFunction, - to = '#resolvedReference', + into = '#resolvedReference', notFoundMode = 'null', }) { if (!['exit', 'null'].includes(notFoundMode)) { @@ -391,13 +391,13 @@ export function withResolvedReference({ } return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + raiseWithoutDependency(ref, {map: {into}, raise: {into: null}}), exitWithoutDependency(data), { options: {findFunction, notFoundMode}, mapDependencies: {ref, data}, - mapContinuation: {match: to}, + mapContinuation: {match: into}, compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { const match = findFunction(ref, data, {mode: 'quiet'}); @@ -421,7 +421,7 @@ export function withResolvedReferenceList({ list, data, find: findFunction, - to = '#resolvedReferenceList', + into = '#resolvedReferenceList', notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { @@ -431,15 +431,15 @@ export function withResolvedReferenceList({ return compositeFrom(`withResolvedReferenceList`, [ exitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { - map: {to}, - raise: {to: []}, + map: {into}, + raise: {into: []}, mode: 'empty', }), { options: {findFunction, notFoundMode}, mapDependencies: {list, data}, - mapContinuation: {matches: to}, + mapContinuation: {matches: into}, compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { let matches = @@ -470,7 +470,7 @@ export function withResolvedReferenceList({ export function withReverseReferenceList({ data, list: refListProperty, - to = '#reverseReferenceList', + into = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ exitWithoutDependency(data, {value: []}), @@ -478,12 +478,12 @@ export function withReverseReferenceList({ { dependencies: ['this'], mapDependencies: {data}, - mapContinuation: {to}, + mapContinuation: {into}, options: {refListProperty}, compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => continuation({ - to: data.filter(thing => thing[refListProperty].includes(thisThing)), + into: data.filter(thing => thing[refListProperty].includes(thisThing)), }), }, ]); diff --git a/src/data/things/track.js b/src/data/things/track.js index 1818e003..1adfe71a 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -216,7 +216,7 @@ export class Track extends Thing { withResolvedContribs({ from: 'artistContribsByRef', - to: '#artistContribs', + into: '#artistContribs', }), { @@ -250,7 +250,7 @@ export class Track extends Thing { withResolvedContribs({ from: 'coverArtistContribsByRef', - to: '#coverArtistContribs', + into: '#coverArtistContribs', }), { @@ -363,20 +363,20 @@ function inheritFromOriginalRelease({ // the output dependency will be null; set {notFoundMode: 'exit'} to early // exit instead. function withAlbum({ - to = '#album', + into = '#album', notFoundMode = 'null', } = {}) { return compositeFrom(`withAlbum`, [ withResultOfAvailabilityCheck({ fromDependency: 'albumData', mode: 'empty', - to: '#albumDataAvailability', + into: '#albumDataAvailability', }), { dependencies: ['#albumDataAvailability'], options: {notFoundMode}, - mapContinuation: {to}, + mapContinuation: {into}, compute: ({ '#albumDataAvailability': albumDataAvailability, @@ -386,7 +386,7 @@ function withAlbum({ ? continuation() : (notFoundMode === 'exit' ? continuation.exit(null) - : continuation.raise({to: null}))), + : continuation.raise({into: null}))), }, { @@ -400,17 +400,17 @@ function withAlbum({ { dependencies: ['#album'], options: {notFoundMode}, - mapContinuation: {to}, + mapContinuation: {into}, compute: ({ '#album': album, '#options': {notFoundMode}, }, continuation) => (album - ? continuation.raise({to: album}) + ? continuation.raise({into: album}) : (notFoundMode === 'exit' ? continuation.exit(null) - : continuation.raise({to: null}))), + : continuation.raise({into: null}))), }, ]); } @@ -420,7 +420,7 @@ function withAlbum({ // isn't available, then by default, the property will be provided as null; // set {notFoundMode: 'exit'} to early exit instead. function withAlbumProperty(property, { - to = '#album.' + property, + into = '#album.' + property, notFoundMode = 'null', } = {}) { return compositeFrom(`withAlbumProperty`, [ @@ -429,15 +429,15 @@ function withAlbumProperty(property, { { dependencies: ['#album'], options: {property}, - mapContinuation: {to}, + mapContinuation: {into}, compute: ({ '#album': album, '#options': {property}, }, continuation) => (album - ? continuation.raise({to: album[property]}) - : continuation.raise({to: null})), + ? continuation.raise({into: album[property]}) + : continuation.raise({into: null})), }, ]); } @@ -485,7 +485,7 @@ function withAlbumProperties({ // If notFoundMode is set to 'exit', this will early exit if the album can't be // found or if none of its trackSections includes the track for some reason. function withContainingTrackSection({ - to = '#trackSection', + into = '#trackSection', notFoundMode = 'null', } = {}) { if (!['exit', 'null'].includes(notFoundMode)) { @@ -498,7 +498,7 @@ function withContainingTrackSection({ { dependencies: ['this', '#album.trackSections'], options: {notFoundMode}, - mapContinuation: {to}, + mapContinuation: {into}, compute({ this: track, @@ -506,18 +506,18 @@ function withContainingTrackSection({ '#options': {notFoundMode}, }, continuation) { if (!trackSections) { - return continuation.raise({to: null}); + return continuation.raise({into: null}); } const trackSection = trackSections.find(({tracks}) => tracks.includes(track)); if (trackSection) { - return continuation.raise({to: trackSection}); + return continuation.raise({into: trackSection}); } else if (notFoundMode === 'exit') { return continuation.exit(null); } else { - return continuation.raise({to: null}); + return continuation.raise({into: null}); } }, }, @@ -531,14 +531,14 @@ function withContainingTrackSection({ // specified by reference and that reference doesn't resolve to anything. // Outputs to '#originalRelease' by default. function withOriginalRelease({ - to = '#originalRelease', + into = '#originalRelease', selfIfOriginal = false, } = {}) { return compositeFrom(`withOriginalRelease`, [ withResolvedReference({ ref: 'originalReleaseTrackByRef', data: 'trackData', - to: '#originalRelease', + into: '#originalRelease', find: find.track, notFoundMode: 'exit', }), @@ -546,14 +546,14 @@ function withOriginalRelease({ { dependencies: ['this', '#originalRelease'], options: {selfIfOriginal}, - mapContinuation: {to}, + mapContinuation: {into}, compute: ({ this: track, '#originalRelease': originalRelease, '#options': {selfIfOriginal}, }, continuation) => continuation.raise({ - to: + into: (originalRelease ?? (selfIfOriginal ? track @@ -566,41 +566,41 @@ function withOriginalRelease({ // The algorithm for checking if a track has unique cover art is used in a // couple places, so it's defined in full as a compositional step. function withHasUniqueCoverArt({ - to = '#hasUniqueCoverArt', + into = '#hasUniqueCoverArt', } = {}) { return compositeFrom(`withHasUniqueCoverArt`, [ { dependencies: ['disableUniqueCoverArt'], - mapContinuation: {to}, + mapContinuation: {into}, compute: ({disableUniqueCoverArt}, continuation) => (disableUniqueCoverArt - ? continuation.raise({to: false}) + ? continuation.raise({into: false}) : continuation()), }, withResolvedContribs({ from: 'coverArtistContribsByRef', - to: '#coverArtistContribs', + into: '#coverArtistContribs', }), { dependencies: ['#coverArtistContribs'], - mapContinuation: {to}, + mapContinuation: {into}, compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => (empty(contribsFromTrack) ? continuation() - : continuation.raise({to: true})), + : continuation.raise({into: true})), }, withAlbumProperty('trackCoverArtistContribs'), { dependencies: ['#album.trackCoverArtistContribs'], - mapContinuation: {to}, + mapContinuation: {into}, compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => (empty(contribsFromAlbum) - ? continuation.raise({to: false}) - : continuation.raise({to: true})), + ? continuation.raise({into: false}) + : continuation.raise({into: true})), }, ]); } -- cgit 1.3.0-6-gf8a5 From 8b379954c9d74f0d47ac32ef395627353940c728 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 11:56:05 -0300 Subject: data: use key/value-style for all compositional utility args --- src/data/things/composite.js | 25 ++++++++++------- src/data/things/thing.js | 39 ++++++++++++++++++-------- src/data/things/track.js | 65 ++++++++++++++++++++++++++------------------ 3 files changed, 81 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 976f7804..5b6de901 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -801,9 +801,10 @@ export function debugComposite(fn) { // compositional step, the property will be exposed as undefined instead // of null. // -export function exposeDependency(dependency, { +export function exposeDependency({ + dependency, update = false, -} = {}) { +}) { return { annotation: `exposeDependency`, flags: {expose: true, update: !!update}, @@ -826,9 +827,10 @@ export function exposeDependency(dependency, { // exit with some other value, with the exposeConstant base serving as the // fallback default value. Like exposeDependency, set {update} to true or // an object to indicate that the property as a whole updates. -export function exposeConstant(value, { +export function exposeConstant({ + value, update = false, -} = {}) { +}) { return { annotation: `exposeConstant`, flags: {expose: true, update: !!update}, @@ -912,9 +914,10 @@ export function withResultOfAvailabilityCheck({ // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! -export function exposeDependencyOrContinue(dependency, { +export function exposeDependencyOrContinue({ + dependency, mode = 'null', -} = {}) { +}) { return compositeFrom(`exposeDependencyOrContinue`, [ withResultOfAvailabilityCheck({ fromDependency: dependency, @@ -990,10 +993,11 @@ export function exitIfAvailabilityCheckFailed({ // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutDependency(dependency, { +export function exitWithoutDependency({ + dependency, mode = 'null', value = null, -} = {}) { +}) { return compositeFrom(`exitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), exitIfAvailabilityCheckFailed({value}), @@ -1014,11 +1018,12 @@ export function exitWithoutUpdateValue({ // Raises if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutDependency(dependency, { +export function raiseWithoutDependency({ + dependency, mode = 'null', map = {}, raise = {}, -} = {}) { +}) { return compositeFrom(`raiseWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 16003b00..98dec3c3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -205,7 +205,8 @@ export default class Thing extends CacheableObject { list, data, find, notFoundMode: 'filter', }), - exposeDependency('#resolvedReferenceList'), + + exposeDependency({dependency: '#resolvedReferenceList'}), ]); }, @@ -213,7 +214,7 @@ export default class Thing extends CacheableObject { resolvedReference({ref, data, find}) { return compositeFrom(`Thing.common.resolvedReference`, [ withResolvedReference({ref, data, find}), - exposeDependency('#resolvedReference'), + exposeDependency({dependency: '#resolvedReference'}), ]); }, @@ -239,7 +240,7 @@ export default class Thing extends CacheableObject { into: '#contribs', }), - exposeDependency('#contribs'), + exposeDependency({dependency: '#contribs'}), ]); }, @@ -265,7 +266,7 @@ export default class Thing extends CacheableObject { reverseReferenceList({data, list}) { return compositeFrom(`Thing.common.reverseReferenceList`, [ withReverseReferenceList({data, list}), - exposeDependency('#reverseReferenceList'), + exposeDependency({dependency: '#reverseReferenceList'}), ]); }, @@ -337,7 +338,8 @@ export default class Thing extends CacheableObject { // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, into}) { return compositeFrom(`withResolvedContribs`, [ - raiseWithoutDependency(from, { + raiseWithoutDependency({ + dependency: from, mode: 'empty', map: {into}, raise: {into: []}, @@ -391,8 +393,15 @@ export function withResolvedReference({ } return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency(ref, {map: {into}, raise: {into: null}}), - exitWithoutDependency(data), + raiseWithoutDependency({ + dependency: ref, + map: {into}, + raise: {into: null}, + }), + + exitWithoutDependency({ + dependency: data, + }), { options: {findFunction, notFoundMode}, @@ -429,11 +438,16 @@ export function withResolvedReferenceList({ } return compositeFrom(`withResolvedReferenceList`, [ - exitWithoutDependency(data, {value: []}), - raiseWithoutDependency(list, { + exitWithoutDependency({ + dependency: data, + value: [], + }), + + raiseWithoutDependency({ + dependency: list, + mode: 'empty', map: {into}, raise: {into: []}, - mode: 'empty', }), { @@ -473,7 +487,10 @@ export function withReverseReferenceList({ into = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ - exitWithoutDependency(data, {value: []}), + exitWithoutDependency({ + dependency: data, + value: [], + }), { dependencies: ['this'], diff --git a/src/data/things/track.js b/src/data/things/track.js index 1adfe71a..7dde88db 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -73,8 +73,10 @@ export class Track extends Thing { : continuation()), }, - withAlbumProperty('color'), - exposeDependency('#album.color', { + withAlbumProperty({property: 'color'}), + + exposeDependency({ + dependency: '#album.color', update: {validate: isColor}, }), ]), @@ -93,17 +95,18 @@ export class Track extends Thing { // No cover art file extension if the track doesn't have unique artwork // in the first place. withHasUniqueCoverArt(), - exitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), // Expose custom coverArtFileExtension update value first. exposeUpdateValueOrContinue(), // Expose album's trackCoverArtFileExtension if no update value set. - withAlbumProperty('trackCoverArtFileExtension'), - exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), + withAlbumProperty({property: 'trackCoverArtFileExtension'}), + exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), // Fallback to 'jpg'. - exposeConstant('jpg', { + exposeConstant({ + value: 'jpg', update: {validate: isFileExtension}, }), ]), @@ -114,12 +117,13 @@ export class Track extends Thing { // is specified, this value is null. coverArtDate: compositeFrom(`Track.coverArtDate`, [ withHasUniqueCoverArt(), - exitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), exposeUpdateValueOrContinue(), - withAlbumProperty('trackArtDate'), - exposeDependency('#album.trackArtDate', { + withAlbumProperty({property: 'trackArtDate'}), + exposeDependency({ + dependency: '#album.trackArtDate', update: {validate: isDate}, }), ]), @@ -148,7 +152,7 @@ export class Track extends Thing { album: compositeFrom(`Track.album`, [ withAlbum(), - exposeDependency('#album'), + exposeDependency({dependency: '#album'}), ]), // Note - this is an internal property used only to help identify a track. @@ -165,9 +169,9 @@ export class Track extends Thing { }), date: compositeFrom(`Track.date`, [ - exposeDependencyOrContinue('dateFirstReleased'), - withAlbumProperty('date'), - exposeDependency('#album.date'), + exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), + withAlbumProperty({property: 'date'}), + exposeDependency({dependency: '#album.date'}), ]), // Whether or not the track has "unique" cover artwork - a cover which is @@ -179,16 +183,16 @@ export class Track extends Thing { // album.) hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ withHasUniqueCoverArt(), - exposeDependency('#hasUniqueCoverArt'), + exposeDependency({dependency: '#hasUniqueCoverArt'}), ]), originalReleaseTrack: compositeFrom(`Track.originalReleaseTrack`, [ withOriginalRelease(), - exposeDependency('#originalRelease'), + exposeDependency({dependency: '#originalRelease'}), ]), otherReleases: compositeFrom(`Track.otherReleases`, [ - exitWithoutDependency('trackData', {mode: 'empty'}), + exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), withOriginalRelease({selfIfOriginal: true}), { @@ -227,8 +231,8 @@ export class Track extends Thing { : contribsFromTrack), }, - withAlbumProperty('artistContribs'), - exposeDependency('#album.artistContribs'), + withAlbumProperty({property: 'artistContribs'}), + exposeDependency({dependency: '#album.artistContribs'}), ]), contributorContribs: compositeFrom(`Track.contributorContribs`, [ @@ -261,8 +265,8 @@ export class Track extends Thing { : contribsFromTrack), }, - withAlbumProperty('trackCoverArtistContribs'), - exposeDependency('#album.trackCoverArtistContribs'), + withAlbumProperty({property: 'trackCoverArtistContribs'}), + exposeDependency({dependency: '#album.trackCoverArtistContribs'}), ]), referencedTracks: compositeFrom(`Track.referencedTracks`, [ @@ -297,10 +301,14 @@ export class Track extends Thing { // counting the number of times a track has been referenced, for use in // the "Tracks - by Times Referenced" listing page (or other data // processing). - referencedByTracks: trackReverseReferenceList('referencedTracks'), + referencedByTracks: trackReverseReferenceList({ + property: 'referencedTracks', + }), // For the same reasoning, exclude re-releases from sampled tracks too. - sampledByTracks: trackReverseReferenceList('sampledTracks'), + sampledByTracks: trackReverseReferenceList({ + property: 'sampledTracks', + }), featuredInFlashes: Thing.common.reverseReferenceList({ data: 'flashData', @@ -419,10 +427,11 @@ function withAlbum({ // property name prefixed with '#album.' (by default). If the track's album // isn't available, then by default, the property will be provided as null; // set {notFoundMode: 'exit'} to early exit instead. -function withAlbumProperty(property, { +function withAlbumProperty({ + property, into = '#album.' + property, notFoundMode = 'null', -} = {}) { +}) { return compositeFrom(`withAlbumProperty`, [ withAlbum({notFoundMode}), @@ -493,7 +502,7 @@ function withContainingTrackSection({ } return compositeFrom(`withContainingTrackSection`, [ - withAlbumProperty('trackSections', {notFoundMode}), + withAlbumProperty({property: 'trackSections', notFoundMode}), { dependencies: ['this', '#album.trackSections'], @@ -592,7 +601,7 @@ function withHasUniqueCoverArt({ : continuation.raise({into: true})), }, - withAlbumProperty('trackCoverArtistContribs'), + withAlbumProperty({property: 'trackCoverArtistContribs'}), { dependencies: ['#album.trackCoverArtistContribs'], @@ -605,7 +614,9 @@ function withHasUniqueCoverArt({ ]); } -function trackReverseReferenceList(refListProperty) { +function trackReverseReferenceList({ + property: refListProperty, +}) { return compositeFrom(`trackReverseReferenceList`, [ withReverseReferenceList({ data: 'trackData', -- cgit 1.3.0-6-gf8a5 From 6889c764caef5542ba9ad8362acf6e8b7b879ea9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 12:06:06 -0300 Subject: data, infra: import validators directly --- src/data/things/album.js | 16 +++------------- src/data/things/art-tag.js | 5 +---- src/data/things/artist.js | 12 ++---------- src/data/things/flash.js | 27 ++++++++++----------------- src/data/things/group.js | 8 ++------ src/data/things/homepage-layout.js | 36 ++++++++++++------------------------ src/data/things/index.js | 3 +-- src/data/things/language.js | 8 +++----- src/data/things/static-page.js | 8 +++----- src/data/things/track.js | 15 ++------------- src/data/things/wiki-info.js | 11 ++--------- 11 files changed, 41 insertions(+), 108 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 81f04f70..da018856 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,23 +1,13 @@ -import {empty} from '#sugar'; import find from '#find'; +import {empty} from '#sugar'; +import {isDate, isDimensions, isTrackSectionList} from '#validators'; import Thing from './thing.js'; export class Album extends Thing { static [Thing.referenceType] = 'album'; - static [Thing.getPropertyDescriptors] = ({ - ArtTag, - Artist, - Group, - Track, - - validators: { - isDate, - isDimensions, - isTrackSectionList, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({ArtTag, Artist, Group, Track}) => ({ // Update & expose name: Thing.common.name('Unnamed Album'), diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index bb36e09e..5d7d0cbf 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -5,10 +5,7 @@ import Thing from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; - static [Thing.getPropertyDescriptors] = ({ - Album, - Track, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ // Update & expose name: Thing.common.name('Unnamed Art Tag'), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index b2383057..93a1b51b 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -1,20 +1,12 @@ import find from '#find'; +import {isName, validateArrayItems} from '#validators'; import Thing from './thing.js'; export class Artist extends Thing { static [Thing.referenceType] = 'artist'; - static [Thing.getPropertyDescriptors] = ({ - Album, - Flash, - Track, - - validators: { - isName, - validateArrayItems, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({ // Update & expose name: Thing.common.name('Unnamed Artist'), diff --git a/src/data/things/flash.js b/src/data/things/flash.js index baef23d8..ce2e7fac 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -1,22 +1,19 @@ import find from '#find'; +import { + isColor, + isDirectory, + isNumber, + isString, + oneOf, +} from '#validators'; + import Thing from './thing.js'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; - static [Thing.getPropertyDescriptors] = ({ - Artist, - Track, - FlashAct, - - validators: { - isDirectory, - isNumber, - isString, - oneOf, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Artist, Track, FlashAct}) => ({ // Update & expose name: Thing.common.name('Unnamed Flash'), @@ -111,11 +108,7 @@ export class Flash extends Thing { } export class FlashAct extends Thing { - static [Thing.getPropertyDescriptors] = ({ - validators: { - isColor, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose name: Thing.common.name('Unnamed Flash Act'), diff --git a/src/data/things/group.js b/src/data/things/group.js index d04fcf56..6c712847 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -5,9 +5,7 @@ import Thing from './thing.js'; export class Group extends Thing { static [Thing.referenceType] = 'group'; - static [Thing.getPropertyDescriptors] = ({ - Album, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album}) => ({ // Update & expose name: Thing.common.name('Unnamed Group'), @@ -76,9 +74,7 @@ export class Group extends Thing { } export class GroupCategory extends Thing { - static [Thing.getPropertyDescriptors] = ({ - Group, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose name: Thing.common.name('Unnamed Group Category'), diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index c478bc41..59656b41 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,17 +1,18 @@ import find from '#find'; +import { + is, + isCountingNumber, + isString, + isStringNonEmpty, + validateArrayItems, + validateInstanceOf, +} from '#validators'; + import Thing from './thing.js'; export class HomepageLayout extends Thing { - static [Thing.getPropertyDescriptors] = ({ - HomepageLayoutRow, - - validators: { - isStringNonEmpty, - validateArrayItems, - validateInstanceOf, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ // Update & expose sidebarContent: Thing.common.simpleString(), @@ -32,10 +33,7 @@ export class HomepageLayout extends Thing { } export class HomepageLayoutRow extends Thing { - static [Thing.getPropertyDescriptors] = ({ - Album, - Group, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({ // Update & expose name: Thing.common.name('Unnamed Homepage Row'), @@ -63,17 +61,7 @@ export class HomepageLayoutRow extends Thing { } export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { - static [Thing.getPropertyDescriptors] = (opts, { - Album, - Group, - - validators: { - is, - isCountingNumber, - isString, - validateArrayItems, - }, - } = opts) => ({ + static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({ ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), // Update & expose diff --git a/src/data/things/index.js b/src/data/things/index.js index 2d4f77d7..3b73a772 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -4,7 +4,6 @@ import {fileURLToPath} from 'node:url'; import {logError} from '#cli'; import * as serialize from '#serialize'; import {openAggregate, showAggregate} from '#sugar'; -import * as validators from '#validators'; import Thing from './thing.js'; @@ -121,7 +120,7 @@ function descriptorAggregateHelper({ } function evaluatePropertyDescriptors() { - const opts = {...allClasses, validators}; + const opts = {...allClasses}; return descriptorAggregateHelper({ message: `Errors evaluating Thing class property descriptors`, diff --git a/src/data/things/language.js b/src/data/things/language.js index 7755c505..0638afa2 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,11 +1,9 @@ +import {isLanguageCode} from '#validators'; + import Thing from './thing.js'; export class Language extends Thing { - static [Thing.getPropertyDescriptors] = ({ - validators: { - isLanguageCode, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose // General language code. This is used to identify the language distinctly diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index 3d8d474c..ae0ca420 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -1,13 +1,11 @@ +import {isName} from '#validators'; + import Thing from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; - static [Thing.getPropertyDescriptors] = ({ - validators: { - isName, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose name: Thing.common.name('Unnamed Static Page'), diff --git a/src/data/things/track.js b/src/data/things/track.js index 7dde88db..10b966a7 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -3,6 +3,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; import {empty} from '#sugar'; +import {isColor, isDate, isDuration, isFileExtension} from '#validators'; import { compositeFrom, @@ -23,19 +24,7 @@ import Thing, { export class Track extends Thing { static [Thing.referenceType] = 'track'; - static [Thing.getPropertyDescriptors] = ({ - Album, - ArtTag, - Artist, - Flash, - - validators: { - isColor, - isDate, - isDuration, - isFileExtension, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album, ArtTag, Artist, Flash}) => ({ // Update & expose name: Thing.common.name('Unnamed Track'), diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index d6790c55..0ccef5ed 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -1,17 +1,10 @@ import find from '#find'; +import {isLanguageCode, isName, isURL} from '#validators'; import Thing from './thing.js'; export class WikiInfo extends Thing { - static [Thing.getPropertyDescriptors] = ({ - Group, - - validators: { - isLanguageCode, - isName, - isURL, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose name: Thing.common.name('Unnamed Wiki'), -- cgit 1.3.0-6-gf8a5 From eb00f2993a1aaaba171ad6c918656552f80bb748 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 12:38:34 -0300 Subject: data: import Thing.common utilities directly Also rename 'color' (from #cli) to 'colors'. --- src/content/dependencies/index.js | 8 +- src/data/things/album.js | 108 ++++--- src/data/things/art-tag.js | 20 +- src/data/things/artist.js | 35 ++- src/data/things/cacheable-object.js | 4 +- src/data/things/composite.js | 22 +- src/data/things/flash.js | 50 ++-- src/data/things/group.js | 37 ++- src/data/things/homepage-layout.js | 29 +- src/data/things/language.js | 12 +- src/data/things/news-entry.js | 15 +- src/data/things/static-page.js | 14 +- src/data/things/thing.js | 554 +++++++++++++++++++----------------- src/data/things/track.js | 83 +++--- src/data/things/validators.js | 6 +- src/data/things/wiki-info.js | 34 ++- src/data/yaml.js | 42 +-- src/find.js | 6 +- src/gen-thumbs.js | 6 +- src/upd8.js | 14 +- src/util/cli.js | 2 +- src/util/sugar.js | 10 +- 22 files changed, 622 insertions(+), 489 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index 3bc34845..57025a5d 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -6,7 +6,7 @@ import {fileURLToPath} from 'node:url'; import chokidar from 'chokidar'; import {ESLint} from 'eslint'; -import {color, logWarn} from '#cli'; +import {colors, logWarn} from '#cli'; import contentFunction, {ContentFunctionSpecError} from '#content-function'; import {annotateFunction} from '#sugar'; @@ -192,7 +192,7 @@ export function watchContentDependencies({ if (logging && emittedReady) { const timestamp = new Date().toLocaleString('en-US', {timeStyle: 'medium'}); - console.log(color.green(`[${timestamp}] Updated ${functionName}`)); + console.log(colors.green(`[${timestamp}] Updated ${functionName}`)); } contentDependencies[functionName] = fn; @@ -219,9 +219,9 @@ export function watchContentDependencies({ } if (typeof error === 'string') { - console.error(color.yellow(error)); + console.error(colors.yellow(error)); } else if (error instanceof ContentFunctionSpecError) { - console.error(color.yellow(error.message)); + console.error(colors.yellow(error.message)); } else { console.error(error); } diff --git a/src/data/things/album.js b/src/data/things/album.js index da018856..9cf58641 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -2,7 +2,25 @@ import find from '#find'; import {empty} from '#sugar'; import {isDate, isDimensions, isTrackSectionList} from '#validators'; -import Thing from './thing.js'; +import Thing, { + additionalFiles, + commentary, + color, + commentatorArtists, + contribsByRef, + contribsPresent, + directory, + dynamicContribs, + fileExtension, + flag, + name, + resolvedReferenceList, + referenceList, + simpleDate, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Album extends Thing { static [Thing.referenceType] = 'album'; @@ -10,14 +28,14 @@ export class Album extends Thing { static [Thing.getPropertyDescriptors] = ({ArtTag, Artist, Group, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Album'), - color: Thing.common.color(), - directory: Thing.common.directory(), - urls: Thing.common.urls(), + name: name('Unnamed Album'), + color: color(), + directory: directory(), + urls: urls(), - date: Thing.common.simpleDate(), - trackArtDate: Thing.common.simpleDate(), - dateAddedToWiki: Thing.common.simpleDate(), + date: simpleDate(), + trackArtDate: simpleDate(), + dateAddedToWiki: simpleDate(), coverArtDate: { flags: {update: true, expose: true}, @@ -36,14 +54,14 @@ export class Album extends Thing { }, }, - artistContribsByRef: Thing.common.contribsByRef(), - coverArtistContribsByRef: Thing.common.contribsByRef(), - trackCoverArtistContribsByRef: Thing.common.contribsByRef(), - wallpaperArtistContribsByRef: Thing.common.contribsByRef(), - bannerArtistContribsByRef: Thing.common.contribsByRef(), + artistContribsByRef: contribsByRef(), + coverArtistContribsByRef: contribsByRef(), + trackCoverArtistContribsByRef: contribsByRef(), + wallpaperArtistContribsByRef: contribsByRef(), + bannerArtistContribsByRef: contribsByRef(), - groupsByRef: Thing.common.referenceList(Group), - artTagsByRef: Thing.common.referenceList(ArtTag), + groupsByRef: referenceList(Group), + artTagsByRef: referenceList(ArtTag), trackSections: { flags: {update: true, expose: true}, @@ -81,58 +99,58 @@ export class Album extends Thing { }, }, - coverArtFileExtension: Thing.common.fileExtension('jpg'), - trackCoverArtFileExtension: Thing.common.fileExtension('jpg'), + coverArtFileExtension: fileExtension('jpg'), + trackCoverArtFileExtension: fileExtension('jpg'), - wallpaperStyle: Thing.common.simpleString(), - wallpaperFileExtension: Thing.common.fileExtension('jpg'), + wallpaperStyle: simpleString(), + wallpaperFileExtension: fileExtension('jpg'), - bannerStyle: Thing.common.simpleString(), - bannerFileExtension: Thing.common.fileExtension('jpg'), + bannerStyle: simpleString(), + bannerFileExtension: fileExtension('jpg'), bannerDimensions: { flags: {update: true, expose: true}, update: {validate: isDimensions}, }, - hasTrackNumbers: Thing.common.flag(true), - isListedOnHomepage: Thing.common.flag(true), - isListedInGalleries: Thing.common.flag(true), + hasTrackNumbers: flag(true), + isListedOnHomepage: flag(true), + isListedInGalleries: flag(true), - commentary: Thing.common.commentary(), - additionalFiles: Thing.common.additionalFiles(), + commentary: commentary(), + additionalFiles: additionalFiles(), // Update only - artistData: Thing.common.wikiData(Artist), - artTagData: Thing.common.wikiData(ArtTag), - groupData: Thing.common.wikiData(Group), - trackData: Thing.common.wikiData(Track), + artistData: wikiData(Artist), + artTagData: wikiData(ArtTag), + groupData: wikiData(Group), + trackData: wikiData(Track), // Expose only - artistContribs: Thing.common.dynamicContribs('artistContribsByRef'), - coverArtistContribs: Thing.common.dynamicContribs('coverArtistContribsByRef'), - trackCoverArtistContribs: Thing.common.dynamicContribs('trackCoverArtistContribsByRef'), - wallpaperArtistContribs: Thing.common.dynamicContribs('wallpaperArtistContribsByRef'), - bannerArtistContribs: Thing.common.dynamicContribs('bannerArtistContribsByRef'), + artistContribs: dynamicContribs('artistContribsByRef'), + coverArtistContribs: dynamicContribs('coverArtistContribsByRef'), + trackCoverArtistContribs: dynamicContribs('trackCoverArtistContribsByRef'), + wallpaperArtistContribs: dynamicContribs('wallpaperArtistContribsByRef'), + bannerArtistContribs: dynamicContribs('bannerArtistContribsByRef'), - commentatorArtists: Thing.common.commentatorArtists(), + commentatorArtists: commentatorArtists(), - groups: Thing.common.resolvedReferenceList({ + groups: resolvedReferenceList({ list: 'groupsByRef', data: 'groupData', find: find.group, }), - artTags: Thing.common.resolvedReferenceList({ + artTags: resolvedReferenceList({ list: 'artTagsByRef', data: 'artTagData', find: find.artTag, }), - hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'), - hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'), - hasBannerArt: Thing.common.contribsPresent('bannerArtistContribsByRef'), + hasCoverArt: contribsPresent('coverArtistContribsByRef'), + hasWallpaperArt: contribsPresent('wallpaperArtistContribsByRef'), + hasBannerArt: contribsPresent('bannerArtistContribsByRef'), tracks: { flags: {expose: true}, @@ -192,9 +210,9 @@ export class Album extends Thing { export class TrackSectionHelper extends Thing { static [Thing.getPropertyDescriptors] = () => ({ - name: Thing.common.name('Unnamed Track Group'), - color: Thing.common.color(), - dateOriginallyReleased: Thing.common.simpleDate(), - isDefaultTrackGroup: Thing.common.flag(false), + name: name('Unnamed Track Group'), + color: color(), + dateOriginallyReleased: simpleDate(), + isDefaultTrackGroup: flag(false), }) } diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 5d7d0cbf..3d65b578 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,6 +1,12 @@ import {sortAlbumsTracksChronologically} from '#wiki-data'; -import Thing from './thing.js'; +import Thing, { + color, + directory, + flag, + name, + wikiData, +} from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; @@ -8,10 +14,10 @@ export class ArtTag extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Art Tag'), - directory: Thing.common.directory(), - color: Thing.common.color(), - isContentWarning: Thing.common.flag(false), + name: name('Unnamed Art Tag'), + directory: directory(), + color: color(), + isContentWarning: flag(false), nameShort: { flags: {update: true, expose: true}, @@ -25,8 +31,8 @@ export class ArtTag extends Thing { // Update only - albumData: Thing.common.wikiData(Album), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + trackData: wikiData(Track), // Expose only diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 93a1b51b..2676591a 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -1,7 +1,16 @@ import find from '#find'; import {isName, validateArrayItems} from '#validators'; -import Thing from './thing.js'; +import Thing, { + directory, + fileExtension, + flag, + name, + simpleString, + singleReference, + urls, + wikiData, +} from './thing.js'; export class Artist extends Thing { static [Thing.referenceType] = 'artist'; @@ -9,13 +18,13 @@ export class Artist extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Artist'), - directory: Thing.common.directory(), - urls: Thing.common.urls(), - contextNotes: Thing.common.simpleString(), + name: name('Unnamed Artist'), + directory: directory(), + urls: urls(), + contextNotes: simpleString(), - hasAvatar: Thing.common.flag(false), - avatarFileExtension: Thing.common.fileExtension('jpg'), + hasAvatar: flag(false), + avatarFileExtension: fileExtension('jpg'), aliasNames: { flags: {update: true, expose: true}, @@ -23,15 +32,15 @@ export class Artist extends Thing { expose: {transform: (names) => names ?? []}, }, - isAlias: Thing.common.flag(), - aliasedArtistRef: Thing.common.singleReference(Artist), + isAlias: flag(), + aliasedArtistRef: singleReference(Artist), // Update only - albumData: Thing.common.wikiData(Album), - artistData: Thing.common.wikiData(Artist), - flashData: Thing.common.wikiData(Flash), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + artistData: wikiData(Artist), + flashData: wikiData(Flash), + trackData: wikiData(Track), // Expose only diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 62c23d13..92a46d66 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -76,7 +76,7 @@ import {inspect as nodeInspect} from 'node:util'; -import {color, ENABLE_COLOR} from '#cli'; +import {colors, ENABLE_COLOR} from '#cli'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -183,7 +183,7 @@ export default class CacheableObject { } } catch (error) { error.message = [ - `Property ${color.green(property)}`, + `Property ${colors.green(property)}`, `(${inspect(this[property])} -> ${inspect(newValue)}):`, error.message ].join(' '); diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 5b6de901..fd52aa0f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import { empty, @@ -346,8 +346,8 @@ export function compositeFrom(firstArg, secondArg) { if (compositeFrom.debug === true) { const label = (annotation - ? color.dim(`[composite: ${annotation}]`) - : color.dim(`[composite]`)); + ? colors.dim(`[composite: ${annotation}]`) + : colors.dim(`[composite]`)); const result = fn(); if (Array.isArray(result)) { console.log(label, ...result.map(value => @@ -594,9 +594,9 @@ export function compositeFrom(firstArg, secondArg) { const availableDependencies = {...initialDependencies}; if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); } else { - debug(() => color.bright(`begin composition - not transforming`)); + debug(() => colors.bright(`begin composition - not transforming`)); } for (let i = 0; i < steps.length; i++) { @@ -641,7 +641,7 @@ export function compositeFrom(firstArg, secondArg) { throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - debug(() => color.bright(`end composition - exit (inferred)`)); + debug(() => colors.bright(`end composition - exit (inferred)`)); return result; } @@ -652,7 +652,7 @@ export function compositeFrom(firstArg, secondArg) { const {providedValue} = continuationStorage; debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); + debug(() => colors.bright(`end composition - exit (explicit)`)); if (baseComposes) { return continuationIfApplicable.exit(providedValue); @@ -708,17 +708,17 @@ export function compositeFrom(firstArg, secondArg) { case 'raise': debug(() => (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); + ? colors.bright(`end composition - raise (base: explicit)`) + : colors.bright(`end composition - raise`))); return continuationIfApplicable(...continuationArgs); case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); + debug(() => colors.bright(`end composition - raiseAbove`)); return continuationIfApplicable.raise(...continuationArgs); case 'continuation': if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); + debug(() => colors.bright(`end composition - raise (inferred)`)); return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, continuingWithDependencies); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index ce2e7fac..4e640dac 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -8,7 +8,19 @@ import { oneOf, } from '#validators'; -import Thing from './thing.js'; +import Thing, { + dynamicContribs, + color, + contribsByRef, + fileExtension, + name, + referenceList, + resolvedReferenceList, + simpleDate, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; @@ -16,7 +28,7 @@ export class Flash extends Thing { static [Thing.getPropertyDescriptors] = ({Artist, Track, FlashAct}) => ({ // Update & expose - name: Thing.common.name('Unnamed Flash'), + name: name('Unnamed Flash'), directory: { flags: {update: true, expose: true}, @@ -44,27 +56,27 @@ export class Flash extends Thing { }, }, - date: Thing.common.simpleDate(), + date: simpleDate(), - coverArtFileExtension: Thing.common.fileExtension('jpg'), + coverArtFileExtension: fileExtension('jpg'), - contributorContribsByRef: Thing.common.contribsByRef(), + contributorContribsByRef: contribsByRef(), - featuredTracksByRef: Thing.common.referenceList(Track), + featuredTracksByRef: referenceList(Track), - urls: Thing.common.urls(), + urls: urls(), // Update only - artistData: Thing.common.wikiData(Artist), - trackData: Thing.common.wikiData(Track), - flashActData: Thing.common.wikiData(FlashAct), + artistData: wikiData(Artist), + trackData: wikiData(Track), + flashActData: wikiData(FlashAct), // Expose only - contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'), + contributorContribs: dynamicContribs('contributorContribsByRef'), - featuredTracks: Thing.common.resolvedReferenceList({ + featuredTracks: resolvedReferenceList({ list: 'featuredTracksByRef', data: 'trackData', find: find.track, @@ -111,10 +123,10 @@ export class FlashAct extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed Flash Act'), - color: Thing.common.color(), - anchor: Thing.common.simpleString(), - jump: Thing.common.simpleString(), + name: name('Unnamed Flash Act'), + color: color(), + anchor: simpleString(), + jump: simpleString(), jumpColor: { flags: {update: true, expose: true}, @@ -126,15 +138,15 @@ export class FlashAct extends Thing { } }, - flashesByRef: Thing.common.referenceList(Flash), + flashesByRef: referenceList(Flash), // Update only - flashData: Thing.common.wikiData(Flash), + flashData: wikiData(Flash), // Expose only - flashes: Thing.common.resolvedReferenceList({ + flashes: resolvedReferenceList({ list: 'flashesByRef', data: 'flashData', find: find.flash, diff --git a/src/data/things/group.js b/src/data/things/group.js index 6c712847..873c6d88 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,6 +1,15 @@ import find from '#find'; -import Thing from './thing.js'; +import Thing, { + color, + directory, + name, + referenceList, + resolvedReferenceList, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Group extends Thing { static [Thing.referenceType] = 'group'; @@ -8,23 +17,23 @@ export class Group extends Thing { static [Thing.getPropertyDescriptors] = ({Album}) => ({ // Update & expose - name: Thing.common.name('Unnamed Group'), - directory: Thing.common.directory(), + name: name('Unnamed Group'), + directory: directory(), - description: Thing.common.simpleString(), + description: simpleString(), - urls: Thing.common.urls(), + urls: urls(), - featuredAlbumsByRef: Thing.common.referenceList(Album), + featuredAlbumsByRef: referenceList(Album), // Update only - albumData: Thing.common.wikiData(Album), - groupCategoryData: Thing.common.wikiData(GroupCategory), + albumData: wikiData(Album), + groupCategoryData: wikiData(GroupCategory), // Expose only - featuredAlbums: Thing.common.resolvedReferenceList({ + featuredAlbums: resolvedReferenceList({ list: 'featuredAlbumsByRef', data: 'albumData', find: find.album, @@ -77,18 +86,18 @@ export class GroupCategory extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Group Category'), - color: Thing.common.color(), + name: name('Unnamed Group Category'), + color: color(), - groupsByRef: Thing.common.referenceList(Group), + groupsByRef: referenceList(Group), // Update only - groupData: Thing.common.wikiData(Group), + groupData: wikiData(Group), // Expose only - groups: Thing.common.resolvedReferenceList({ + groups: resolvedReferenceList({ list: 'groupsByRef', data: 'groupData', find: find.group, diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 59656b41..ab6f4cff 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -9,13 +9,22 @@ import { validateInstanceOf, } from '#validators'; -import Thing from './thing.js'; +import Thing, { + color, + name, + referenceList, + resolvedReference, + resolvedReferenceList, + simpleString, + singleReference, + wikiData, +} from './thing.js'; export class HomepageLayout extends Thing { static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ // Update & expose - sidebarContent: Thing.common.simpleString(), + sidebarContent: simpleString(), navbarLinks: { flags: {update: true, expose: true}, @@ -36,7 +45,7 @@ export class HomepageLayoutRow extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Homepage Row'), + name: name('Unnamed Homepage Row'), type: { flags: {update: true, expose: true}, @@ -48,15 +57,15 @@ export class HomepageLayoutRow extends Thing { }, }, - color: Thing.common.color(), + color: color(), // Update only // These aren't necessarily used by every HomepageLayoutRow subclass, but // for convenience of providing this data, every row accepts all wiki data // arrays depended upon by any subclass's behavior. - albumData: Thing.common.wikiData(Album), - groupData: Thing.common.wikiData(Group), + albumData: wikiData(Album), + groupData: wikiData(Group), }); } @@ -92,8 +101,8 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }, }, - sourceGroupByRef: Thing.common.singleReference(Group), - sourceAlbumsByRef: Thing.common.referenceList(Album), + sourceGroupByRef: singleReference(Group), + sourceAlbumsByRef: referenceList(Album), countAlbumsFromGroup: { flags: {update: true, expose: true}, @@ -107,13 +116,13 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { // Expose only - sourceGroup: Thing.common.resolvedReference({ + sourceGroup: resolvedReference({ ref: 'sourceGroupByRef', data: 'groupData', find: find.group, }), - sourceAlbums: Thing.common.resolvedReferenceList({ + sourceAlbums: resolvedReferenceList({ list: 'sourceAlbumsByRef', data: 'albumData', find: find.album, diff --git a/src/data/things/language.js b/src/data/things/language.js index 0638afa2..c98495dc 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,6 +1,10 @@ import {isLanguageCode} from '#validators'; -import Thing from './thing.js'; +import Thing, { + externalFunction, + flag, + simpleString, +} from './thing.js'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ @@ -16,7 +20,7 @@ export class Language extends Thing { // Human-readable name. This should be the language's own native name, not // localized to any other language. - name: Thing.common.simpleString(), + name: simpleString(), // Language code specific to JavaScript's Internationalization (Intl) API. // Usually this will be the same as the language's general code, but it @@ -38,7 +42,7 @@ export class Language extends Thing { // with languages that are currently in development and not ready for // formal release, or which are just kept hidden as "experimental zones" // for wiki development or content testing. - hidden: Thing.common.flag(false), + hidden: flag(false), // Mapping of translation keys to values (strings). Generally, don't // access this object directly - use methods instead. @@ -66,7 +70,7 @@ export class Language extends Thing { // Update only - escapeHTML: Thing.common.externalFunction(), + escapeHTML: externalFunction(), // Expose only diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index 43911410..6984874e 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,4 +1,9 @@ -import Thing from './thing.js'; +import Thing, { + directory, + name, + simpleDate, + simpleString, +} from './thing.js'; export class NewsEntry extends Thing { static [Thing.referenceType] = 'news-entry'; @@ -6,11 +11,11 @@ export class NewsEntry extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed News Entry'), - directory: Thing.common.directory(), - date: Thing.common.simpleDate(), + name: name('Unnamed News Entry'), + directory: directory(), + date: simpleDate(), - content: Thing.common.simpleString(), + content: simpleString(), // Expose only diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index ae0ca420..0133e0b6 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -1,6 +1,10 @@ import {isName} from '#validators'; -import Thing from './thing.js'; +import Thing, { + directory, + name, + simpleString, +} from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; @@ -8,7 +12,7 @@ export class StaticPage extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed Static Page'), + name: name('Unnamed Static Page'), nameShort: { flags: {update: true, expose: true}, @@ -20,8 +24,8 @@ export class StaticPage extends Thing { }, }, - directory: Thing.common.directory(), - content: Thing.common.simpleString(), - stylesheet: Thing.common.simpleString(), + directory: directory(), + content: simpleString(), + stylesheet: simpleString(), }); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 98dec3c3..19f00b3e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -3,7 +3,7 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import find from '#find'; import {empty, stitchArrays} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; @@ -41,297 +41,329 @@ export default class Thing extends CacheableObject { static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); - // Regularly reused property descriptors, for ease of access and generally - // duplicating less code across wiki data types. These are specialized utility - // functions, so check each for how its own arguments behave! - static common = { - name: (defaultName) => ({ - flags: {update: true, expose: true}, - update: {validate: isName, default: defaultName}, - }), + // Default custom inspect function, which may be overridden by Thing + // subclasses. This will be used when displaying aggregate errors and other + // command-line logging - it's the place to provide information useful in + // identifying the Thing being presented. + [inspect.custom]() { + const cname = this.constructor.name; - color: () => ({ - flags: {update: true, expose: true}, - update: {validate: isColor}, - }), + return ( + (this.name ? `${cname} ${colors.green(`"${this.name}"`)}` : `${cname}`) + + (this.directory ? ` (${colors.blue(Thing.getReference(this))})` : '') + ); + } - directory: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: { - dependencies: ['name'], - transform(directory, {name}) { - if (directory === null && name === null) return null; - else if (directory === null) return getKebabCase(name); - else return directory; - }, - }, - }), + static getReference(thing) { + if (!thing.constructor[Thing.referenceType]) { + throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); + } - urls: () => ({ - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, - expose: {transform: (value) => value ?? []}, - }), + if (!thing.directory) { + throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); + } - // A file extension! Or the default, if provided when calling this. - fileExtension: (defaultFileExtension = null) => ({ - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: {transform: (value) => value ?? defaultFileExtension}, - }), + return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; + } +} + +// Property descriptor templates +// +// Regularly reused property descriptors, for ease of access and generally +// duplicating less code across wiki data types. These are specialized utility +// functions, so check each for how its own arguments behave! + +export function name(defaultName) { + return { + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName}, + }; +} + +export function color() { + return { + flags: {update: true, expose: true}, + update: {validate: isColor}, + }; +} - // Straightforward flag descriptor for a variety of property purposes. - // Provide a default value, true or false! - flag: (defaultValue = false) => { - if (typeof defaultValue !== 'boolean') { - throw new TypeError(`Always set explicit defaults for flags!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: defaultValue}, - }; +export function directory() { + return { + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + expose: { + dependencies: ['name'], + transform(directory, {name}) { + if (directory === null && name === null) return null; + else if (directory === null) return getKebabCase(name); + else return directory; + }, }, + }; +} - // General date type, used as the descriptor for a bunch of properties. - // This isn't dynamic though - it won't inherit from a date stored on - // another object, for example. - simpleDate: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDate}, - }), +export function urls() { + return { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)}, + expose: {transform: (value) => value ?? []}, + }; +} - // General string type. This should probably generally be avoided in favor - // of more specific validation, but using it makes it easy to find where we - // might want to improve later, and it's a useful shorthand meanwhile. - simpleString: () => ({ - flags: {update: true, expose: true}, - update: {validate: isString}, - }), +// A file extension! Or the default, if provided when calling this. +export function fileExtension(defaultFileExtension = null) { + return { + flags: {update: true, expose: true}, + update: {validate: isFileExtension}, + expose: {transform: (value) => value ?? defaultFileExtension}, + }; +} - // External function. These should only be used as dependencies for other - // properties, so they're left unexposed. - externalFunction: () => ({ - flags: {update: true}, - update: {validate: (t) => typeof t === 'function'}, - }), +// Straightforward flag descriptor for a variety of property purposes. +// Provide a default value, true or false! +export function flag(defaultValue = false) { + // TODO: ^ Are you actually kidding me + if (typeof defaultValue !== 'boolean') { + throw new TypeError(`Always set explicit defaults for flags!`); + } - // Super simple "contributions by reference" list, used for a variety of - // properties (Artists, Cover Artists, etc). This is the property which is - // externally provided, in the form: - // - // [ - // {who: 'Artist Name', what: 'Viola'}, - // {who: 'artist:john-cena', what: null}, - // ... - // ] - // - // ...processed from YAML, spreadsheet, or any other kind of input. - contribsByRef: () => ({ - flags: {update: true, expose: true}, - update: {validate: isContributionList}, - }), + return { + flags: {update: true, expose: true}, + update: {validate: isBoolean, default: defaultValue}, + }; +} - // Artist commentary! Generally present on tracks and albums. - commentary: () => ({ - flags: {update: true, expose: true}, - update: {validate: isCommentary}, - }), +// General date type, used as the descriptor for a bunch of properties. +// This isn't dynamic though - it won't inherit from a date stored on +// another object, for example. +export function simpleDate() { + return { + flags: {update: true, expose: true}, + update: {validate: isDate}, + }; +} - // This is a somewhat more involved data structure - it's for additional - // or "bonus" files associated with albums or tracks (or anything else). - // It's got this form: - // - // [ - // {title: 'Booklet', files: ['Booklet.pdf']}, - // { - // title: 'Wallpaper', - // description: 'Cool Wallpaper!', - // files: ['1440x900.png', '1920x1080.png'] - // }, - // {title: 'Alternate Covers', description: null, files: [...]}, - // ... - // ] - // - additionalFiles: () => ({ - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }), +// General string type. This should probably generally be avoided in favor +// of more specific validation, but using it makes it easy to find where we +// might want to improve later, and it's a useful shorthand meanwhile. +export function simpleString() { + return { + flags: {update: true, expose: true}, + update: {validate: isString}, + }; +} - // A reference list! Keep in mind this is for general references to wiki - // objects of (usually) other Thing subclasses, not specifically leitmotif - // references in tracks (although that property uses referenceList too!). - // - // The underlying function validateReferenceList expects a string like - // 'artist' or 'track', but this utility keeps from having to hard-code the - // string in multiple places by referencing the value saved on the class - // instead. - referenceList: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList(referenceType)}, - }; - }, +// External function. These should only be used as dependencies for other +// properties, so they're left unexposed. +export function externalFunction() { + return { + flags: {update: true}, + update: {validate: (t) => typeof t === 'function'}, + }; +} - // Corresponding function for a single reference. - singleReference: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReference(referenceType)}, - }; - }, +// Super simple "contributions by reference" list, used for a variety of +// properties (Artists, Cover Artists, etc). This is the property which is +// externally provided, in the form: +// +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] +// +// ...processed from YAML, spreadsheet, or any other kind of input. +export function contribsByRef() { + return { + flags: {update: true, expose: true}, + update: {validate: isContributionList}, + }; +} - // Corresponding dynamic property to referenceList, which takes the values - // in the provided property and searches the specified wiki data for - // matching actual Thing-subclass objects. - resolvedReferenceList({list, data, find}) { - return compositeFrom(`Thing.common.resolvedReferenceList`, [ - withResolvedReferenceList({ - list, data, find, - notFoundMode: 'filter', - }), +// Artist commentary! Generally present on tracks and albums. +export function commentary() { + return { + flags: {update: true, expose: true}, + update: {validate: isCommentary}, + }; +} - exposeDependency({dependency: '#resolvedReferenceList'}), - ]); +// 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: [...]}, +// ... +// ] +// +export function additionalFiles() { + return { + flags: {update: true, expose: true}, + update: {validate: isAdditionalFileList}, + expose: { + transform: (additionalFiles) => + additionalFiles ?? [], }, + }; +} - // Corresponding function for a single reference. - resolvedReference({ref, data, find}) { - return compositeFrom(`Thing.common.resolvedReference`, [ - withResolvedReference({ref, data, find}), - exposeDependency({dependency: '#resolvedReference'}), - ]); - }, +// A reference list! Keep in mind this is for general references to wiki +// objects of (usually) other Thing subclasses, not specifically leitmotif +// references in tracks (although that property uses referenceList too!). +// +// The underlying function validateReferenceList expects a string like +// 'artist' or 'track', but this utility keeps from having to hard-code the +// string in multiple places by referencing the value saved on the class +// instead. +export function referenceList(thingClass) { + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } - // Corresponding dynamic property to contribsByRef, which takes the values - // in the provided property and searches the object's artistData for - // matching actual Artist objects. The computed structure has the same form - // as contribsByRef, but with Artist objects instead of string references: - // - // [ - // {who: (an Artist), what: 'Viola'}, - // {who: (an Artist), what: null}, - // ... - // ] - // - // Contributions whose "who" values don't match anything in artistData are - // filtered out. (So if the list is all empty, chances are that either the - // reference list is somehow messed up, or artistData isn't being provided - // properly.) - dynamicContribs(contribsByRefProperty) { - return compositeFrom(`Thing.common.dynamicContribs`, [ - withResolvedContribs({ - from: contribsByRefProperty, - into: '#contribs', - }), + return { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList(referenceType)}, + }; +} - exposeDependency({dependency: '#contribs'}), - ]); - }, +// Corresponding function for a single reference. +export function singleReference(thingClass) { + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } - // Nice 'n simple shorthand for an exposed-only flag which is true when any - // contributions are present in the specified property. - contribsPresent: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: [contribsByRefProperty], - compute({ - [contribsByRefProperty]: contribsByRef, - }) { - return !empty(contribsByRef); - }, - } + return { + flags: {update: true, expose: true}, + update: {validate: validateReference(referenceType)}, + }; +} + +// Corresponding dynamic property to referenceList, which takes the values +// in the provided property and searches the specified wiki data for +// matching actual Thing-subclass objects. +export function resolvedReferenceList({list, data, find}) { + return compositeFrom(`resolvedReferenceList`, [ + withResolvedReferenceList({ + list, data, find, + notFoundMode: 'filter', }), - // Neat little shortcut for "reversing" the reference lists stored on other - // things - for example, tracks specify a "referenced tracks" property, and - // you would use this to compute a corresponding "referenced *by* tracks" - // property. Naturally, the passed ref list property is of the things in the - // wiki data provided, not the requesting Thing itself. - reverseReferenceList({data, list}) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ - withReverseReferenceList({data, list}), - exposeDependency({dependency: '#reverseReferenceList'}), - ]); - }, + exposeDependency({dependency: '#resolvedReferenceList'}), + ]); +} - // General purpose wiki data constructor, for properties like artistData, - // trackData, etc. - wikiData: (thingClass) => ({ - flags: {update: true}, - update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), - }, - }), +// Corresponding function for a single reference. +export function resolvedReference({ref, data, find}) { + return compositeFrom(`resolvedReference`, [ + withResolvedReference({ref, data, find}), + exposeDependency({dependency: '#resolvedReference'}), + ]); +} - // This one's kinda tricky: it parses artist "references" from the - // commentary content, and finds the matching artist for each reference. - // This is mostly useful for credits and listings on artist pages. - commentatorArtists: () => ({ - flags: {expose: true}, - - expose: { - dependencies: ['artistData', 'commentary'], - - compute: ({artistData, commentary}) => - artistData && commentary - ? Array.from( - new Set( - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/<i>(?<who>.*?):<\/i>/g) - ).map(({groups: {who}}) => - find.artist(who, artistData, {mode: 'quiet'}) - ) - ) - ) - : [], - }, +// Corresponding dynamic property to contribsByRef, which takes the values +// in the provided property and searches the object's artistData for +// matching actual Artist objects. The computed structure has the same form +// as contribsByRef, but with Artist objects instead of string references: +// +// [ +// {who: (an Artist), what: 'Viola'}, +// {who: (an Artist), what: null}, +// ... +// ] +// +// Contributions whose "who" values don't match anything in artistData are +// filtered out. (So if the list is all empty, chances are that either the +// reference list is somehow messed up, or artistData isn't being provided +// properly.) +export function dynamicContribs(contribsByRefProperty) { + return compositeFrom(`dynamicContribs`, [ + withResolvedContribs({ + from: contribsByRefProperty, + into: '#contribs', }), - }; - - // Default custom inspect function, which may be overridden by Thing - // subclasses. This will be used when displaying aggregate errors and other - // command-line logging - it's the place to provide information useful in - // identifying the Thing being presented. - [inspect.custom]() { - const cname = this.constructor.name; - return ( - (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) + - (this.directory ? ` (${color.blue(Thing.getReference(this))})` : '') - ); - } + exposeDependency({dependency: '#contribs'}), + ]); +} - static getReference(thing) { - if (!thing.constructor[Thing.referenceType]) { - throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); +// Nice 'n simple shorthand for an exposed-only flag which is true when any +// contributions are present in the specified property. +export function contribsPresent(contribsByRefProperty) { + return { + flags: {expose: true}, + expose: { + dependencies: [contribsByRefProperty], + compute({ + [contribsByRefProperty]: contribsByRef, + }) { + return !empty(contribsByRef); + }, } + }; +} - if (!thing.directory) { - throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); - } +// Neat little shortcut for "reversing" the reference lists stored on other +// things - for example, tracks specify a "referenced tracks" property, and +// you would use this to compute a corresponding "referenced *by* tracks" +// property. Naturally, the passed ref list property is of the things in the +// wiki data provided, not the requesting Thing itself. +export function reverseReferenceList({data, list}) { + return compositeFrom(`reverseReferenceList`, [ + withReverseReferenceList({data, list}), + exposeDependency({dependency: '#reverseReferenceList'}), + ]); +} - return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; - } +// General purpose wiki data constructor, for properties like artistData, +// trackData, etc. +export function wikiData(thingClass) { + return { + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)), + }, + }; } +// This one's kinda tricky: it parses artist "references" from the +// commentary content, and finds the matching artist for each reference. +// This is mostly useful for credits and listings on artist pages. +export function commentatorArtists(){ + return { + flags: {expose: true}, + + expose: { + dependencies: ['artistData', 'commentary'], + + compute: ({artistData, commentary}) => + artistData && commentary + ? Array.from( + new Set( + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/<i>(?<who>.*?):<\/i>/g) + ).map(({groups: {who}}) => + find.artist(who, artistData, {mode: 'quiet'}) + ) + ) + ) + : [], + }, + }; +} + +// Compositional utilities + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist @@ -479,14 +511,14 @@ export function withResolvedReferenceList({ ]); } -// Check out the info on Thing.common.reverseReferenceList! +// Check out the info on reverseReferenceList! // This is its composable form. export function withReverseReferenceList({ data, list: refListProperty, into = '#reverseReferenceList', }) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ + return compositeFrom(`withReverseReferenceList`, [ exitWithoutDependency({ dependency: data, value: [], diff --git a/src/data/things/track.js b/src/data/things/track.js index 10b966a7..41c92092 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import find from '#find'; import {empty} from '#sugar'; import {isColor, isDate, isDuration, isFileExtension} from '#validators'; @@ -16,6 +16,23 @@ import { } from '#composite'; import Thing, { + additionalFiles, + commentary, + commentatorArtists, + contribsByRef, + directory, + dynamicContribs, + flag, + name, + referenceList, + resolvedReference, + resolvedReferenceList, + reverseReferenceList, + simpleDate, + singleReference, + simpleString, + urls, + wikiData, withResolvedContribs, withResolvedReference, withReverseReferenceList, @@ -27,24 +44,24 @@ export class Track extends Thing { static [Thing.getPropertyDescriptors] = ({Album, ArtTag, Artist, Flash}) => ({ // Update & expose - name: Thing.common.name('Unnamed Track'), - directory: Thing.common.directory(), + name: name('Unnamed Track'), + directory: directory(), duration: { flags: {update: true, expose: true}, update: {validate: isDuration}, }, - urls: Thing.common.urls(), - dateFirstReleased: Thing.common.simpleDate(), + urls: urls(), + dateFirstReleased: simpleDate(), - artistContribsByRef: Thing.common.contribsByRef(), - contributorContribsByRef: Thing.common.contribsByRef(), - coverArtistContribsByRef: Thing.common.contribsByRef(), + artistContribsByRef: contribsByRef(), + contributorContribsByRef: contribsByRef(), + coverArtistContribsByRef: contribsByRef(), - referencedTracksByRef: Thing.common.referenceList(Track), - sampledTracksByRef: Thing.common.referenceList(Track), - artTagsByRef: Thing.common.referenceList(ArtTag), + referencedTracksByRef: referenceList(Track), + sampledTracksByRef: referenceList(Track), + artTagsByRef: referenceList(ArtTag), color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), @@ -74,7 +91,7 @@ export class Track extends Thing { // 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: Thing.common.flag(), + disableUniqueCoverArt: flag(), // File extension for track's corresponding media file. This represents the // track's unique cover artwork, if any, and does not inherit the extension @@ -117,27 +134,27 @@ export class Track extends Thing { }), ]), - originalReleaseTrackByRef: Thing.common.singleReference(Track), + originalReleaseTrackByRef: singleReference(Track), - dataSourceAlbumByRef: Thing.common.singleReference(Album), + dataSourceAlbumByRef: singleReference(Album), - commentary: Thing.common.commentary(), - lyrics: Thing.common.simpleString(), - additionalFiles: Thing.common.additionalFiles(), - sheetMusicFiles: Thing.common.additionalFiles(), - midiProjectFiles: Thing.common.additionalFiles(), + commentary: commentary(), + lyrics: simpleString(), + additionalFiles: additionalFiles(), + sheetMusicFiles: additionalFiles(), + midiProjectFiles: additionalFiles(), // Update only - albumData: Thing.common.wikiData(Album), - artistData: Thing.common.wikiData(Artist), - artTagData: Thing.common.wikiData(ArtTag), - flashData: Thing.common.wikiData(Flash), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + artistData: wikiData(Artist), + artTagData: wikiData(ArtTag), + flashData: wikiData(Flash), + trackData: wikiData(Track), // Expose only - commentatorArtists: Thing.common.commentatorArtists(), + commentatorArtists: commentatorArtists(), album: compositeFrom(`Track.album`, [ withAlbum(), @@ -151,7 +168,7 @@ export class Track extends Thing { // not generally relevant information). It's also not guaranteed that // dataSourceAlbum is available (depending on the Track creator to optionally // provide dataSourceAlbumByRef). - dataSourceAlbum: Thing.common.resolvedReference({ + dataSourceAlbum: resolvedReference({ ref: 'dataSourceAlbumByRef', data: 'albumData', find: find.album, @@ -226,7 +243,7 @@ export class Track extends Thing { contributorContribs: compositeFrom(`Track.contributorContribs`, [ inheritFromOriginalRelease({property: 'contributorContribs'}), - Thing.common.dynamicContribs('contributorContribsByRef'), + dynamicContribs('contributorContribsByRef'), ]), // Cover artists aren't inherited from the original release, since it @@ -260,7 +277,7 @@ export class Track extends Thing { referencedTracks: compositeFrom(`Track.referencedTracks`, [ inheritFromOriginalRelease({property: 'referencedTracks'}), - Thing.common.resolvedReferenceList({ + resolvedReferenceList({ list: 'referencedTracksByRef', data: 'trackData', find: find.track, @@ -269,14 +286,14 @@ export class Track extends Thing { sampledTracks: compositeFrom(`Track.sampledTracks`, [ inheritFromOriginalRelease({property: 'sampledTracks'}), - Thing.common.resolvedReferenceList({ + resolvedReferenceList({ list: 'sampledTracksByRef', data: 'trackData', find: find.track, }), ]), - artTags: Thing.common.resolvedReferenceList({ + artTags: resolvedReferenceList({ list: 'artTagsByRef', data: 'artTagData', find: find.artTag, @@ -299,7 +316,7 @@ export class Track extends Thing { property: 'sampledTracks', }), - featuredInFlashes: Thing.common.reverseReferenceList({ + featuredInFlashes: reverseReferenceList({ data: 'flashData', list: 'featuredTracks', }), @@ -311,7 +328,7 @@ export class Track extends Thing { parts.push(Thing.prototype[inspect.custom].apply(this)); if (this.originalReleaseTrackByRef) { - parts.unshift(`${color.yellow('[rerelease]')} `); + parts.unshift(`${colors.yellow('[rerelease]')} `); } let album; @@ -322,7 +339,7 @@ export class Track extends Thing { (albumIndex === -1 ? '#?' : `#${albumIndex + 1}`); - parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); + parts.push(` (${colors.yellow(trackNum)} in ${colors.green(albumName)})`); } return parts.join(''); diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 5748eacf..4c8f683b 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -1,6 +1,6 @@ import {inspect as nodeInspect} from 'node:util'; -import {color, ENABLE_COLOR} from '#cli'; +import {colors, ENABLE_COLOR} from '#cli'; import {withAggregate} from '#sugar'; function inspect(value) { @@ -174,7 +174,7 @@ function validateArrayItemsHelper(itemValidator) { throw new Error(`Expected validator to return true`); } } catch (error) { - error.message = `(index: ${color.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; + error.message = `(index: ${colors.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; throw error; } }; @@ -264,7 +264,7 @@ export function validateProperties(spec) { try { specValidator(value); } catch (error) { - error.message = `(key: ${color.green(specKey)}, value: ${inspect(value)}) ${error.message}`; + error.message = `(key: ${colors.green(specKey)}, value: ${inspect(value)}) ${error.message}`; throw error; } }); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 0ccef5ed..416b6c4e 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -1,13 +1,21 @@ import find from '#find'; import {isLanguageCode, isName, isURL} from '#validators'; -import Thing from './thing.js'; +import Thing, { + color, + flag, + name, + referenceList, + resolvedReferenceList, + simpleString, + wikiData, +} from './thing.js'; export class WikiInfo extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Wiki'), + name: name('Unnamed Wiki'), // Displayed in nav bar. nameShort: { @@ -20,12 +28,12 @@ export class WikiInfo extends Thing { }, }, - color: Thing.common.color(), + color: color(), // One-line description used for <meta rel="description"> tag. - description: Thing.common.simpleString(), + description: simpleString(), - footerContent: Thing.common.simpleString(), + footerContent: simpleString(), defaultLanguage: { flags: {update: true, expose: true}, @@ -37,22 +45,22 @@ export class WikiInfo extends Thing { update: {validate: isURL}, }, - divideTrackListsByGroupsByRef: Thing.common.referenceList(Group), + divideTrackListsByGroupsByRef: referenceList(Group), // Feature toggles - enableFlashesAndGames: Thing.common.flag(false), - enableListings: Thing.common.flag(false), - enableNews: Thing.common.flag(false), - enableArtTagUI: Thing.common.flag(false), - enableGroupUI: Thing.common.flag(false), + enableFlashesAndGames: flag(false), + enableListings: flag(false), + enableNews: flag(false), + enableArtTagUI: flag(false), + enableGroupUI: flag(false), // Update only - groupData: Thing.common.wikiData(Group), + groupData: wikiData(Group), // Expose only - divideTrackListsByGroups: Thing.common.resolvedReferenceList({ + divideTrackListsByGroups: resolvedReferenceList({ list: 'divideTrackListsByGroupsByRef', data: 'groupData', find: find.group, diff --git a/src/data/yaml.js b/src/data/yaml.js index 2ad2d41d..c0aad943 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -7,7 +7,7 @@ import {inspect as nodeInspect} from 'node:util'; import yaml from 'js-yaml'; -import {color, ENABLE_COLOR, logInfo, logWarn} from '#cli'; +import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import find, {bindFind} from '#find'; import {traverse} from '#node-utils'; import T from '#things'; @@ -137,7 +137,7 @@ function makeProcessDocument( const name = document[nameField]; error.message = name ? `(name: ${inspect(name)}) ${error.message}` - : `(${color.dim(`no name found`)}) ${error.message}`; + : `(${colors.dim(`no name found`)}) ${error.message}`; throw error; } }; @@ -195,7 +195,7 @@ function makeProcessDocument( const thing = Reflect.construct(thingClass, []); - withAggregate({message: `Errors applying ${color.green(thingClass.name)} properties`}, ({call}) => { + withAggregate({message: `Errors applying ${colors.green(thingClass.name)} properties`}, ({call}) => { for (const [property, value] of Object.entries(sourceProperties)) { call(() => (thing[property] = value)); } @@ -228,7 +228,7 @@ makeProcessDocument.FieldCombinationsError = class FieldCombinationsError extend makeProcessDocument.FieldCombinationError = class FieldCombinationError extends Error { constructor(fields, message) { const fieldNames = Object.keys(fields); - const combinePart = `Don't combine ${fieldNames.map(field => color.red(field)).join(', ')}`; + const combinePart = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; const messagePart = (typeof message === 'function' @@ -1009,7 +1009,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { } catch (error) { error.message += (error.message.includes('\n') ? '\n' : ' ') + - `(file: ${color.bright(color.blue(path.relative(dataPath, x.file)))})`; + `(file: ${colors.bright(colors.blue(path.relative(dataPath, x.file)))})`; throw error; } }; @@ -1032,7 +1032,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { // just without the callbacks. Thank you. const filterBlankDocuments = documents => { const aggregate = openAggregate({ - message: `Found blank documents - check for extra '${color.cyan(`---`)}'`, + message: `Found blank documents - check for extra '${colors.cyan(`---`)}'`, }); const filteredDocuments = @@ -1076,10 +1076,10 @@ export async function loadAndProcessDataDocuments({dataPath}) { if (count === 1) { const range = `#${start + 1}`; - parts.push(`${count} document (${color.yellow(range)}), `); + parts.push(`${count} document (${colors.yellow(range)}), `); } else { const range = `#${start + 1}-${end + 1}`; - parts.push(`${count} documents (${color.yellow(range)}), `); + parts.push(`${count} documents (${colors.yellow(range)}), `); } if (previous === null) { @@ -1089,7 +1089,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { } else { const previousDescription = Object.entries(previous).at(0).join(': '); const nextDescription = Object.entries(next).at(0).join(': '); - parts.push(`between "${color.cyan(previousDescription)}" and "${color.cyan(nextDescription)}"`); + parts.push(`between "${colors.cyan(previousDescription)}" and "${colors.cyan(nextDescription)}"`); } aggregate.push(new Error(parts.join(''))); @@ -1395,7 +1395,7 @@ export function filterDuplicateDirectories(wikiData) { const aggregate = openAggregate({message: `Duplicate directories found`}); for (const thingDataProp of deduplicateSpec) { const thingData = wikiData[thingDataProp]; - aggregate.nest({message: `Duplicate directories found in ${color.green('wikiData.' + thingDataProp)}`}, ({call}) => { + aggregate.nest({message: `Duplicate directories found in ${colors.green('wikiData.' + thingDataProp)}`}, ({call}) => { const directoryPlaces = Object.create(null); const duplicateDirectories = []; @@ -1421,7 +1421,7 @@ export function filterDuplicateDirectories(wikiData) { const places = directoryPlaces[directory]; call(() => { throw new Error( - `Duplicate directory ${color.green(directory)}:\n` + + `Duplicate directory ${colors.green(directory)}:\n` + places.map((thing) => ` - ` + inspect(thing)).join('\n') ); }); @@ -1516,7 +1516,7 @@ export function filterReferenceErrors(wikiData) { for (const [thingDataProp, providedProcessDocumentFn, propSpec] of referenceSpec) { const thingData = getNestedProp(wikiData, thingDataProp); - aggregate.nest({message: `Reference errors in ${color.green('wikiData.' + thingDataProp)}`}, ({nest}) => { + aggregate.nest({message: `Reference errors in ${colors.green('wikiData.' + thingDataProp)}`}, ({nest}) => { const things = Array.isArray(thingData) ? thingData : [thingData]; for (const thing of things) { @@ -1535,7 +1535,7 @@ export function filterReferenceErrors(wikiData) { const value = thing[property]; if (value === undefined) { - push(new TypeError(`Property ${color.red(property)} isn't valid for ${color.green(thing.constructor.name)}`)); + push(new TypeError(`Property ${colors.red(property)} isn't valid for ${colors.green(thing.constructor.name)}`)); continue; } @@ -1553,7 +1553,7 @@ export function filterReferenceErrors(wikiData) { // No need to check if the original exists here. Aliases are automatically // created from a field on the original, so the original certainly exists. const original = find.artist(alias.aliasedArtistRef, wikiData.artistData, {mode: 'quiet'}); - throw new Error(`Reference ${color.red(contribRef.who)} is to an alias, should be ${color.green(original.name)}`); + throw new Error(`Reference ${colors.red(contribRef.who)} is to an alias, should be ${colors.green(original.name)}`); } return boundFind.artist(contribRef.who); @@ -1578,12 +1578,12 @@ export function filterReferenceErrors(wikiData) { const shouldBeMessage = (originalByName - ? color.green(original.name) + ? colors.green(original.name) : original - ? color.green('track:' + original.directory) - : color.green(track.originalReleaseTrackByRef)); + ? colors.green('track:' + original.directory) + : colors.green(track.originalReleaseTrackByRef)); - throw new Error(`Reference ${color.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`); + throw new Error(`Reference ${colors.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`); } return track; @@ -1614,13 +1614,13 @@ export function filterReferenceErrors(wikiData) { const fieldPropertyMessage = (processDocumentFn?.propertyFieldMapping?.[property] - ? ` in field ${color.green(processDocumentFn.propertyFieldMapping[property])}` - : ` in property ${color.green(property)}`); + ? ` in field ${colors.green(processDocumentFn.propertyFieldMapping[property])}` + : ` in property ${colors.green(property)}`); const findFnMessage = (findFnKey.startsWith('_') ? `` - : ` (${color.green('find.' + findFnKey)})`); + : ` (${colors.green('find.' + findFnKey)})`); const errorMessage = (Array.isArray(value) diff --git a/src/find.js b/src/find.js index b8230800..5ad8dae7 100644 --- a/src/find.js +++ b/src/find.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color, logWarn} from '#cli'; +import {colors, logWarn} from '#cli'; function warnOrThrow(mode, message) { if (mode === 'error') { @@ -66,7 +66,7 @@ function findHelper(keys, findFns = {}) { const found = key ? byDirectory(ref, data, mode) : byName(ref, data, mode); if (!found) { - warnOrThrow(mode, `Didn't match anything for ${color.bright(fullRef)}`); + warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`); } cacheForThisData[fullRef] = found; @@ -102,7 +102,7 @@ function matchName(ref, data, mode) { if (ref !== thing.name) { warnOrThrow( mode, - `Bad capitalization: ${color.red(ref)} -> ${color.green(thing.name)}` + `Bad capitalization: ${colors.red(ref)} -> ${colors.green(thing.name)}` ); } diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 741cdff3..fafd17f6 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -94,7 +94,7 @@ import * as path from 'node:path'; import dimensionsOf from 'image-size'; import { - color, + colors, fileIssue, logError, logInfo, @@ -662,14 +662,14 @@ export async function verifyImagePaths(mediaPath, {urls, wikiData}) { if (!empty(missing)) { logWarn`** Some image files are missing! (${missing.length + ' files'}) **`; for (const file of missing) { - console.warn(color.yellow(` - `) + file); + console.warn(colors.yellow(` - `) + file); } } if (!empty(misplaced)) { logWarn`** Some image files are misplaced! (${misplaced.length + ' files'}) **`; for (const file of misplaced) { - console.warn(color.yellow(` - `) + file); + console.warn(colors.yellow(` - `) + file); } } } diff --git a/src/upd8.js b/src/upd8.js index 2ec231c9..f6091ca2 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -47,7 +47,7 @@ import {generateURLs, urlSpec} from '#urls'; import {sortByName} from '#wiki-data'; import { - color, + colors, decorateTime, logWarn, logInfo, @@ -279,7 +279,7 @@ async function main() { const indentWrap = (spaces, str) => wrap(str, {width: 60 - spaces, indent: ' '.repeat(spaces)}); const showOptions = (msg, options) => { - console.log(color.bright(msg)); + console.log(colors.bright(msg)); const entries = Object.entries(options); const sortedOptions = sortByName(entries @@ -310,13 +310,13 @@ async function main() { console.log(''); } - console.log(color.bright(` --` + name) + + console.log(colors.bright(` --` + name) + (aliases.length - ? ` (or: ${aliases.map(alias => color.bright(`--` + alias)).join(', ')})` + ? ` (or: ${aliases.map(alias => colors.bright(`--` + alias)).join(', ')})` : '') + (descriptor.help ? '' - : color.dim(' (no help provided)'))); + : colors.dim(' (no help provided)'))); if (wrappedHelp) { console.log(wrappedHelp); @@ -336,7 +336,7 @@ async function main() { }; console.log( - color.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) + + colors.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) + `static wiki software cataloguing collaborative creation\n`); console.log(indentWrap(0, @@ -496,7 +496,7 @@ async function main() { { const logThings = (thingDataProp, label) => - logInfo` - ${wikiData[thingDataProp]?.length ?? color.red('(Missing!)')} ${color.normal(color.dim(label))}`; + logInfo` - ${wikiData[thingDataProp]?.length ?? colors.red('(Missing!)')} ${colors.normal(colors.dim(label))}`; try { logInfo`Loaded data and processed objects:`; logThings('albumData', 'albums'); diff --git a/src/util/cli.js b/src/util/cli.js index e8c8c79f..9f2b35ab 100644 --- a/src/util/cli.js +++ b/src/util/cli.js @@ -17,7 +17,7 @@ export const ENABLE_COLOR = const C = (n) => ENABLE_COLOR ? (text) => `\x1b[${n}m${text}\x1b[0m` : (text) => text; -export const color = { +export const colors = { bright: C('1'), dim: C('2'), normal: C('22'), diff --git a/src/util/sugar.js b/src/util/sugar.js index 1ba3f3ae..ebb7d61e 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -6,7 +6,7 @@ // It will likely only do exactly what I want it to, and only in the cases I // decided were relevant enough to 8other handling. -import {color} from './cli.js'; +import {colors} from './cli.js'; // Apparently JavaScript doesn't come with a function to split an array into // chunks! Weird. Anyway, this is an awesome place to use a generator, even @@ -566,10 +566,10 @@ export function showAggregate(topError, { .trim() .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match)) : '(no stack trace)'; - header += ` ${color.dim(tracePart)}`; + header += ` ${colors.dim(tracePart)}`; } - const bar = level % 2 === 0 ? '\u2502' : color.dim('\u254e'); - const head = level % 2 === 0 ? '\u257f' : color.dim('\u257f'); + const bar = level % 2 === 0 ? '\u2502' : colors.dim('\u254e'); + const head = level % 2 === 0 ? '\u257f' : colors.dim('\u257f'); if (error instanceof AggregateError) { return ( @@ -605,7 +605,7 @@ export function decorateErrorWithIndex(fn) { try { return fn(x, index, array); } catch (error) { - error.message = `(${color.yellow(`#${index + 1}`)}) ${error.message}`; + error.message = `(${colors.yellow(`#${index + 1}`)}) ${error.message}`; throw error; } }; -- cgit 1.3.0-6-gf8a5 From d33effa272c3388640974648fe2888a284c6701c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 14:50:41 -0300 Subject: data: withAlbum: perform proper availability check on album --- src/data/things/composite.js | 5 +++-- src/data/things/track.js | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fd52aa0f..29f5770c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -853,8 +853,9 @@ export function exposeConstant({ // consider using instead. Customize {mode} to select one of these modes, // or leave unset and default to 'null': // -// * 'null': Check that the value isn't null. +// * 'null': Check that the value isn't null (and not undefined either). // * 'empty': Check that the value is neither null nor an empty array. +// This will outright error for undefined. // * 'falsy': Check that the value isn't false when treated as a boolean // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! @@ -879,7 +880,7 @@ export function withResultOfAvailabilityCheck({ const checkAvailability = (value, mode) => { switch (mode) { - case 'null': return value !== null; + case 'null': return value !== null && value !== undefined; case 'empty': return !empty(value); case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); default: return false; diff --git a/src/data/things/track.js b/src/data/things/track.js index 41c92092..fcfd39c7 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -411,21 +411,34 @@ function withAlbum({ }), }, + withResultOfAvailabilityCheck({ + fromDependency: '#album', + mode: 'null', + into: '#albumAvailability', + }), + { - dependencies: ['#album'], + dependencies: ['#albumAvailability'], options: {notFoundMode}, mapContinuation: {into}, compute: ({ - '#album': album, + '#albumAvailability': albumAvailability, '#options': {notFoundMode}, }, continuation) => - (album - ? continuation.raise({into: album}) + (albumAvailability + ? continuation() : (notFoundMode === 'exit' ? continuation.exit(null) : continuation.raise({into: null}))), }, + + { + dependencies: ['#album'], + mapContinuation: {into}, + compute: ({'#album': album}, continuation) => + continuation({into: album}), + }, ]); } -- cgit 1.3.0-6-gf8a5 From 9db4b91c66f8b9b98d098bfe446e29f5b3caee53 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 14:53:25 -0300 Subject: data: withResolvedContribs: use default "into" --- src/data/things/thing.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19f00b3e..9d8b2ea2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -286,12 +286,8 @@ export function resolvedReference({ref, data, find}) { // properly.) export function dynamicContribs(contribsByRefProperty) { return compositeFrom(`dynamicContribs`, [ - withResolvedContribs({ - from: contribsByRefProperty, - into: '#contribs', - }), - - exposeDependency({dependency: '#contribs'}), + withResolvedContribs({from: contribsByRefProperty}), + exposeDependency({dependency: '#resolvedContribs'}), ]); } @@ -368,7 +364,10 @@ export function commentatorArtists(){ // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({from, into}) { +export function withResolvedContribs({ + from, + into = '#resolvedContribs', +}) { return compositeFrom(`withResolvedContribs`, [ raiseWithoutDependency({ dependency: from, -- cgit 1.3.0-6-gf8a5 From a24a72339f6e6e416a797d869fe9c4d9057fcac0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 17:23:54 -0300 Subject: data: custom _homepageSourceGroup reference validation function --- src/data/yaml.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index c0aad943..8aca3299 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1490,7 +1490,7 @@ export function filterReferenceErrors(wikiData) { }], ['homepageLayout.rows', undefined, { - sourceGroupByRef: 'group', + sourceGroupByRef: '_homepageSourceGroup', sourceAlbumsByRef: 'album', }], @@ -1560,6 +1560,16 @@ export function filterReferenceErrors(wikiData) { }; break; + case '_homepageSourceGroup': + findFn = groupRef => { + if (groupRef === 'new-additions' || groupRef === 'new-releases') { + return true; + } + + return boundFind.group(groupRef); + }; + break; + case '_trackNotRerelease': findFn = trackRef => { const track = find.track(trackRef, wikiData.trackData, {mode: 'error'}); -- cgit 1.3.0-6-gf8a5 From c18844784bd1c0ead7c49d0519727b7a92e23e13 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 17:24:26 -0300 Subject: repl: expose CacheableObject in repl --- src/repl.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/repl.js b/src/repl.js index 9ab4ddf0..ead01567 100644 --- a/src/repl.js +++ b/src/repl.js @@ -11,7 +11,7 @@ import {generateURLs, urlSpec} from '#urls'; import {quickLoadAllFromYAML} from '#yaml'; import _find, {bindFind} from '#find'; -import thingConstructors from '#things'; +import thingConstructors, {CacheableObject} from '#things'; import * as serialize from '#serialize'; import * as sugar from '#sugar'; import * as wikiDataUtils from '#wiki-data'; @@ -63,6 +63,7 @@ export async function getContextAssignments({ WD: wikiData, ...thingConstructors, + CacheableObject, language, ...sugar, -- cgit 1.3.0-6-gf8a5 From bbccaf51222cb4bed73466164496f5bc1030292c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 7 Sep 2023 17:30:54 -0300 Subject: data: roll paired "byRef" and "dynamic" properties into one --- .../dependencies/generateWikiHomeAlbumsRow.js | 2 +- src/data/things/album.js | 75 +++++------ src/data/things/artist.js | 19 +-- src/data/things/cacheable-object.js | 26 ++-- src/data/things/composite.js | 15 +++ src/data/things/flash.js | 34 ++--- src/data/things/group.js | 27 ++-- src/data/things/homepage-layout.js | 60 ++++++--- src/data/things/thing.js | 147 +++++++++++---------- src/data/things/track.js | 132 ++++++++---------- src/data/things/validators.js | 2 +- src/data/things/wiki-info.js | 15 +-- src/data/yaml.js | 133 ++++++++++--------- src/gen-thumbs.js | 9 +- 14 files changed, 341 insertions(+), 355 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateWikiHomeAlbumsRow.js b/src/content/dependencies/generateWikiHomeAlbumsRow.js index 99c1be55..cb0860f5 100644 --- a/src/content/dependencies/generateWikiHomeAlbumsRow.js +++ b/src/content/dependencies/generateWikiHomeAlbumsRow.js @@ -16,7 +16,7 @@ export default { sprawl({albumData}, row) { const sprawl = {}; - switch (row.sourceGroupByRef) { + switch (row.sourceGroup) { case 'new-releases': sprawl.albums = getNewReleases(row.countAlbumsFromGroup, {albumData}); break; diff --git a/src/data/things/album.js b/src/data/things/album.js index 9cf58641..88308182 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -7,14 +7,12 @@ import Thing, { commentary, color, commentatorArtists, - contribsByRef, contribsPresent, + contributionList, directory, - dynamicContribs, fileExtension, flag, name, - resolvedReferenceList, referenceList, simpleDate, simpleString, @@ -43,25 +41,31 @@ export class Album extends Thing { update: {validate: isDate}, expose: { - dependencies: ['date', 'coverArtistContribsByRef'], - transform: (coverArtDate, { - coverArtistContribsByRef, - date, - }) => - (!empty(coverArtistContribsByRef) + dependencies: ['date', 'coverArtistContribs'], + transform: (coverArtDate, {coverArtistContribs, date}) => + (!empty(coverArtistContribs) ? coverArtDate ?? date ?? null : null), }, }, - artistContribsByRef: contribsByRef(), - coverArtistContribsByRef: contribsByRef(), - trackCoverArtistContribsByRef: contribsByRef(), - wallpaperArtistContribsByRef: contribsByRef(), - bannerArtistContribsByRef: contribsByRef(), + artistContribs: contributionList(), + coverArtistContribs: contributionList(), + trackCoverArtistContribs: contributionList(), + wallpaperArtistContribs: contributionList(), + bannerArtistContribs: contributionList(), - groupsByRef: referenceList(Group), - artTagsByRef: referenceList(ArtTag), + groups: referenceList({ + class: Group, + find: find.group, + data: 'groupData', + }), + + artTags: referenceList({ + class: ArtTag, + find: find.artTag, + data: 'artTagData', + }), trackSections: { flags: {update: true, expose: true}, @@ -84,13 +88,12 @@ export class Album extends Thing { isDefaultTrackSection: section.isDefaultTrackSection ?? false, startIndex: ( - startIndex += section.tracksByRef.length, - startIndex - section.tracksByRef.length + startIndex += section.tracks.length, + startIndex - section.tracks.length ), - tracksByRef: section.tracksByRef ?? [], tracks: - (trackData && section.tracksByRef + (trackData && section.tracks ?.map(ref => find.track(ref, trackData, {mode: 'quiet'})) .filter(Boolean)) ?? [], @@ -128,29 +131,11 @@ export class Album extends Thing { // Expose only - artistContribs: dynamicContribs('artistContribsByRef'), - coverArtistContribs: dynamicContribs('coverArtistContribsByRef'), - trackCoverArtistContribs: dynamicContribs('trackCoverArtistContribsByRef'), - wallpaperArtistContribs: dynamicContribs('wallpaperArtistContribsByRef'), - bannerArtistContribs: dynamicContribs('bannerArtistContribsByRef'), - commentatorArtists: commentatorArtists(), - groups: resolvedReferenceList({ - list: 'groupsByRef', - data: 'groupData', - find: find.group, - }), - - artTags: resolvedReferenceList({ - list: 'artTagsByRef', - data: 'artTagData', - find: find.artTag, - }), - - hasCoverArt: contribsPresent('coverArtistContribsByRef'), - hasWallpaperArt: contribsPresent('wallpaperArtistContribsByRef'), - hasBannerArt: contribsPresent('bannerArtistContribsByRef'), + hasCoverArt: contribsPresent('coverArtistContribs'), + hasWallpaperArt: contribsPresent('wallpaperArtistContribs'), + hasBannerArt: contribsPresent('bannerArtistContribs'), tracks: { flags: {expose: true}, @@ -158,12 +143,12 @@ export class Album extends Thing { expose: { dependencies: ['trackSections', 'trackData'], compute: ({trackSections, trackData}) => - trackSections && trackData + (trackSections && trackData ? trackSections - .flatMap((section) => section.tracksByRef ?? []) - .map((ref) => find.track(ref, trackData, {mode: 'quiet'})) + .flatMap(section => section.tracks ?? []) + .map(ref => find.track(ref, trackData, {mode: 'quiet'})) .filter(Boolean) - : [], + : []), }, }, }); diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 2676591a..7a9dbd3c 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -33,7 +33,12 @@ export class Artist extends Thing { }, isAlias: flag(), - aliasedArtistRef: singleReference(Artist), + + aliasedArtist: singleReference({ + class: Artist, + find: find.artist, + data: 'artistData', + }), // Update only @@ -44,18 +49,6 @@ export class Artist extends Thing { // Expose only - aliasedArtist: { - flags: {expose: true}, - - expose: { - dependencies: ['artistData', 'aliasedArtistRef'], - compute: ({artistData, aliasedArtistRef}) => - aliasedArtistRef && artistData - ? find.artist(aliasedArtistRef, artistData, {mode: 'quiet'}) - : null, - }, - }, - tracksAsArtist: Artist.filterByContrib('trackData', 'artistContribs'), tracksAsContributor: diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 92a46d66..4bc3668d 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -86,16 +86,14 @@ export default class CacheableObject { #propertyUpdateValues = Object.create(null); #propertyUpdateCacheInvalidators = Object.create(null); - /* - // Note the constructor doesn't take an initial data source. Due to a quirk - // of JavaScript, private members can't be accessed before the superclass's - // constructor is finished processing - so if we call the overridden - // update() function from inside this constructor, it will error when - // writing to private members. Pretty bad! - // - // That means initial data must be provided by following up with update() - // after constructing the new instance of the Thing (sub)class. - */ + // Note the constructor doesn't take an initial data source. Due to a quirk + // of JavaScript, private members can't be accessed before the superclass's + // constructor is finished processing - so if we call the overridden + // update() function from inside this constructor, it will error when + // writing to private members. Pretty bad! + // + // That means initial data must be provided by following up with update() + // after constructing the new instance of the Thing (sub)class. constructor() { this.#defineProperties(); @@ -352,4 +350,12 @@ export default class CacheableObject { console.log(` - ${line}`); } } + + static getUpdateValue(object, key) { + if (!Object.hasOwn(object, key)) { + return undefined; + } + + return object.#propertyUpdateValues[key] ?? null; + } } diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 29f5770c..96abf4af 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1071,3 +1071,18 @@ export function raiseWithoutUpdateValue({ }, ]); } + +export function withUpdateValueAsDependency({ + into = '#updateValue', +} = {}) { + return { + annotation: `withUpdateValueAsDependency`, + flags: {expose: true, compose: true}, + + expose: { + mapContinuation: {into}, + transform: (value, continuation) => + continuation(value, {into: value}), + }, + }; +} diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 4e640dac..eb16d29e 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,13 +9,11 @@ import { } from '#validators'; import Thing, { - dynamicContribs, color, - contribsByRef, + contributionList, fileExtension, name, referenceList, - resolvedReferenceList, simpleDate, simpleString, urls, @@ -60,9 +58,13 @@ export class Flash extends Thing { coverArtFileExtension: fileExtension('jpg'), - contributorContribsByRef: contribsByRef(), + contributorContribs: contributionList(), - featuredTracksByRef: referenceList(Track), + featuredTracks: referenceList({ + class: Track, + find: find.track, + data: 'trackData', + }), urls: urls(), @@ -74,14 +76,6 @@ export class Flash extends Thing { // Expose only - contributorContribs: dynamicContribs('contributorContribsByRef'), - - featuredTracks: resolvedReferenceList({ - list: 'featuredTracksByRef', - data: 'trackData', - find: find.track, - }), - act: { flags: {expose: true}, @@ -138,18 +132,14 @@ export class FlashAct extends Thing { } }, - flashesByRef: referenceList(Flash), + flashes: referenceList({ + class: Flash, + data: 'flashData', + find: find.flash, + }), // Update only flashData: wikiData(Flash), - - // Expose only - - flashes: resolvedReferenceList({ - list: 'flashesByRef', - data: 'flashData', - find: find.flash, - }), }) } diff --git a/src/data/things/group.js b/src/data/things/group.js index 873c6d88..f53fa48e 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -5,7 +5,6 @@ import Thing, { directory, name, referenceList, - resolvedReferenceList, simpleString, urls, wikiData, @@ -24,7 +23,11 @@ export class Group extends Thing { urls: urls(), - featuredAlbumsByRef: referenceList(Album), + featuredAlbums: referenceList({ + class: Album, + find: find.album, + data: 'albumData', + }), // Update only @@ -33,12 +36,6 @@ export class Group extends Thing { // Expose only - featuredAlbums: resolvedReferenceList({ - list: 'featuredAlbumsByRef', - data: 'albumData', - find: find.album, - }), - descriptionShort: { flags: {expose: true}, @@ -89,18 +86,14 @@ export class GroupCategory extends Thing { name: name('Unnamed Group Category'), color: color(), - groupsByRef: referenceList(Group), + groups: referenceList({ + class: Group, + find: find.group, + data: 'groupData', + }), // Update only groupData: wikiData(Group), - - // Expose only - - groups: resolvedReferenceList({ - list: 'groupsByRef', - data: 'groupData', - find: find.group, - }), }); } diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index ab6f4cff..b509c1e2 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,23 +1,29 @@ import find from '#find'; +import { + compositeFrom, + exposeDependency, + withUpdateValueAsDependency, +} from '#composite'; + import { is, isCountingNumber, isString, isStringNonEmpty, + oneOf, validateArrayItems, validateInstanceOf, + validateReference, } from '#validators'; import Thing, { color, name, referenceList, - resolvedReference, - resolvedReferenceList, simpleString, - singleReference, wikiData, + withResolvedReference, } from './thing.js'; export class HomepageLayout extends Thing { @@ -101,8 +107,38 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }, }, - sourceGroupByRef: singleReference(Group), - sourceAlbumsByRef: referenceList(Album), + sourceGroup: compositeFrom(`HomepageLayoutAlbumsRow.sourceGroup`, [ + { + transform: (value, continuation) => + (value === 'new-releases' || value === 'new-additions' + ? value + : continuation(value)), + }, + + withUpdateValueAsDependency(), + + withResolvedReference({ + ref: '#updateValue', + data: 'groupData', + find: find.group, + }), + + exposeDependency({ + dependency: '#resolvedReference', + update: { + validate: + oneOf( + is('new-releases', 'new-additions'), + validateReference(Group[Thing.referenceType])), + }, + }), + ]), + + sourceAlbums: referenceList({ + class: Album, + find: find.album, + data: 'albumData', + }), countAlbumsFromGroup: { flags: {update: true, expose: true}, @@ -113,19 +149,5 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { flags: {update: true, expose: true}, update: {validate: validateArrayItems(isString)}, }, - - // Expose only - - sourceGroup: resolvedReference({ - ref: 'sourceGroupByRef', - data: 'groupData', - find: find.group, - }), - - sourceAlbums: resolvedReferenceList({ - list: 'sourceAlbumsByRef', - data: 'albumData', - find: find.album, - }), }); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9d8b2ea2..91ad96af 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -11,8 +11,11 @@ import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { compositeFrom, exitWithoutDependency, + exposeConstant, exposeDependency, + exposeDependencyOrContinue, raiseWithoutDependency, + withUpdateValueAsDependency, } from '#composite'; import { @@ -162,22 +165,31 @@ export function externalFunction() { }; } -// Super simple "contributions by reference" list, used for a variety of -// properties (Artists, Cover Artists, etc). This is the property which is -// externally provided, in the form: +// Strong 'n sturdy contribution list, rolling a list of references (provided +// as this property's update value) and the resolved results (as get exposed) +// into one property. Update value will look something like this: // -// [ -// {who: 'Artist Name', what: 'Viola'}, -// {who: 'artist:john-cena', what: null}, -// ... -// ] +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] // -// ...processed from YAML, spreadsheet, or any other kind of input. -export function contribsByRef() { - return { - flags: {update: true, expose: true}, - update: {validate: isContributionList}, - }; +// ...typically as processed from YAML, spreadsheet, or elsewhere. +// Exposes as the same, but with the "who" replaced with matches found in +// artistData - which means this always depends on an `artistData` property +// also existing on this object! +// +export function contributionList() { + return compositeFrom(`contributionList`, [ + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue'}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({ + value: [], + update: {validate: isContributionList}, + }), + ]); } // Artist commentary! Generally present on tracks and albums. @@ -222,88 +234,77 @@ export function additionalFiles() { // 'artist' or 'track', but this utility keeps from having to hard-code the // string in multiple places by referencing the value saved on the class // instead. -export function referenceList(thingClass) { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); +export function referenceList({ + class: thingClass, + data, + find, +}) { + if (!thingClass) { + throw new TypeError(`Expected a Thing class`); } - return { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList(referenceType)}, - }; -} - -// Corresponding function for a single reference. -export function singleReference(thingClass) { const {[Thing.referenceType]: referenceType} = thingClass; if (!referenceType) { throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return { - flags: {update: true, expose: true}, - update: {validate: validateReference(referenceType)}, - }; -} + return compositeFrom(`referenceList`, [ + withUpdateValueAsDependency(), -// Corresponding dynamic property to referenceList, which takes the values -// in the provided property and searches the specified wiki data for -// matching actual Thing-subclass objects. -export function resolvedReferenceList({list, data, find}) { - return compositeFrom(`resolvedReferenceList`, [ withResolvedReferenceList({ - list, data, find, + data, find, + list: '#updateValue', notFoundMode: 'filter', }), - exposeDependency({dependency: '#resolvedReferenceList'}), + exposeDependency({ + dependency: '#resolvedReferenceList', + update: { + validate: validateReferenceList(referenceType), + }, + }), ]); } // Corresponding function for a single reference. -export function resolvedReference({ref, data, find}) { - return compositeFrom(`resolvedReference`, [ - withResolvedReference({ref, data, find}), - exposeDependency({dependency: '#resolvedReference'}), - ]); -} +export function singleReference({ + class: thingClass, + data, + find, +}) { + if (!thingClass) { + throw new TypeError(`Expected a Thing class`); + } -// Corresponding dynamic property to contribsByRef, which takes the values -// in the provided property and searches the object's artistData for -// matching actual Artist objects. The computed structure has the same form -// as contribsByRef, but with Artist objects instead of string references: -// -// [ -// {who: (an Artist), what: 'Viola'}, -// {who: (an Artist), what: null}, -// ... -// ] -// -// Contributions whose "who" values don't match anything in artistData are -// filtered out. (So if the list is all empty, chances are that either the -// reference list is somehow messed up, or artistData isn't being provided -// properly.) -export function dynamicContribs(contribsByRefProperty) { - return compositeFrom(`dynamicContribs`, [ - withResolvedContribs({from: contribsByRefProperty}), - exposeDependency({dependency: '#resolvedContribs'}), + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } + + return compositeFrom(`singleReference`, [ + withUpdateValueAsDependency(), + + withResolvedReference({ref: '#updateValue', data, find}), + + exposeDependency({ + dependency: '#resolvedReference', + update: { + validate: validateReference(referenceType), + }, + }), ]); } // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent(contribsByRefProperty) { +export function contribsPresent(contribsProperty) { return { flags: {expose: true}, expose: { - dependencies: [contribsByRefProperty], - compute({ - [contribsByRefProperty]: contribsByRef, - }) { - return !empty(contribsByRef); - }, - } + dependencies: [contribsProperty], + compute: ({[contribsProperty]: contribs}) => + !empty(contribs), + }, }; } @@ -380,13 +381,13 @@ export function withResolvedContribs({ mapDependencies: {from}, compute: ({from}, continuation) => continuation({ - '#whoByRef': from.map(({who}) => who), + '#artistRefs': from.map(({who}) => who), '#what': from.map(({what}) => what), }), }, withResolvedReferenceList({ - list: '#whoByRef', + list: '#artistRefs', data: 'artistData', into: '#who', find: find.artist, diff --git a/src/data/things/track.js b/src/data/things/track.js index fcfd39c7..8263d399 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -3,7 +3,6 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import find from '#find'; import {empty} from '#sugar'; -import {isColor, isDate, isDuration, isFileExtension} from '#validators'; import { compositeFrom, @@ -13,20 +12,28 @@ import { exposeDependencyOrContinue, exposeUpdateValueOrContinue, withResultOfAvailabilityCheck, + withUpdateValueAsDependency, } from '#composite'; +import { + isColor, + isContributionList, + isDate, + isDuration, + isFileExtension, +} from '#validators'; + +import CacheableObject from './cacheable-object.js'; + import Thing, { additionalFiles, commentary, commentatorArtists, - contribsByRef, + contributionList, directory, - dynamicContribs, flag, name, referenceList, - resolvedReference, - resolvedReferenceList, reverseReferenceList, simpleDate, singleReference, @@ -55,13 +62,11 @@ export class Track extends Thing { urls: urls(), dateFirstReleased: simpleDate(), - artistContribsByRef: contribsByRef(), - contributorContribsByRef: contribsByRef(), - coverArtistContribsByRef: contribsByRef(), - - referencedTracksByRef: referenceList(Track), - sampledTracksByRef: referenceList(Track), - artTagsByRef: referenceList(ArtTag), + artTags: referenceList({ + class: ArtTag, + find: find.artTag, + data: 'artTagData', + }), color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), @@ -134,9 +139,24 @@ export class Track extends Thing { }), ]), - originalReleaseTrackByRef: singleReference(Track), + originalReleaseTrack: singleReference({ + class: Track, + find: find.track, + data: 'trackData', + }), - dataSourceAlbumByRef: singleReference(Album), + // Note - this is an internal property used only to help identify a track. + // It should not be assumed in general that the album and dataSourceAlbum match + // (i.e. a track may dynamically be moved from one album to another, at + // which point dataSourceAlbum refers to where it was originally from, and is + // not generally relevant information). It's also not guaranteed that + // dataSourceAlbum is available (depending on the Track creator to optionally + // provide this property's update value). + dataSourceAlbum: singleReference({ + class: Album, + find: find.album, + data: 'albumData', + }), commentary: commentary(), lyrics: simpleString(), @@ -161,19 +181,6 @@ export class Track extends Thing { exposeDependency({dependency: '#album'}), ]), - // Note - this is an internal property used only to help identify a track. - // It should not be assumed in general that the album and dataSourceAlbum match - // (i.e. a track may dynamically be moved from one album to another, at - // which point dataSourceAlbum refers to where it was originally from, and is - // not generally relevant information). It's also not guaranteed that - // dataSourceAlbum is available (depending on the Track creator to optionally - // provide dataSourceAlbumByRef). - dataSourceAlbum: resolvedReference({ - ref: 'dataSourceAlbumByRef', - data: 'albumData', - find: find.album, - }), - date: compositeFrom(`Track.date`, [ exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), withAlbumProperty({property: 'date'}), @@ -192,11 +199,6 @@ export class Track extends Thing { exposeDependency({dependency: '#hasUniqueCoverArt'}), ]), - originalReleaseTrack: compositeFrom(`Track.originalReleaseTrack`, [ - withOriginalRelease(), - exposeDependency({dependency: '#originalRelease'}), - ]), - otherReleases: compositeFrom(`Track.otherReleases`, [ exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), withOriginalRelease({selfIfOriginal: true}), @@ -224,26 +226,20 @@ export class Track extends Thing { artistContribs: compositeFrom(`Track.artistContribs`, [ inheritFromOriginalRelease({property: 'artistContribs'}), - withResolvedContribs({ - from: 'artistContribsByRef', - into: '#artistContribs', - }), - - { - dependencies: ['#artistContribs'], - compute: ({'#artistContribs': contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : contribsFromTrack), - }, + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue', into: '#artistContribs'}), + exposeDependencyOrContinue({dependency: '#artistContribs'}), withAlbumProperty({property: 'artistContribs'}), - exposeDependency({dependency: '#album.artistContribs'}), + exposeDependency({ + dependency: '#album.artistContribs', + update: {validate: isContributionList}, + }), ]), contributorContribs: compositeFrom(`Track.contributorContribs`, [ inheritFromOriginalRelease({property: 'contributorContribs'}), - dynamicContribs('contributorContribsByRef'), + contributionList(), ]), // Cover artists aren't inherited from the original release, since it @@ -258,47 +254,35 @@ export class Track extends Thing { : continuation()), }, - withResolvedContribs({ - from: 'coverArtistContribsByRef', - into: '#coverArtistContribs', - }), - - { - dependencies: ['#coverArtistContribs'], - compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : contribsFromTrack), - }, + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}), + exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), withAlbumProperty({property: 'trackCoverArtistContribs'}), - exposeDependency({dependency: '#album.trackCoverArtistContribs'}), + exposeDependency({ + dependency: '#album.trackCoverArtistContribs', + update: {validate: isContributionList}, + }), ]), referencedTracks: compositeFrom(`Track.referencedTracks`, [ inheritFromOriginalRelease({property: 'referencedTracks'}), - resolvedReferenceList({ - list: 'referencedTracksByRef', - data: 'trackData', + referenceList({ + class: Track, find: find.track, + data: 'trackData', }), ]), sampledTracks: compositeFrom(`Track.sampledTracks`, [ inheritFromOriginalRelease({property: 'sampledTracks'}), - resolvedReferenceList({ - list: 'sampledTracksByRef', - data: 'trackData', + referenceList({ + class: Track, find: find.track, + data: 'trackData', }), ]), - artTags: resolvedReferenceList({ - list: 'artTagsByRef', - data: 'artTagData', - find: find.artTag, - }), - // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't // generally relevant from the perspective of the tracks being referenced. @@ -327,7 +311,7 @@ export class Track extends Thing { parts.push(Thing.prototype[inspect.custom].apply(this)); - if (this.originalReleaseTrackByRef) { + if (CacheableObject.getUpdateValue(this, 'originalReleaseTrack')) { parts.unshift(`${colors.yellow('[rerelease]')} `); } @@ -564,7 +548,7 @@ function withOriginalRelease({ } = {}) { return compositeFrom(`withOriginalRelease`, [ withResolvedReference({ - ref: 'originalReleaseTrackByRef', + ref: 'originalReleaseTrack', data: 'trackData', into: '#originalRelease', find: find.track, @@ -607,7 +591,7 @@ function withHasUniqueCoverArt({ }, withResolvedContribs({ - from: 'coverArtistContribsByRef', + from: 'coverArtistContribs', into: '#coverArtistContribs', }), diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 4c8f683b..f0d1d9fd 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -308,7 +308,7 @@ export const isTrackSection = validateProperties({ color: optional(isColor), dateOriginallyReleased: optional(isDate), isDefaultTrackSection: optional(isBoolean), - tracksByRef: optional(validateReferenceList('track')), + tracks: optional(validateReferenceList('track')), }); export const isTrackSectionList = validateArrayItems(isTrackSection); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 416b6c4e..7c2de324 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -6,7 +6,6 @@ import Thing, { flag, name, referenceList, - resolvedReferenceList, simpleString, wikiData, } from './thing.js'; @@ -45,7 +44,11 @@ export class WikiInfo extends Thing { update: {validate: isURL}, }, - divideTrackListsByGroupsByRef: referenceList(Group), + divideTrackListsByGroups: referenceList({ + class: Group, + find: find.group, + data: 'groupData', + }), // Feature toggles enableFlashesAndGames: flag(false), @@ -57,13 +60,5 @@ export class WikiInfo extends Thing { // Update only groupData: wikiData(Group), - - // Expose only - - divideTrackListsByGroups: resolvedReferenceList({ - list: 'divideTrackListsByGroupsByRef', - data: 'groupData', - find: find.group, - }), }); } diff --git a/src/data/yaml.js b/src/data/yaml.js index 8aca3299..e1e5803d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -10,7 +10,7 @@ import yaml from 'js-yaml'; import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import find, {bindFind} from '#find'; import {traverse} from '#node-utils'; -import T from '#things'; +import T, {CacheableObject, Thing} from '#things'; import { conditionallySuppressError, @@ -278,11 +278,11 @@ export const processAlbumDocument = makeProcessDocument(T.Album, { coverArtFileExtension: 'Cover Art File Extension', trackCoverArtFileExtension: 'Track Art File Extension', - wallpaperArtistContribsByRef: 'Wallpaper Artists', + wallpaperArtistContribs: 'Wallpaper Artists', wallpaperStyle: 'Wallpaper Style', wallpaperFileExtension: 'Wallpaper File Extension', - bannerArtistContribsByRef: 'Banner Artists', + bannerArtistContribs: 'Banner Artists', bannerStyle: 'Banner Style', bannerFileExtension: 'Banner File Extension', bannerDimensions: 'Banner Dimensions', @@ -290,11 +290,11 @@ export const processAlbumDocument = makeProcessDocument(T.Album, { commentary: 'Commentary', additionalFiles: 'Additional Files', - artistContribsByRef: 'Artists', - coverArtistContribsByRef: 'Cover Artists', - trackCoverArtistContribsByRef: 'Default Track Cover Artists', - groupsByRef: 'Groups', - artTagsByRef: 'Art Tags', + artistContribs: 'Artists', + coverArtistContribs: 'Cover Artists', + trackCoverArtistContribs: 'Default Track Cover Artists', + groups: 'Groups', + artTags: 'Art Tags', }, }); @@ -348,13 +348,13 @@ export const processTrackDocument = makeProcessDocument(T.Track, { sheetMusicFiles: 'Sheet Music Files', midiProjectFiles: 'MIDI Project Files', - originalReleaseTrackByRef: 'Originally Released As', - referencedTracksByRef: 'Referenced Tracks', - sampledTracksByRef: 'Sampled Tracks', - artistContribsByRef: 'Artists', - contributorContribsByRef: 'Contributors', - coverArtistContribsByRef: 'Cover Artists', - artTagsByRef: 'Art Tags', + originalReleaseTrack: 'Originally Released As', + referencedTracks: 'Referenced Tracks', + sampledTracks: 'Sampled Tracks', + artistContribs: 'Artists', + contributorContribs: 'Contributors', + coverArtistContribs: 'Cover Artists', + artTags: 'Art Tags', }, invalidFieldCombinations: [ @@ -424,8 +424,8 @@ export const processFlashDocument = makeProcessDocument(T.Flash, { date: 'Date', coverArtFileExtension: 'Cover Art File Extension', - featuredTracksByRef: 'Featured Tracks', - contributorContribsByRef: 'Contributors', + featuredTracks: 'Featured Tracks', + contributorContribs: 'Contributors', }, }); @@ -470,7 +470,7 @@ export const processGroupDocument = makeProcessDocument(T.Group, { description: 'Description', urls: 'URLs', - featuredAlbumsByRef: 'Featured Albums', + featuredAlbums: 'Featured Albums', }, }); @@ -501,7 +501,7 @@ export const processWikiInfoDocument = makeProcessDocument(T.WikiInfo, { footerContent: 'Footer Content', defaultLanguage: 'Default Language', canonicalBase: 'Canonical Base', - divideTrackListsByGroupsByRef: 'Divide Track Lists By Groups', + divideTrackListsByGroups: 'Divide Track Lists By Groups', enableFlashesAndGames: 'Enable Flashes & Games', enableListings: 'Enable Listings', enableNews: 'Enable News', @@ -536,9 +536,9 @@ export const homepageLayoutRowTypeProcessMapping = { albums: makeProcessHomepageLayoutRowDocument(T.HomepageLayoutAlbumsRow, { propertyFieldMapping: { displayStyle: 'Display Style', - sourceGroupByRef: 'Group', + sourceGroup: 'Group', countAlbumsFromGroup: 'Count', - sourceAlbumsByRef: 'Albums', + sourceAlbums: 'Albums', actionLinks: 'Actions', }, }), @@ -771,13 +771,13 @@ export const dataSteps = [ let currentTrackSection = { name: `Default Track Section`, isDefaultTrackSection: true, - tracksByRef: [], + tracks: [], }; - const albumRef = T.Thing.getReference(album); + const albumRef = Thing.getReference(album); const closeCurrentTrackSection = () => { - if (!empty(currentTrackSection.tracksByRef)) { + if (!empty(currentTrackSection.tracks)) { trackSections.push(currentTrackSection); } }; @@ -791,7 +791,7 @@ export const dataSteps = [ color: entry.color, dateOriginallyReleased: entry.dateOriginallyReleased, isDefaultTrackSection: false, - tracksByRef: [], + tracks: [], }; continue; @@ -799,9 +799,9 @@ export const dataSteps = [ trackData.push(entry); - entry.dataSourceAlbumByRef = albumRef; + entry.dataSourceAlbum = albumRef; - currentTrackSection.tracksByRef.push(T.Thing.getReference(entry)); + currentTrackSection.tracks.push(Thing.getReference(entry)); } closeCurrentTrackSection(); @@ -825,12 +825,12 @@ export const dataSteps = [ const artistData = results; const artistAliasData = results.flatMap((artist) => { - const origRef = T.Thing.getReference(artist); + const origRef = Thing.getReference(artist); return artist.aliasNames?.map((name) => { const alias = new T.Artist(); alias.name = name; alias.isAlias = true; - alias.aliasedArtistRef = origRef; + alias.aliasedArtist = origRef; alias.artistData = artistData; return alias; }) ?? []; @@ -854,7 +854,7 @@ export const dataSteps = [ save(results) { let flashAct; - let flashesByRef = []; + let flashRefs = []; if (results[0] && !(results[0] instanceof T.FlashAct)) { throw new Error(`Expected an act at top of flash data file`); @@ -863,18 +863,18 @@ export const dataSteps = [ for (const thing of results) { if (thing instanceof T.FlashAct) { if (flashAct) { - Object.assign(flashAct, {flashesByRef}); + Object.assign(flashAct, {flashes: flashRefs}); } flashAct = thing; - flashesByRef = []; + flashRefs = []; } else { - flashesByRef.push(T.Thing.getReference(thing)); + flashRefs.push(Thing.getReference(thing)); } } if (flashAct) { - Object.assign(flashAct, {flashesByRef}); + Object.assign(flashAct, {flashes: flashRefs}); } const flashData = results.filter((x) => x instanceof T.Flash); @@ -897,7 +897,7 @@ export const dataSteps = [ save(results) { let groupCategory; - let groupsByRef = []; + let groupRefs = []; if (results[0] && !(results[0] instanceof T.GroupCategory)) { throw new Error(`Expected a category at top of group data file`); @@ -906,18 +906,18 @@ export const dataSteps = [ for (const thing of results) { if (thing instanceof T.GroupCategory) { if (groupCategory) { - Object.assign(groupCategory, {groupsByRef}); + Object.assign(groupCategory, {groups: groupRefs}); } groupCategory = thing; - groupsByRef = []; + groupRefs = []; } else { - groupsByRef.push(T.Thing.getReference(thing)); + groupRefs.push(Thing.getReference(thing)); } } if (groupCategory) { - Object.assign(groupCategory, {groupsByRef}); + Object.assign(groupCategory, {groups: groupRefs}); } const groupData = results.filter((x) => x instanceof T.Group); @@ -1462,45 +1462,45 @@ export function filterDuplicateDirectories(wikiData) { export function filterReferenceErrors(wikiData) { const referenceSpec = [ ['wikiInfo', processWikiInfoDocument, { - divideTrackListsByGroupsByRef: 'group', + divideTrackListsByGroups: 'group', }], ['albumData', processAlbumDocument, { - artistContribsByRef: '_contrib', - coverArtistContribsByRef: '_contrib', - trackCoverArtistContribsByRef: '_contrib', - wallpaperArtistContribsByRef: '_contrib', - bannerArtistContribsByRef: '_contrib', - groupsByRef: 'group', - artTagsByRef: 'artTag', + artistContribs: '_contrib', + coverArtistContribs: '_contrib', + trackCoverArtistContribs: '_contrib', + wallpaperArtistContribs: '_contrib', + bannerArtistContribs: '_contrib', + groups: 'group', + artTags: 'artTag', }], ['trackData', processTrackDocument, { - artistContribsByRef: '_contrib', - contributorContribsByRef: '_contrib', - coverArtistContribsByRef: '_contrib', - referencedTracksByRef: '_trackNotRerelease', - sampledTracksByRef: '_trackNotRerelease', - artTagsByRef: 'artTag', - originalReleaseTrackByRef: '_trackNotRerelease', + artistContribs: '_contrib', + contributorContribs: '_contrib', + coverArtistContribs: '_contrib', + referencedTracks: '_trackNotRerelease', + sampledTracks: '_trackNotRerelease', + artTags: 'artTag', + originalReleaseTrack: '_trackNotRerelease', }], ['groupCategoryData', processGroupCategoryDocument, { - groupsByRef: 'group', + groups: 'group', }], ['homepageLayout.rows', undefined, { - sourceGroupByRef: '_homepageSourceGroup', - sourceAlbumsByRef: 'album', + sourceGroup: '_homepageSourceGroup', + sourceAlbums: 'album', }], ['flashData', processFlashDocument, { - contributorContribsByRef: '_contrib', - featuredTracksByRef: 'track', + contributorContribs: '_contrib', + featuredTracks: 'track', }], ['flashActData', processFlashActDocument, { - flashesByRef: 'flash', + flashes: 'flash', }], ]; @@ -1532,7 +1532,7 @@ export function filterReferenceErrors(wikiData) { nest({message: `Reference errors in ${inspect(thing)}`}, ({push, filter}) => { for (const [property, findFnKey] of Object.entries(propSpec)) { - const value = thing[property]; + const value = CacheableObject.getUpdateValue(thing, property); if (value === undefined) { push(new TypeError(`Property ${colors.red(property)} isn't valid for ${colors.green(thing.constructor.name)}`)); @@ -1552,7 +1552,7 @@ export function filterReferenceErrors(wikiData) { if (alias) { // No need to check if the original exists here. Aliases are automatically // created from a field on the original, so the original certainly exists. - const original = find.artist(alias.aliasedArtistRef, wikiData.artistData, {mode: 'quiet'}); + const original = alias.aliasedArtist; throw new Error(`Reference ${colors.red(contribRef.who)} is to an alias, should be ${colors.green(original.name)}`); } @@ -1573,12 +1573,13 @@ export function filterReferenceErrors(wikiData) { case '_trackNotRerelease': findFn = trackRef => { const track = find.track(trackRef, wikiData.trackData, {mode: 'error'}); + const originalRef = track && CacheableObject.getUpdateValue(track, 'originalReleaseTrack'); - if (track?.originalReleaseTrackByRef) { + if (originalRef) { // It's possible for the original to not actually exist, in this case. // It should still be reported since the 'Originally Released As' field // was present. - const original = find.track(track.originalReleaseTrackByRef, wikiData.trackData, {mode: 'quiet'}); + const original = find.track(originalRef, wikiData.trackData, {mode: 'quiet'}); // Prefer references by name, but only if it's unambiguous. const originalByName = @@ -1591,7 +1592,7 @@ export function filterReferenceErrors(wikiData) { ? colors.green(original.name) : original ? colors.green('track:' + original.directory) - : colors.green(track.originalReleaseTrackByRef)); + : colors.green(originalRef)); throw new Error(`Reference ${colors.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`); } @@ -1606,7 +1607,7 @@ export function filterReferenceErrors(wikiData) { } const suppress = fn => conditionallySuppressError(error => { - if (property === 'sampledTracksByRef') { + 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 diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index fafd17f6..4977ade7 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -93,6 +93,9 @@ import * as path from 'node:path'; import dimensionsOf from 'image-size'; +import {delay, empty, queue} from '#sugar'; +import {CacheableObject} from '#things'; + import { colors, fileIssue, @@ -110,8 +113,6 @@ import { traverse, } from '#node-utils'; -import {delay, empty, queue} from '#sugar'; - export const defaultMagickThreads = 8; export function getThumbnailsAvailableForDimensions([width, height]) { @@ -608,8 +609,8 @@ export function getExpectedImagePaths(mediaPath, {urls, wikiData}) { wikiData.albumData .flatMap(album => [ album.hasCoverArt && fromRoot.to('media.albumCover', album.directory, album.coverArtFileExtension), - !empty(album.bannerArtistContribsByRef) && fromRoot.to('media.albumBanner', album.directory, album.bannerFileExtension), - !empty(album.wallpaperArtistContribsByRef) && fromRoot.to('media.albumWallpaper', album.directory, album.wallpaperFileExtension), + !empty(CacheableObject.getUpdateValue(album, 'bannerArtistContribs')) && fromRoot.to('media.albumBanner', album.directory, album.bannerFileExtension), + !empty(CacheableObject.getUpdateValue(album, 'wallpaperArtistContribs')) && fromRoot.to('media.albumWallpaper', album.directory, album.wallpaperFileExtension), ]) .filter(Boolean), -- cgit 1.3.0-6-gf8a5 From 19b6cbd1cde35399c0ecdee5459c5a4946e84299 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 08:40:03 -0300 Subject: util: fix missing color -> colors rename in cli utils --- src/util/cli.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/util/cli.js b/src/util/cli.js index 9f2b35ab..4c08c085 100644 --- a/src/util/cli.js +++ b/src/util/cli.js @@ -335,8 +335,8 @@ export function fileIssue({ topMessage = `This shouldn't happen.`, } = {}) { if (topMessage) { - console.error(color.red(`${topMessage} Please let the HSMusic developers know:`)); + console.error(colors.red(`${topMessage} Please let the HSMusic developers know:`)); } - console.error(color.red(`- https://hsmusic.wiki/feedback/`)); - console.error(color.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`)); + console.error(colors.red(`- https://hsmusic.wiki/feedback/`)); + console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`)); } -- cgit 1.3.0-6-gf8a5 From 745322c71c2443a631533ad7ef651ea30ad87a7c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 08:45:30 -0300 Subject: util: remove unused getPagePathnameAcrossLanguages util --- src/util/urls.js | 21 --------------------- src/write/build-modes/live-dev-server.js | 8 -------- src/write/build-modes/static-build.js | 9 --------- 3 files changed, 38 deletions(-) (limited to 'src') diff --git a/src/util/urls.js b/src/util/urls.js index d2b303e9..11b9b8b0 100644 --- a/src/util/urls.js +++ b/src/util/urls.js @@ -237,27 +237,6 @@ export function getPagePathname({ : to('localized.' + pagePath[0], ...pagePath.slice(1))); } -export function getPagePathnameAcrossLanguages({ - defaultLanguage, - languages, - pagePath, - urls, -}) { - return withEntries(languages, entries => entries - .filter(([key, language]) => key !== 'default' && !language.hidden) - .map(([_key, language]) => [ - language.code, - getPagePathname({ - baseDirectory: - (language === defaultLanguage - ? '' - : language.code), - pagePath, - urls, - }), - ])); -} - // Needed for the rare path arguments which themselves contains one or more // slashes, e.g. for listings, with arguments like 'albums/by-name'. export function getPageSubdirectoryPrefix({ diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 9889b3f0..94f0962c 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -14,7 +14,6 @@ import {empty} from '#sugar'; import { getPagePathname, - getPagePathnameAcrossLanguages, getURLsFrom, getURLsFromRoot, } from '#urls'; @@ -332,13 +331,6 @@ export async function go({ return; } - const localizedPathnames = getPagePathnameAcrossLanguages({ - defaultLanguage, - languages, - pagePath: servePath, - urls, - }); - const bound = bindUtilities({ absoluteTo, cachebust, diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 82a947c7..c10342e9 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -21,13 +21,11 @@ import { logError, logInfo, logWarn, - progressCallAll, progressPromiseAll, } from '#cli'; import { getPagePathname, - getPagePathnameAcrossLanguages, getURLsFrom, getURLsFromRoot, } from '#urls'; @@ -270,13 +268,6 @@ export async function go({ ...pageWrites.map(page => () => { const pagePath = page.path; - const localizedPathnames = getPagePathnameAcrossLanguages({ - defaultLanguage, - languages, - pagePath, - urls, - }); - const pathname = getPagePathname({ baseDirectory, pagePath, -- cgit 1.3.0-6-gf8a5 From 40729920984ca1362f1672b1307bf7aa32687107 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 08:49:54 -0300 Subject: infra, content: use watchPath variable where appropriate --- src/content/dependencies/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index 57025a5d..65241faa 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -77,12 +77,12 @@ export function watchContentDependencies({ // prematurely find out there aren't any nulls - before the nulls have // been entered at all!). - readdir(metaDirname).then(files => { + readdir(watchPath).then(files => { if (closed) { return; } - const filePaths = files.map(file => path.join(metaDirname, file)); + const filePaths = files.map(file => path.join(watchPath, file)); for (const filePath of filePaths) { if (filePath === metaPath) continue; const functionName = getFunctionName(filePath); @@ -91,7 +91,7 @@ export function watchContentDependencies({ } } - const watcher = chokidar.watch(metaDirname); + const watcher = chokidar.watch(watchPath); watcher.on('all', (event, filePath) => { if (!['add', 'change'].includes(event)) return; -- cgit 1.3.0-6-gf8a5 From 21a270ca6efa561cad3e87048cf8deb8a166d55f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 08:51:20 -0300 Subject: fix miscellaneous eslint errors --- src/content/dependencies/index.js | 1 - src/listing-spec.js | 12 +----------- src/page/flash.js | 2 -- src/util/html.js | 2 +- src/util/replacer.js | 1 - src/util/wiki-data.js | 2 +- src/write/build-modes/live-dev-server.js | 1 - src/write/build-modes/static-build.js | 6 ------ 8 files changed, 3 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index 65241faa..71802050 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -30,7 +30,6 @@ export function watchContentDependencies({ const contentDependencies = {}; let emittedReady = false; - let allDependenciesFulfilled = false; let closed = false; let _close = () => {}; diff --git a/src/listing-spec.js b/src/listing-spec.js index fe36fc01..2b33744a 100644 --- a/src/listing-spec.js +++ b/src/listing-spec.js @@ -1,14 +1,4 @@ -import {accumulateSum, empty, showAggregate} from '#sugar'; - -import { - chunkByProperties, - getArtistNumContributions, - getTotalDuration, - sortAlphabetically, - sortByDate, - sortChronologically, - sortFlashesChronologically, -} from '#wiki-data'; +import {empty, showAggregate} from '#sugar'; const listingSpec = []; diff --git a/src/page/flash.js b/src/page/flash.js index b9d27d0f..7df74158 100644 --- a/src/page/flash.js +++ b/src/page/flash.js @@ -1,5 +1,3 @@ -import {empty} from '#sugar'; - export const description = `flash & game pages`; export function condition({wikiData}) { diff --git a/src/util/html.js b/src/util/html.js index a311bbba..b1668558 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -242,7 +242,7 @@ export class Tag { this.selfClosing && !(value === null || value === undefined || - !Boolean(value) || + !value || Array.isArray(value) && value.filter(Boolean).length === 0) ) { throw new Error(`Tag <${this.tagName}> is self-closing but got content`); diff --git a/src/util/replacer.js b/src/util/replacer.js index c5289cc5..647d1f0e 100644 --- a/src/util/replacer.js +++ b/src/util/replacer.js @@ -5,7 +5,6 @@ // function, which converts nodes parsed here into actual HTML, links, etc // for embedding in a wiki webpage. -import {logError, logWarn} from '#cli'; import * as html from '#html'; import {escapeRegex} from '#sugar'; diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index ad2f82fb..0eab2204 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -1,6 +1,6 @@ // Utility functions for interacting with wiki data. -import {accumulateSum, empty, stitchArrays, unique} from './sugar.js'; +import {accumulateSum, empty, unique} from './sugar.js'; // Generic value operations diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 94f0962c..644efdbc 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -10,7 +10,6 @@ import {quickEvaluate} from '#content-function'; import * as html from '#html'; import * as pageSpecs from '#page-specs'; import {serializeThings} from '#serialize'; -import {empty} from '#sugar'; import { getPagePathname, diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index c10342e9..6ef69759 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -91,7 +91,6 @@ export async function go({ srcRootPath, thumbsCache, urls, - urlSpec, wikiData, cachebust, @@ -469,14 +468,9 @@ async function writeFavicon({ } async function writeSharedFilesAndPages({ - language, outputPath, - urls, - wikiData, wikiDataJSON, }) { - const {groupData, wikiInfo} = wikiData; - return progressPromiseAll(`Writing files & pages shared across languages.`, [ wikiDataJSON && writeFile( -- cgit 1.3.0-6-gf8a5 From ee46a4f78f1bfc8348834fbd3349849148f178a8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 10:54:58 -0300 Subject: data: Album.coverArt{Date,FileExtension}: depend on contribs --- src/data/things/album.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 88308182..3726a463 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -2,6 +2,13 @@ import find from '#find'; import {empty} from '#sugar'; import {isDate, isDimensions, isTrackSectionList} from '#validators'; +import { + compositeFrom, + exitWithoutDependency, + exposeDependency, + exposeUpdateValueOrContinue, +} from '#composite'; + import Thing, { additionalFiles, commentary, @@ -18,6 +25,7 @@ import Thing, { simpleString, urls, wikiData, + withResolvedContribs, } from './thing.js'; export class Album extends Thing { @@ -35,19 +43,16 @@ export class Album extends Thing { trackArtDate: simpleDate(), dateAddedToWiki: simpleDate(), - coverArtDate: { - flags: {update: true, expose: true}, + coverArtDate: compositeFrom(`Album.coverArtDate`, [ + withResolvedContribs({from: 'coverArtistContribs'}), + exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), - update: {validate: isDate}, - - expose: { - dependencies: ['date', 'coverArtistContribs'], - transform: (coverArtDate, {coverArtistContribs, date}) => - (!empty(coverArtistContribs) - ? coverArtDate ?? date ?? null - : null), - }, - }, + exposeUpdateValueOrContinue(), + exposeDependency({ + dependency: 'date', + update: {validate: isDate}, + }), + ]), artistContribs: contributionList(), coverArtistContribs: contributionList(), @@ -102,7 +107,12 @@ export class Album extends Thing { }, }, - coverArtFileExtension: fileExtension('jpg'), + coverArtFileExtension: compositeFrom(`Album.coverArtFileExtension`, [ + withResolvedContribs({from: 'coverArtistContribs'}), + exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), + fileExtension('jpg'), + ]), + trackCoverArtFileExtension: fileExtension('jpg'), wallpaperStyle: simpleString(), -- cgit 1.3.0-6-gf8a5 From 3ebe98d51d94a3e5277d65b2a4d2b5b433449214 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 11:29:53 -0300 Subject: data: withResolvedReferenceList: handle undefined matches --- src/data/things/thing.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 91ad96af..79d8ae0e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -491,19 +491,20 @@ export function withResolvedReferenceList({ let matches = list.map(ref => findFunction(ref, data, {mode: 'quiet'})); - if (!matches.includes(null)) { + if (matches.every(match => match)) { return continuation.raise({matches}); } switch (notFoundMode) { case 'filter': - matches = matches.filter(value => value !== null); + matches = matches.filter(match => match); return continuation.raise({matches}); case 'exit': return continuation.exit([]); case 'null': + matches = matches.map(match => match ?? null); return continuation.raise({matches}); } }, -- cgit 1.3.0-6-gf8a5 From bf0be010c9d9b860ad42762fc2e373130c7535eb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 11:32:50 -0300 Subject: data: update Album.tracks --- src/data/things/album.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 3726a463..76e0f638 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -26,6 +26,7 @@ import Thing, { urls, wikiData, withResolvedContribs, + withResolvedReferenceList, } from './thing.js'; export class Album extends Thing { @@ -147,20 +148,30 @@ export class Album extends Thing { hasWallpaperArt: contribsPresent('wallpaperArtistContribs'), hasBannerArt: contribsPresent('bannerArtistContribs'), - tracks: { - flags: {expose: true}, + tracks: compositeFrom(`Album.tracks`, [ + exitWithoutDependency({ + dependency: 'trackSections', + mode: 'empty', + value: [], + }), - expose: { + { dependencies: ['trackSections', 'trackData'], - compute: ({trackSections, trackData}) => - (trackSections && trackData - ? trackSections - .flatMap(section => section.tracks ?? []) - .map(ref => find.track(ref, trackData, {mode: 'quiet'})) - .filter(Boolean) - : []), + compute: ({trackSections, trackData}, continuation) => + continuation({ + '#trackRefs': trackSections + .flatMap(section => section.tracks ?? []), + }), }, - }, + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + find: find.track, + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ]), }); static [Thing.getSerializeDescriptors] = ({ -- cgit 1.3.0-6-gf8a5 From 65260d7fc2790ece0c13820ba18bc821163f164e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 12:19:49 -0300 Subject: data: new withFlattenedArray, withUnflattenedArray utilities --- src/data/things/composite.js | 75 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 96abf4af..1f6482f6 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1072,6 +1072,8 @@ export function raiseWithoutUpdateValue({ ]); } +// Turns an updating property's update value into a dependency, so it can be +// conveniently passed to other functions. export function withUpdateValueAsDependency({ into = '#updateValue', } = {}) { @@ -1086,3 +1088,76 @@ export function withUpdateValueAsDependency({ }, }; } + +// Flattens an array with one level of nested arrays, providing as dependencies +// both the flattened array as well as the original starting indices of each +// successive source array. +export function withFlattenedArray({ + from, + into = '#flattenedArray', + intoIndices = '#flattenedIndices', +}) { + return { + annotation: `withFlattenedArray`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {from}, + mapContinuation: {into, intoIndices}, + + compute({from: sourceArray}, continuation) { + const into = sourceArray.flat(); + const intoIndices = []; + + let lastEndIndex = 0; + for (const {length} of sourceArray) { + intoIndices.push(lastEndIndex); + lastEndIndex += length; + } + + return continuation({into, intoIndices}); + }, + }, + }; +} + +// After mapping the contents of a flattened array in-place (being careful to +// retain the original indices by replacing unmatched results with null instead +// of filtering them out), this function allows for recombining them. It will +// filter out null and undefined items by default (pass {filter: false} to +// disable this). +export function withUnflattenedArray({ + from, + fromIndices = '#flattenedIndices', + into = '#unflattenedArray', + filter = true, +}) { + return { + annotation: `withUnflattenedArray`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {from, fromIndices}, + mapContinuation: {into}, + compute({from, fromIndices}, continuation) { + const arrays = []; + + for (let i = 0; i < fromIndices.length; i++) { + const startIndex = fromIndices[i]; + const endIndex = + (i === fromIndices.length - 1 + ? from.length + : fromIndices[i + 1]); + + const values = from.slice(startIndex, endIndex); + arrays.push( + (filter + ? values.filter(value => value !== null && value !== undefined) + : values)); + } + + return continuation({into: arrays}); + }, + }, + }; +} -- cgit 1.3.0-6-gf8a5 From 4ed5649e83e344615eb0e710c7a942d0dea8fa22 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 12:35:22 -0300 Subject: data: update Album.trackSections --- src/data/things/album.js | 121 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 76e0f638..01f52c2d 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,12 +1,15 @@ import find from '#find'; -import {empty} from '#sugar'; +import {stitchArrays} from '#sugar'; import {isDate, isDimensions, isTrackSectionList} from '#validators'; import { compositeFrom, exitWithoutDependency, + exitWithoutUpdateValue, exposeDependency, exposeUpdateValueOrContinue, + withFlattenedArray, + withUnflattenedArray, } from '#composite'; import Thing, { @@ -73,40 +76,87 @@ export class Album extends Thing { data: 'artTagData', }), - trackSections: { - flags: {update: true, expose: true}, + trackSections: compositeFrom(`Album.trackSections`, [ + exitWithoutDependency({dependency: 'trackData', value: []}), + exitWithoutUpdateValue({value: [], mode: 'empty'}), - update: { - validate: isTrackSectionList, + { + transform: (trackSections, continuation) => + continuation(trackSections, { + '#sectionTrackRefs': + trackSections.map(section => section.tracks), + + '#sectionDateOriginallyReleased': + trackSections + .map(({dateOriginallyReleased}) => dateOriginallyReleased ?? null), + + '#sectionIsDefaultTrackSection': + trackSections + .map(({isDefaultTrackSection}) => isDefaultTrackSection ?? false), + }), }, - expose: { - dependencies: ['color', 'trackData'], - transform(trackSections, { - color: albumColor, - trackData, - }) { - let startIndex = 0; - return trackSections?.map(section => ({ - name: section.name ?? null, - color: section.color ?? albumColor ?? null, - dateOriginallyReleased: section.dateOriginallyReleased ?? null, - isDefaultTrackSection: section.isDefaultTrackSection ?? false, - - startIndex: ( - startIndex += section.tracks.length, - startIndex - section.tracks.length - ), - - tracks: - (trackData && section.tracks - ?.map(ref => find.track(ref, trackData, {mode: 'quiet'})) - .filter(Boolean)) ?? - [], - })); + { + dependencies: ['color'], + transform: (trackSections, {color: albumColor}, continuation) => + continuation(trackSections, { + '#sectionColor': + trackSections + .map(({color: sectionColor}) => sectionColor ?? albumColor), + }), + }, + + withFlattenedArray({ + from: '#sectionTrackRefs', + into: '#trackRefs', + intoIndices: '#sectionStartIndex', + }), + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + mode: 'null', + find: find.track, + into: '#tracks', + }), + + withUnflattenedArray({ + from: '#tracks', + fromIndices: '#sectionStartIndex', + into: '#sectionTracks', + }), + + { + flags: {update: true, expose: true}, + + update: {validate: isTrackSectionList}, + + expose: { + dependencies: [ + '#sectionTracks', + '#sectionColor', + '#sectionDateOriginallyReleased', + '#sectionIsDefaultTrackSection', + '#sectionStartIndex', + ], + + transform: (trackSections, { + '#sectionTracks': tracks, + '#sectionColor': color, + '#sectionDateOriginallyReleased': dateOriginallyReleased, + '#sectionIsDefaultTrackSection': isDefaultTrackSection, + '#sectionStartIndex': startIndex, + }) => + stitchArrays({ + tracks, + color, + dateOriginallyReleased, + isDefaultTrackSection, + startIndex, + }), }, }, - }, + ]), coverArtFileExtension: compositeFrom(`Album.coverArtFileExtension`, [ withResolvedContribs({from: 'coverArtistContribs'}), @@ -149,15 +199,12 @@ export class Album extends Thing { hasBannerArt: contribsPresent('bannerArtistContribs'), tracks: compositeFrom(`Album.tracks`, [ - exitWithoutDependency({ - dependency: 'trackSections', - mode: 'empty', - value: [], - }), + exitWithoutDependency({dependency: 'trackData', value: []}), + exitWithoutDependency({dependency: 'trackSections', mode: 'empty', value: []}), { - dependencies: ['trackSections', 'trackData'], - compute: ({trackSections, trackData}, continuation) => + dependencies: ['trackSections'], + compute: ({trackSections}, continuation) => continuation({ '#trackRefs': trackSections .flatMap(section => section.tracks ?? []), -- cgit 1.3.0-6-gf8a5 From 15b0b5422a3de8da52e14666909418405bdb8c39 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 16:09:16 -0300 Subject: data: update commentatorArtists --- src/data/things/thing.js | 55 +++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 79d8ae0e..9e7f940f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import find from '#find'; -import {empty, stitchArrays} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { @@ -334,29 +334,40 @@ export function wikiData(thingClass) { // This one's kinda tricky: it parses artist "references" from the // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. -export function commentatorArtists(){ - return { - flags: {expose: true}, +export function commentatorArtists() { + return compositeFrom(`commentatorArtists`, [ + exitWithoutDependency({dependency: 'commentary', mode: 'falsy', value: []}), - expose: { - dependencies: ['artistData', 'commentary'], - - compute: ({artistData, commentary}) => - artistData && commentary - ? Array.from( - new Set( - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/<i>(?<who>.*?):<\/i>/g) - ).map(({groups: {who}}) => - find.artist(who, artistData, {mode: 'quiet'}) - ) - ) - ) - : [], + { + dependencies: ['commentary'], + compute: ({commentary}, continuation) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/<i>(?<who>.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), }, - }; + + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + into: '#artists', + find: find.artist, + }), + + { + flags: {expose: true}, + + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), + }, + }, + ]); } // Compositional utilities -- cgit 1.3.0-6-gf8a5 From f39164ed44fe5c86f1f1911514d38a5549e51f92 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 16:17:05 -0300 Subject: data: rearrange track properties --- src/data/things/track.js | 153 +++++++++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 78 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 8263d399..0cd39dca 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -62,12 +62,6 @@ export class Track extends Thing { urls: urls(), dateFirstReleased: simpleDate(), - artTags: referenceList({ - class: ArtTag, - find: find.artTag, - data: 'artTagData', - }), - color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), withContainingTrackSection(), @@ -139,90 +133,28 @@ export class Track extends Thing { }), ]), + commentary: commentary(), + lyrics: simpleString(), + + additionalFiles: additionalFiles(), + sheetMusicFiles: additionalFiles(), + midiProjectFiles: additionalFiles(), + originalReleaseTrack: singleReference({ class: Track, find: find.track, data: 'trackData', }), - // Note - this is an internal property used only to help identify a track. - // It should not be assumed in general that the album and dataSourceAlbum match - // (i.e. a track may dynamically be moved from one album to another, at - // which point dataSourceAlbum refers to where it was originally from, and is - // not generally relevant information). It's also not guaranteed that - // dataSourceAlbum is available (depending on the Track creator to optionally - // provide this property's update value). + // 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: Album, find: find.album, data: 'albumData', }), - commentary: commentary(), - lyrics: simpleString(), - additionalFiles: additionalFiles(), - sheetMusicFiles: additionalFiles(), - midiProjectFiles: additionalFiles(), - - // Update only - - albumData: wikiData(Album), - artistData: wikiData(Artist), - artTagData: wikiData(ArtTag), - flashData: wikiData(Flash), - trackData: wikiData(Track), - - // Expose only - - commentatorArtists: commentatorArtists(), - - album: compositeFrom(`Track.album`, [ - withAlbum(), - exposeDependency({dependency: '#album'}), - ]), - - date: compositeFrom(`Track.date`, [ - exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), - withAlbumProperty({property: 'date'}), - exposeDependency({dependency: '#album.date'}), - ]), - - // Whether or not the track has "unique" cover artwork - a cover which is - // specifically associated with this track in particular, rather than with - // the track's album as a whole. This is typically used to select between - // displaying the track artwork and a fallback, such as the album artwork - // or a placeholder. (This property is named hasUniqueCoverArt instead of - // the usual hasCoverArt to emphasize that it does not inherit from the - // album.) - hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ - withHasUniqueCoverArt(), - exposeDependency({dependency: '#hasUniqueCoverArt'}), - ]), - - otherReleases: compositeFrom(`Track.otherReleases`, [ - exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), - withOriginalRelease({selfIfOriginal: true}), - - { - flags: {expose: true}, - expose: { - dependencies: ['this', 'trackData', '#originalRelease'], - compute: ({ - this: thisTrack, - trackData, - '#originalRelease': originalRelease, - }) => - (originalRelease === thisTrack - ? [] - : [originalRelease]) - .concat(trackData.filter(track => - track !== originalRelease && - track !== thisTrack && - track.originalReleaseTrack === originalRelease)), - }, - }, - ]), - artistContribs: compositeFrom(`Track.artistContribs`, [ inheritFromOriginalRelease({property: 'artistContribs'}), @@ -283,6 +215,71 @@ export class Track extends Thing { }), ]), + artTags: referenceList({ + class: ArtTag, + find: find.artTag, + data: 'artTagData', + }), + + // Update only + + albumData: wikiData(Album), + artistData: wikiData(Artist), + artTagData: wikiData(ArtTag), + flashData: wikiData(Flash), + trackData: wikiData(Track), + + // Expose only + + commentatorArtists: commentatorArtists(), + + album: compositeFrom(`Track.album`, [ + withAlbum(), + exposeDependency({dependency: '#album'}), + ]), + + date: compositeFrom(`Track.date`, [ + exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), + withAlbumProperty({property: 'date'}), + exposeDependency({dependency: '#album.date'}), + ]), + + // Whether or not the track has "unique" cover artwork - a cover which is + // specifically associated with this track in particular, rather than with + // the track's album as a whole. This is typically used to select between + // displaying the track artwork and a fallback, such as the album artwork + // or a placeholder. (This property is named hasUniqueCoverArt instead of + // the usual hasCoverArt to emphasize that it does not inherit from the + // album.) + hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ + withHasUniqueCoverArt(), + exposeDependency({dependency: '#hasUniqueCoverArt'}), + ]), + + otherReleases: compositeFrom(`Track.otherReleases`, [ + exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), + withOriginalRelease({selfIfOriginal: true}), + + { + flags: {expose: true}, + expose: { + dependencies: ['this', 'trackData', '#originalRelease'], + compute: ({ + this: thisTrack, + trackData, + '#originalRelease': originalRelease, + }) => + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), + }, + }, + ]), + // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't // generally relevant from the perspective of the tracks being referenced. -- cgit 1.3.0-6-gf8a5 From e01b73d286fbb11ac8ded59b4c23738dff195171 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 16:22:10 -0300 Subject: data: dimensions utility --- src/data/things/album.js | 8 +++----- src/data/things/thing.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 01f52c2d..5e281f06 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,6 +1,6 @@ import find from '#find'; import {stitchArrays} from '#sugar'; -import {isDate, isDimensions, isTrackSectionList} from '#validators'; +import {isDate, isTrackSectionList} from '#validators'; import { compositeFrom, @@ -19,6 +19,7 @@ import Thing, { commentatorArtists, contribsPresent, contributionList, + dimensions, directory, fileExtension, flag, @@ -171,10 +172,7 @@ export class Album extends Thing { bannerStyle: simpleString(), bannerFileExtension: fileExtension('jpg'), - bannerDimensions: { - flags: {update: true, expose: true}, - update: {validate: isDimensions}, - }, + bannerDimensions: dimensions(), hasTrackNumbers: flag(true), isListedOnHomepage: flag(true), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9e7f940f..0484b589 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -25,6 +25,7 @@ import { isColor, isContributionList, isDate, + isDimensions, isDirectory, isFileExtension, isName, @@ -122,6 +123,15 @@ export function fileExtension(defaultFileExtension = null) { }; } +// Plain ol' image dimensions. This is a two-item array of positive integers, +// corresponding to width and height respectively. +export function dimensions() { + return { + flags: {update: true, expose: true}, + update: {validate: isDimensions}, + }; +} + // Straightforward flag descriptor for a variety of property purposes. // Provide a default value, true or false! export function flag(defaultValue = false) { -- cgit 1.3.0-6-gf8a5 From cd3e2ae7384d82f0f2758beb0ae38ce0fe9f5e09 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 16:25:22 -0300 Subject: data: duration utility --- src/data/things/thing.js | 10 ++++++++++ src/data/things/track.js | 8 ++------ 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0484b589..169fc1ca 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -27,6 +27,7 @@ import { isDate, isDimensions, isDirectory, + isDuration, isFileExtension, isName, isString, @@ -132,6 +133,15 @@ export function dimensions() { }; } +// Duration! This is a number of seconds, possibly floating point, always +// at minimum zero. +export function duration() { + return { + flags: {update: true, expose: true}, + update: {validate: isDuration}, + }; +} + // Straightforward flag descriptor for a variety of property purposes. // Provide a default value, true or false! export function flag(defaultValue = false) { diff --git a/src/data/things/track.js b/src/data/things/track.js index 0cd39dca..53798cda 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -19,7 +19,6 @@ import { isColor, isContributionList, isDate, - isDuration, isFileExtension, } from '#validators'; @@ -31,6 +30,7 @@ import Thing, { commentatorArtists, contributionList, directory, + duration, flag, name, referenceList, @@ -54,11 +54,7 @@ export class Track extends Thing { name: name('Unnamed Track'), directory: directory(), - duration: { - flags: {update: true, expose: true}, - update: {validate: isDuration}, - }, - + duration: duration(), urls: urls(), dateFirstReleased: simpleDate(), -- cgit 1.3.0-6-gf8a5 From 6fe22802d8220b983a488f4efee1834bacbdb166 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 8 Sep 2023 17:20:48 -0300 Subject: data: cleaner withResolvedReferenceList notFoundMode implementation --- src/data/things/thing.js | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 169fc1ca..96ac9b12 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -514,29 +514,45 @@ export function withResolvedReferenceList({ }), { - options: {findFunction, notFoundMode}, mapDependencies: {list, data}, - mapContinuation: {matches: into}, + options: {findFunction}, - compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { - let matches = - list.map(ref => findFunction(ref, data, {mode: 'quiet'})); + compute: ({list, data, '#options': {findFunction}}, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, - if (matches.every(match => match)) { - return continuation.raise({matches}); - } + { + dependencies: ['#matches'], + mapContinuation: {into}, - switch (notFoundMode) { - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({matches}); + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({into: matches}) + : continuation()), + }, + + { + dependencies: ['#matches'], + options: {notFoundMode}, + mapContinuation: {into}, + compute({ + '#matches': matches, + '#options': {notFoundMode}, + }, continuation) { + switch (notFoundMode) { case 'exit': return continuation.exit([]); + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({into: matches}); + case 'null': matches = matches.map(match => match ?? null); - return continuation.raise({matches}); + return continuation.raise({into: matches}); } }, }, -- cgit 1.3.0-6-gf8a5 From c82784ebb4e5141bfe97664f3252303b3e833863 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 08:13:44 -0300 Subject: data: withPropertyFrom{Object,List}, fillMissingListItems utils --- src/data/things/album.js | 66 +++++++++------------- src/data/things/composite.js | 127 +++++++++++++++++++++++++++++++++++++++++++ src/data/things/thing.js | 11 +--- src/data/things/track.js | 16 +----- 4 files changed, 159 insertions(+), 61 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 5e281f06..288caa04 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -8,8 +8,11 @@ import { exitWithoutUpdateValue, exposeDependency, exposeUpdateValueOrContinue, + fillMissingListItems, withFlattenedArray, + withPropertyFromList, withUnflattenedArray, + withUpdateValueAsDependency, } from '#composite'; import Thing, { @@ -81,50 +84,35 @@ export class Album extends Thing { exitWithoutDependency({dependency: 'trackData', value: []}), exitWithoutUpdateValue({value: [], mode: 'empty'}), - { - transform: (trackSections, continuation) => - continuation(trackSections, { - '#sectionTrackRefs': - trackSections.map(section => section.tracks), - - '#sectionDateOriginallyReleased': - trackSections - .map(({dateOriginallyReleased}) => dateOriginallyReleased ?? null), - - '#sectionIsDefaultTrackSection': - trackSections - .map(({isDefaultTrackSection}) => isDefaultTrackSection ?? false), - }), - }, + withUpdateValueAsDependency({into: '#sections'}), - { - dependencies: ['color'], - transform: (trackSections, {color: albumColor}, continuation) => - continuation(trackSections, { - '#sectionColor': - trackSections - .map(({color: sectionColor}) => sectionColor ?? albumColor), - }), - }, + withPropertyFromList({list: '#sections', property: 'tracks', into: '#sections.trackRefs'}), + withPropertyFromList({list: '#sections', property: 'dateOriginallyReleased'}), + withPropertyFromList({list: '#sections', property: 'isDefaultTrackSection'}), + withPropertyFromList({list: '#sections', property: 'color'}), + + fillMissingListItems({list: '#sections.trackRefs', value: []}), + fillMissingListItems({list: '#sections.isDefaultTrackSection', value: false}), + fillMissingListItems({list: '#sections.color', dependency: 'color'}), withFlattenedArray({ - from: '#sectionTrackRefs', + from: '#sections.trackRefs', into: '#trackRefs', - intoIndices: '#sectionStartIndex', + intoIndices: '#sections.startIndex', }), withResolvedReferenceList({ list: '#trackRefs', data: 'trackData', - mode: 'null', + notFoundMode: 'null', find: find.track, into: '#tracks', }), withUnflattenedArray({ from: '#tracks', - fromIndices: '#sectionStartIndex', - into: '#sectionTracks', + fromIndices: '#sections.startIndex', + into: '#sections.tracks', }), { @@ -134,19 +122,19 @@ export class Album extends Thing { expose: { dependencies: [ - '#sectionTracks', - '#sectionColor', - '#sectionDateOriginallyReleased', - '#sectionIsDefaultTrackSection', - '#sectionStartIndex', + '#sections.tracks', + '#sections.color', + '#sections.dateOriginallyReleased', + '#sections.isDefaultTrackSection', + '#sections.startIndex', ], transform: (trackSections, { - '#sectionTracks': tracks, - '#sectionColor': color, - '#sectionDateOriginallyReleased': dateOriginallyReleased, - '#sectionIsDefaultTrackSection': isDefaultTrackSection, - '#sectionStartIndex': startIndex, + '#sections.tracks': tracks, + '#sections.color': color, + '#sections.dateOriginallyReleased': dateOriginallyReleased, + '#sections.isDefaultTrackSection': isDefaultTrackSection, + '#sections.startIndex': startIndex, }) => stitchArrays({ tracks, diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1f6482f6..a5adc3e9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1089,6 +1089,133 @@ export function withUpdateValueAsDependency({ }; } +// Gets a property of some object (in a dependency) and provides that value. +// If the object itself is null, the provided dependency will also always be +// null. By default, it'll also be null if the object doesn't have the listed +// property (or its value is undefined/null); provide a value on {missing} to +// default to something else here. +export function withPropertyFromObject({ + object, + property, + into = null, +}) { + into ??= + (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`); + + return { + annotation: `withPropertyFromObject`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {object}, + mapContinuation: {into}, + options: {property}, + + compute({object, '#options': {property}}, continuation) { + if (object === null || object === undefined) return continuation({into: null}); + if (!Object.hasOwn(object, property)) return continuation({into: null}); + return continuation({into: object[property] ?? null}); + }, + }, + }; +} + +// Gets a property from each of a list of objects (in a dependency) and +// provides the results. This doesn't alter any list indices, so positions +// which were null in the original list are kept null here. Objects which don't +// have the specified property are also included in-place as null, by default; +// provide a value on {missing} to default to something else here. +export function withPropertyFromList({ + list, + property, + into = null, + missing = null, +}) { + into ??= + (list.startsWith('#') + ? `${list}.${property}` + : `#${list}.${property}`); + + return { + annotation: `withPropertyFromList`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + mapContinuation: {into}, + options: {property}, + + compute({list, '#options': {property}}, continuation) { + if (list === undefined || empty(list)) { + return continuation({into: []}); + } + + return continuation({ + into: + list.map(item => { + if (item === null || item === undefined) return null; + if (!Object.hasOwn(item, property)) return missing; + return item[property] ?? missing; + }), + }); + }, + }, + }; +} + +// Replaces items of a list, which are null or undefined, with some fallback +// value, either a constant (set {value}) or from a dependency ({dependency}). +// By default, this replaces the passed dependency. +export function fillMissingListItems({ + list, + value, + dependency, + into = list, +}) { + if (value !== undefined && dependency !== undefined) { + throw new TypeError(`Don't provide both value and dependency`); + } + + if (value === undefined && dependency === undefined) { + throw new TypeError(`Missing value or dependency`); + } + + if (dependency) { + return { + annotation: `fillMissingListItems.fromDependency`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list, dependency}, + mapContinuation: {into}, + + compute: ({list, dependency}, continuation) => + continuation({ + into: list.map(item => item ?? dependency), + }), + }, + }; + } else { + return { + annotation: `fillMissingListItems.fromValue`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + mapContinuation: {into}, + options: {value}, + + compute: ({list, '#options': {value}}, continuation) => + continuation({ + into: list.map(item => item ?? value), + }), + }, + }; + } +} + // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 96ac9b12..a87e6ed6 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,6 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, + withPropertyFromList, withUpdateValueAsDependency, } from '#composite'; @@ -408,14 +409,8 @@ export function withResolvedContribs({ raise: {into: []}, }), - { - mapDependencies: {from}, - compute: ({from}, continuation) => - continuation({ - '#artistRefs': from.map(({who}) => who), - '#what': from.map(({what}) => what), - }), - }, + withPropertyFromList({list: from, property: 'who', into: '#artistRefs'}), + withPropertyFromList({list: from, property: 'what', into: '#what'}), withResolvedReferenceList({ list: '#artistRefs', diff --git a/src/data/things/track.js b/src/data/things/track.js index 53798cda..a307fda9 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -11,6 +11,7 @@ import { exposeDependency, exposeDependencyOrContinue, exposeUpdateValueOrContinue, + withPropertyFromObject, withResultOfAvailabilityCheck, withUpdateValueAsDependency, } from '#composite'; @@ -430,20 +431,7 @@ function withAlbumProperty({ }) { return compositeFrom(`withAlbumProperty`, [ withAlbum({notFoundMode}), - - { - dependencies: ['#album'], - options: {property}, - mapContinuation: {into}, - - compute: ({ - '#album': album, - '#options': {property}, - }, continuation) => - (album - ? continuation.raise({into: album[property]}) - : continuation.raise({into: null})), - }, + withPropertyFromObject({object: '#album', property, into}), ]); } -- cgit 1.3.0-6-gf8a5 From 57d07a308dfee6d16b49f7c009853b1789597e82 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 08:17:44 -0300 Subject: data: withAlbumProperty -> withPropertyFromAlbum Also remove withAlbumProperties, since it's not used anywhere and mostly serves as reference code. --- src/data/things/track.js | 59 ++++++++---------------------------------------- 1 file changed, 10 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index a307fda9..5e553b48 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -75,7 +75,7 @@ export class Track extends Thing { : continuation()), }, - withAlbumProperty({property: 'color'}), + withPropertyFromAlbum({property: 'color'}), exposeDependency({ dependency: '#album.color', @@ -103,7 +103,7 @@ export class Track extends Thing { exposeUpdateValueOrContinue(), // Expose album's trackCoverArtFileExtension if no update value set. - withAlbumProperty({property: 'trackCoverArtFileExtension'}), + withPropertyFromAlbum({property: 'trackCoverArtFileExtension'}), exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), // Fallback to 'jpg'. @@ -123,7 +123,7 @@ export class Track extends Thing { exposeUpdateValueOrContinue(), - withAlbumProperty({property: 'trackArtDate'}), + withPropertyFromAlbum({property: 'trackArtDate'}), exposeDependency({ dependency: '#album.trackArtDate', update: {validate: isDate}, @@ -159,7 +159,7 @@ export class Track extends Thing { withResolvedContribs({from: '#updateValue', into: '#artistContribs'}), exposeDependencyOrContinue({dependency: '#artistContribs'}), - withAlbumProperty({property: 'artistContribs'}), + withPropertyFromAlbum({property: 'artistContribs'}), exposeDependency({ dependency: '#album.artistContribs', update: {validate: isContributionList}, @@ -187,7 +187,7 @@ export class Track extends Thing { withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}), exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), - withAlbumProperty({property: 'trackCoverArtistContribs'}), + withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), exposeDependency({ dependency: '#album.trackCoverArtistContribs', update: {validate: isContributionList}, @@ -237,7 +237,7 @@ export class Track extends Thing { date: compositeFrom(`Track.date`, [ exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), - withAlbumProperty({property: 'date'}), + withPropertyFromAlbum({property: 'date'}), exposeDependency({dependency: '#album.date'}), ]), @@ -424,56 +424,17 @@ function withAlbum({ // property name prefixed with '#album.' (by default). If the track's album // isn't available, then by default, the property will be provided as null; // set {notFoundMode: 'exit'} to early exit instead. -function withAlbumProperty({ +function withPropertyFromAlbum({ property, into = '#album.' + property, notFoundMode = 'null', }) { - return compositeFrom(`withAlbumProperty`, [ + return compositeFrom(`withPropertyFromAlbum`, [ withAlbum({notFoundMode}), withPropertyFromObject({object: '#album', property, into}), ]); } -// Gets the listed properties from this track's album, providing them as -// dependencies (by default) with '#album.' prefixed before each property -// name. If the track's album isn't available, then by default, the same -// dependency names will be provided as null; set {notFoundMode: 'exit'} -// to early exit instead. -function withAlbumProperties({ - properties, - prefix = '#album', - notFoundMode = 'null', -}) { - return compositeFrom(`withAlbumProperties`, [ - withAlbum({notFoundMode}), - - { - dependencies: ['#album'], - options: {properties, prefix}, - - compute({ - '#album': album, - '#options': {properties, prefix}, - }, continuation) { - const raise = {}; - - if (album) { - for (const property of properties) { - raise[prefix + '.' + property] = album[property]; - } - } else { - for (const property of properties) { - raise[prefix + '.' + property] = null; - } - } - - return continuation.raise(raise); - }, - }, - ]); -} - // Gets the track section containing this track from its album's track list. // If notFoundMode is set to 'exit', this will early exit if the album can't be // found or if none of its trackSections includes the track for some reason. @@ -486,7 +447,7 @@ function withContainingTrackSection({ } return compositeFrom(`withContainingTrackSection`, [ - withAlbumProperty({property: 'trackSections', notFoundMode}), + withPropertyFromAlbum({property: 'trackSections', notFoundMode}), { dependencies: ['this', '#album.trackSections'], @@ -585,7 +546,7 @@ function withHasUniqueCoverArt({ : continuation.raise({into: true})), }, - withAlbumProperty({property: 'trackCoverArtistContribs'}), + withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), { dependencies: ['#album.trackCoverArtistContribs'], -- cgit 1.3.0-6-gf8a5 From 726118e7e8eefa9002562ca2dd0a4f6deb8a05b9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 08:26:33 -0300 Subject: data: refactor {missing} out of withPropertyFrom{Object,List} --- src/data/things/composite.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index a5adc3e9..b37b8e31 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1090,10 +1090,8 @@ export function withUpdateValueAsDependency({ } // Gets a property of some object (in a dependency) and provides that value. -// If the object itself is null, the provided dependency will also always be -// null. By default, it'll also be null if the object doesn't have the listed -// property (or its value is undefined/null); provide a value on {missing} to -// default to something else here. +// If the object itself is null, or the object doesn't have the listed property, +// the provided dependency will also be null. export function withPropertyFromObject({ object, property, @@ -1113,11 +1111,10 @@ export function withPropertyFromObject({ mapContinuation: {into}, options: {property}, - compute({object, '#options': {property}}, continuation) { - if (object === null || object === undefined) return continuation({into: null}); - if (!Object.hasOwn(object, property)) return continuation({into: null}); - return continuation({into: object[property] ?? null}); - }, + compute: ({object, '#options': {property}}, continuation) => + (object === null || object === undefined + ? continuation({into: null}) + : continuation({into: object[property] ?? null})), }, }; } @@ -1125,13 +1122,11 @@ export function withPropertyFromObject({ // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions // which were null in the original list are kept null here. Objects which don't -// have the specified property are also included in-place as null, by default; -// provide a value on {missing} to default to something else here. +// have the specified property are retained in-place as null. export function withPropertyFromList({ list, property, into = null, - missing = null, }) { into ??= (list.startsWith('#') @@ -1154,11 +1149,10 @@ export function withPropertyFromList({ return continuation({ into: - list.map(item => { - if (item === null || item === undefined) return null; - if (!Object.hasOwn(item, property)) return missing; - return item[property] ?? missing; - }), + list.map(item => + (item === null || item === undefined + ? null + : item[property] ?? null)), }); }, }, -- cgit 1.3.0-6-gf8a5 From 7a21c665d888b0db4c47c72049f7649bf1dabcde Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 08:47:38 -0300 Subject: data: withPropertiesFrom{Object,List} --- src/data/things/album.js | 19 +++++++----- src/data/things/composite.js | 72 ++++++++++++++++++++++++++++++++++++++++++++ src/data/things/thing.js | 17 ++++++----- 3 files changed, 94 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 288caa04..e11d0909 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -10,7 +10,7 @@ import { exposeUpdateValueOrContinue, fillMissingListItems, withFlattenedArray, - withPropertyFromList, + withPropertiesFromList, withUnflattenedArray, withUpdateValueAsDependency, } from '#composite'; @@ -86,17 +86,22 @@ export class Album extends Thing { withUpdateValueAsDependency({into: '#sections'}), - withPropertyFromList({list: '#sections', property: 'tracks', into: '#sections.trackRefs'}), - withPropertyFromList({list: '#sections', property: 'dateOriginallyReleased'}), - withPropertyFromList({list: '#sections', property: 'isDefaultTrackSection'}), - withPropertyFromList({list: '#sections', property: 'color'}), + withPropertiesFromList({ + list: '#sections', + properties: [ + 'tracks', + 'dateOriginallyReleased', + 'isDefaultTrackSection', + 'color', + ], + }), - fillMissingListItems({list: '#sections.trackRefs', value: []}), + fillMissingListItems({list: '#sections.tracks', value: []}), fillMissingListItems({list: '#sections.isDefaultTrackSection', value: false}), fillMissingListItems({list: '#sections.color', dependency: 'color'}), withFlattenedArray({ - from: '#sections.trackRefs', + from: '#sections.tracks', into: '#trackRefs', intoIndices: '#sections.startIndex', }), diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b37b8e31..e3225563 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1119,6 +1119,39 @@ export function withPropertyFromObject({ }; } +// Gets the listed properties from some object, providing each property's value +// as a dependency prefixed with the same name as the object (by default). +// If the object itself is null, all provided dependencies will be null; +// if it's missing only select properties, those will be provided as null. +export function withPropertiesFromObject({ + object, + properties, + prefix = + (object.startsWith('#') + ? object + : `#${object}`), +}) { + return { + annotation: `withPropertiesFromObject`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {object}, + options: {prefix, properties}, + + compute: ({object, '#options': {prefix, properties}}, continuation) => + continuation( + Object.fromEntries( + properties.map(property => [ + `${prefix}.${property}`, + (object === null || object === undefined + ? null + : object[property] ?? null), + ]))), + }, + }; +} + // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions // which were null in the original list are kept null here. Objects which don't @@ -1159,6 +1192,45 @@ export function withPropertyFromList({ }; } +// Gets the listed properties from each of a list of objects, providing lists +// of property values each into a dependency prefixed with the same name as the +// list (by default). Like withPropertyFromList, this doesn't alter indices. +export function withPropertiesFromList({ + list, + properties, + prefix = + (list.startsWith('#') + ? list + : `#${list}`), +}) { + return { + annotation: `withPropertiesFromList`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + options: {prefix, properties}, + + compute({list, '#options': {prefix, properties}}, continuation) { + const lists = + Object.fromEntries( + properties.map(property => [`${prefix}.${property}`, []])); + + for (const item of list) { + for (const property of properties) { + lists[`${prefix}.${property}`].push( + (item === null || item === undefined + ? null + : item[property] ?? null)); + } + } + + return continuation(lists); + } + } + } +} + // Replaces items of a list, which are null or undefined, with some fallback // value, either a constant (set {value}) or from a dependency ({dependency}). // By default, this replaces the passed dependency. diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a87e6ed6..52f0b773 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,7 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, - withPropertyFromList, + withPropertiesFromList, withUpdateValueAsDependency, } from '#composite'; @@ -409,21 +409,24 @@ export function withResolvedContribs({ raise: {into: []}, }), - withPropertyFromList({list: from, property: 'who', into: '#artistRefs'}), - withPropertyFromList({list: from, property: 'what', into: '#what'}), + withPropertiesFromList({ + list: from, + properties: ['who', 'what'], + prefix: '#contribs', + }), withResolvedReferenceList({ - list: '#artistRefs', + list: '#contribs.who', data: 'artistData', - into: '#who', + into: '#contribs.who', find: find.artist, notFoundMode: 'null', }), { - dependencies: ['#who', '#what'], + dependencies: ['#contribs.who', '#contribs.what'], mapContinuation: {into}, - compute({'#who': who, '#what': what}, continuation) { + compute({'#contribs.who': who, '#contribs.what': what}, continuation) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ into: stitchArrays({who, what}), -- cgit 1.3.0-6-gf8a5 From ceaed5fef3ce2c5d59a6606a6318164b93294f2b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 09:01:09 -0300 Subject: data: clean up some track property implementations --- src/data/things/track.js | 47 ++++++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 5e553b48..25d316eb 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -61,22 +61,12 @@ export class Track extends Thing { color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), - withContainingTrackSection(), - { - dependencies: ['#trackSection'], - compute: ({'#trackSection': trackSection}, continuation) => - // Album.trackSections guarantees the track section will have a - // color property (inheriting from the album's own color), but only - // if it's actually present! Color will be inherited directly from - // album otherwise. - (trackSection - ? trackSection.color - : continuation()), - }, + withContainingTrackSection(), + withPropertyFromObject({object: '#trackSection', property: 'color'}), + exposeDependencyOrContinue({dependency: '#trackSection.color'}), withPropertyFromAlbum({property: 'color'}), - exposeDependency({ dependency: '#album.color', update: {validate: isColor}, @@ -94,19 +84,13 @@ export class Track extends Thing { // of the album's main artwork. It does inherit trackCoverArtFileExtension, // if present on the album. coverArtFileExtension: compositeFrom(`Track.coverArtFileExtension`, [ - // No cover art file extension if the track doesn't have unique artwork - // in the first place. - withHasUniqueCoverArt(), - exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), + exitWithoutUniqueCoverArt(), - // Expose custom coverArtFileExtension update value first. exposeUpdateValueOrContinue(), - // Expose album's trackCoverArtFileExtension if no update value set. withPropertyFromAlbum({property: 'trackCoverArtFileExtension'}), exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - // Fallback to 'jpg'. exposeConstant({ value: 'jpg', update: {validate: isFileExtension}, @@ -175,13 +159,7 @@ export class Track extends Thing { // typically varies by release and isn't defined by the musical qualities // of the track. coverArtistContribs: compositeFrom(`Track.coverArtistContribs`, [ - { - dependencies: ['disableUniqueCoverArt'], - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? null - : continuation()), - }, + exitWithoutUniqueCoverArt(), withUpdateValueAsDependency(), withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}), @@ -559,6 +537,21 @@ function withHasUniqueCoverArt({ ]); } +// Shorthand for checking if the track has unique cover art and exposing a +// fallback value if it isn't. +function exitWithoutUniqueCoverArt({ + value = null, +} = {}) { + return compositeFrom(`exitWithoutUniqueCoverArt`, [ + withHasUniqueCoverArt(), + exitWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: 'falsy', + value, + }), + ]); +} + function trackReverseReferenceList({ property: refListProperty, }) { -- cgit 1.3.0-6-gf8a5 From 7b32066dd9629bbb220c2e2425b5294070b5a0db Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 09:16:50 -0300 Subject: infra, data: cut unneeded boilerplate from top-level compositions --- src/data/things/album.js | 17 ++++++++--------- src/data/things/index.js | 13 +++++++++++-- src/data/things/track.js | 48 ++++++++++++++++++++++++------------------------ 3 files changed, 43 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index e11d0909..07859537 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -3,7 +3,6 @@ import {stitchArrays} from '#sugar'; import {isDate, isTrackSectionList} from '#validators'; import { - compositeFrom, exitWithoutDependency, exitWithoutUpdateValue, exposeDependency, @@ -51,7 +50,7 @@ export class Album extends Thing { trackArtDate: simpleDate(), dateAddedToWiki: simpleDate(), - coverArtDate: compositeFrom(`Album.coverArtDate`, [ + coverArtDate: [ withResolvedContribs({from: 'coverArtistContribs'}), exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), @@ -60,7 +59,7 @@ export class Album extends Thing { dependency: 'date', update: {validate: isDate}, }), - ]), + ], artistContribs: contributionList(), coverArtistContribs: contributionList(), @@ -80,7 +79,7 @@ export class Album extends Thing { data: 'artTagData', }), - trackSections: compositeFrom(`Album.trackSections`, [ + trackSections: [ exitWithoutDependency({dependency: 'trackData', value: []}), exitWithoutUpdateValue({value: [], mode: 'empty'}), @@ -150,13 +149,13 @@ export class Album extends Thing { }), }, }, - ]), + ], - coverArtFileExtension: compositeFrom(`Album.coverArtFileExtension`, [ + coverArtFileExtension: [ withResolvedContribs({from: 'coverArtistContribs'}), exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), fileExtension('jpg'), - ]), + ], trackCoverArtFileExtension: fileExtension('jpg'), @@ -189,7 +188,7 @@ export class Album extends Thing { hasWallpaperArt: contribsPresent('wallpaperArtistContribs'), hasBannerArt: contribsPresent('bannerArtistContribs'), - tracks: compositeFrom(`Album.tracks`, [ + tracks: [ exitWithoutDependency({dependency: 'trackData', value: []}), exitWithoutDependency({dependency: 'trackSections', mode: 'empty', value: []}), @@ -209,7 +208,7 @@ export class Album extends Thing { }), exposeDependency({dependency: '#resolvedReferenceList'}), - ]), + ], }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/things/index.js b/src/data/things/index.js index 3b73a772..4d8d9d1f 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -2,6 +2,7 @@ import * as path from 'node:path'; import {fileURLToPath} from 'node:url'; import {logError} from '#cli'; +import {compositeFrom} from '#composite'; import * as serialize from '#serialize'; import {openAggregate, showAggregate} from '#sugar'; @@ -130,8 +131,16 @@ function evaluatePropertyDescriptors() { throw new Error(`Missing [Thing.getPropertyDescriptors] function`); } - constructor.propertyDescriptors = - constructor[Thing.getPropertyDescriptors](opts); + const results = constructor[Thing.getPropertyDescriptors](opts); + + for (const [key, value] of Object.entries(results)) { + if (Array.isArray(value)) { + results[key] = compositeFrom(`${constructor.name}.${key}`, value); + continue; + } + } + + constructor.propertyDescriptors = results; }, showFailedClasses(failedClasses) { diff --git a/src/data/things/track.js b/src/data/things/track.js index 25d316eb..a8d59023 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -59,7 +59,7 @@ export class Track extends Thing { urls: urls(), dateFirstReleased: simpleDate(), - color: compositeFrom(`Track.color`, [ + color: [ exposeUpdateValueOrContinue(), withContainingTrackSection(), @@ -71,7 +71,7 @@ export class Track extends Thing { dependency: '#album.color', update: {validate: isColor}, }), - ]), + ], // 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 @@ -83,7 +83,7 @@ export class Track extends Thing { // 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: compositeFrom(`Track.coverArtFileExtension`, [ + coverArtFileExtension: [ exitWithoutUniqueCoverArt(), exposeUpdateValueOrContinue(), @@ -95,13 +95,13 @@ export class Track extends Thing { value: 'jpg', update: {validate: isFileExtension}, }), - ]), + ], // Date of cover art release. Like coverArtFileExtension, this represents // only the track's own unique cover artwork, if any. This exposes only as // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. - coverArtDate: compositeFrom(`Track.coverArtDate`, [ + coverArtDate: [ withHasUniqueCoverArt(), exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), @@ -112,7 +112,7 @@ export class Track extends Thing { dependency: '#album.trackArtDate', update: {validate: isDate}, }), - ]), + ], commentary: commentary(), lyrics: simpleString(), @@ -136,7 +136,7 @@ export class Track extends Thing { data: 'albumData', }), - artistContribs: compositeFrom(`Track.artistContribs`, [ + artistContribs: [ inheritFromOriginalRelease({property: 'artistContribs'}), withUpdateValueAsDependency(), @@ -148,17 +148,17 @@ export class Track extends Thing { dependency: '#album.artistContribs', update: {validate: isContributionList}, }), - ]), + ], - contributorContribs: compositeFrom(`Track.contributorContribs`, [ + contributorContribs: [ inheritFromOriginalRelease({property: 'contributorContribs'}), contributionList(), - ]), + ], // Cover artists aren't inherited from the original release, since it // typically varies by release and isn't defined by the musical qualities // of the track. - coverArtistContribs: compositeFrom(`Track.coverArtistContribs`, [ + coverArtistContribs: [ exitWithoutUniqueCoverArt(), withUpdateValueAsDependency(), @@ -170,25 +170,25 @@ export class Track extends Thing { dependency: '#album.trackCoverArtistContribs', update: {validate: isContributionList}, }), - ]), + ], - referencedTracks: compositeFrom(`Track.referencedTracks`, [ + referencedTracks: [ inheritFromOriginalRelease({property: 'referencedTracks'}), referenceList({ class: Track, find: find.track, data: 'trackData', }), - ]), + ], - sampledTracks: compositeFrom(`Track.sampledTracks`, [ + sampledTracks: [ inheritFromOriginalRelease({property: 'sampledTracks'}), referenceList({ class: Track, find: find.track, data: 'trackData', }), - ]), + ], artTags: referenceList({ class: ArtTag, @@ -208,16 +208,16 @@ export class Track extends Thing { commentatorArtists: commentatorArtists(), - album: compositeFrom(`Track.album`, [ + album: [ withAlbum(), exposeDependency({dependency: '#album'}), - ]), + ], - date: compositeFrom(`Track.date`, [ + date: [ exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), withPropertyFromAlbum({property: 'date'}), exposeDependency({dependency: '#album.date'}), - ]), + ], // Whether or not the track has "unique" cover artwork - a cover which is // specifically associated with this track in particular, rather than with @@ -226,12 +226,12 @@ export class Track extends Thing { // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) - hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ + hasUniqueCoverArt: [ withHasUniqueCoverArt(), exposeDependency({dependency: '#hasUniqueCoverArt'}), - ]), + ], - otherReleases: compositeFrom(`Track.otherReleases`, [ + otherReleases: [ exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), withOriginalRelease({selfIfOriginal: true}), @@ -253,7 +253,7 @@ export class Track extends Thing { track.originalReleaseTrack === originalRelease)), }, }, - ]), + ], // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't -- cgit 1.3.0-6-gf8a5 From a9b96deeca6b2dacb7fac309c47e7bc6289270e6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 09:29:51 -0300 Subject: data: be more permissive of steps w/ no special expose behavior --- src/data/things/composite.js | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e3225563..2dd92f17 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -432,13 +432,8 @@ export function compositeFrom(firstArg, secondArg) { ? step.expose : step); - const stepComputes = !!expose.compute; - const stepTransforms = !!expose.transform; - - if (!stepComputes && !stepTransforms) { - push(new TypeError(`Steps must provide compute or transform (or both)`)); - return; - } + const stepComputes = !!expose?.compute; + const stepTransforms = !!expose?.transform; if ( stepTransforms && !stepComputes && @@ -459,7 +454,7 @@ export function compositeFrom(firstArg, secondArg) { // Unmapped dependencies are exposed on the final composition only if // they're "public", i.e. pointing to update values of other properties // on the CacheableObject. - for (const dependency of expose.dependencies ?? []) { + for (const dependency of expose?.dependencies ?? []) { if (typeof dependency === 'string' && dependency.startsWith('#')) { continue; } @@ -470,22 +465,14 @@ export function compositeFrom(firstArg, secondArg) { // Mapped dependencies are always exposed on the final composition. // These are explicitly for reading values which are named outside of // the current compositional step. - for (const dependency of Object.values(expose.mapDependencies ?? {})) { + for (const dependency of Object.values(expose?.mapDependencies ?? {})) { exposeDependencies.add(dependency); } }); } - if (!baseComposes) { - if (baseUpdates) { - if (!anyStepsTransform) { - aggregate.push(new TypeError(`Expected at least one step to transform`)); - } - } else { - if (!anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute`)); - } - } + if (!baseComposes && !baseUpdates && !anyStepsCompute) { + aggregate.push(new TypeError(`Expected at least one step to compute`)); } aggregate.close(); @@ -615,6 +602,11 @@ export function compositeFrom(firstArg, secondArg) { ? step.expose : step); + if (!expose) { + debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + continue; + } + const callingTransformForThisStep = expectingTransform && expose.transform; -- cgit 1.3.0-6-gf8a5 From 9109356037ce98af378765302841c957cc96b8d8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 09:33:04 -0300 Subject: data: exitWithoutContribs utility --- src/data/things/album.js | 9 +++------ src/data/things/thing.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 07859537..dc8d3189 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -23,6 +23,7 @@ import Thing, { contributionList, dimensions, directory, + exitWithoutContribs, fileExtension, flag, name, @@ -31,7 +32,6 @@ import Thing, { simpleString, urls, wikiData, - withResolvedContribs, withResolvedReferenceList, } from './thing.js'; @@ -51,9 +51,7 @@ export class Album extends Thing { dateAddedToWiki: simpleDate(), coverArtDate: [ - withResolvedContribs({from: 'coverArtistContribs'}), - exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), - + exitWithoutContribs({contribs: 'coverArtistContribs'}), exposeUpdateValueOrContinue(), exposeDependency({ dependency: 'date', @@ -152,8 +150,7 @@ export class Album extends Thing { ], coverArtFileExtension: [ - withResolvedContribs({from: 'coverArtistContribs'}), - exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), + exitWithoutContribs({contribs: 'coverArtistContribs'}), fileExtension('jpg'), ], diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 52f0b773..fe9000b4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -436,6 +436,23 @@ export function withResolvedContribs({ ]); } +// Shorthand for exiting if the contribution list (usually a property's update +// value) resolves to empty - ensuring that the later computed results are only +// returned if these contributions are present. +export function exitWithoutContribs({ + contribs, + value = null, +}) { + return compositeFrom(`exitWithoutContribs`, [ + withResolvedContribs({from: contribs}), + exitWithoutDependency({ + dependency: '#resolvedContribs', + mode: 'empty', + value, + }), + ]); +} + // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the // data dependency is null, or, if notFoundMode is set to 'exit', if the find -- cgit 1.3.0-6-gf8a5 From 3083e006fb8be524ca8e37c3194b78b0bf37861f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 09:33:14 -0300 Subject: data: rearrange Album properties, use exitWithoutContribs more --- src/data/things/album.js | 84 +++++++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index dc8d3189..2a8c59ed 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -59,23 +59,44 @@ export class Album extends Thing { }), ], - artistContribs: contributionList(), - coverArtistContribs: contributionList(), - trackCoverArtistContribs: contributionList(), - wallpaperArtistContribs: contributionList(), - bannerArtistContribs: contributionList(), + coverArtFileExtension: [ + exitWithoutContribs({contribs: 'coverArtistContribs'}), + fileExtension('jpg'), + ], - groups: referenceList({ - class: Group, - find: find.group, - data: 'groupData', - }), + trackCoverArtFileExtension: fileExtension('jpg'), - artTags: referenceList({ - class: ArtTag, - find: find.artTag, - data: 'artTagData', - }), + wallpaperFileExtension: [ + exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), + fileExtension('jpg'), + ], + + bannerFileExtension: [ + exitWithoutContribs({contribs: 'bannerArtistContribs'}), + fileExtension('jpg'), + ], + + wallpaperStyle: [ + exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), + simpleString(), + ], + + bannerStyle: [ + exitWithoutContribs({contribs: 'bannerArtistContribs'}), + simpleString(), + ], + + bannerDimensions: [ + exitWithoutContribs({contribs: 'bannerArtistContribs'}), + dimensions(), + ], + + hasTrackNumbers: flag(true), + isListedOnHomepage: flag(true), + isListedInGalleries: flag(true), + + commentary: commentary(), + additionalFiles: additionalFiles(), trackSections: [ exitWithoutDependency({dependency: 'trackData', value: []}), @@ -149,26 +170,23 @@ export class Album extends Thing { }, ], - coverArtFileExtension: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), - fileExtension('jpg'), - ], - - trackCoverArtFileExtension: fileExtension('jpg'), - - wallpaperStyle: simpleString(), - wallpaperFileExtension: fileExtension('jpg'), - - bannerStyle: simpleString(), - bannerFileExtension: fileExtension('jpg'), - bannerDimensions: dimensions(), + artistContribs: contributionList(), + coverArtistContribs: contributionList(), + trackCoverArtistContribs: contributionList(), + wallpaperArtistContribs: contributionList(), + bannerArtistContribs: contributionList(), - hasTrackNumbers: flag(true), - isListedOnHomepage: flag(true), - isListedInGalleries: flag(true), + groups: referenceList({ + class: Group, + find: find.group, + data: 'groupData', + }), - commentary: commentary(), - additionalFiles: additionalFiles(), + artTags: referenceList({ + class: ArtTag, + find: find.artTag, + data: 'artTagData', + }), // Update only -- cgit 1.3.0-6-gf8a5 From f242d1dec3cd905e74eec6ce518781843d5f65d9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 09:40:15 -0300 Subject: data: update contribsPresent syntax & implementation --- src/data/things/album.js | 6 +++--- src/data/things/thing.js | 15 ++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 01f52c2d..fb0c3427 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -194,9 +194,9 @@ export class Album extends Thing { commentatorArtists: commentatorArtists(), - hasCoverArt: contribsPresent('coverArtistContribs'), - hasWallpaperArt: contribsPresent('wallpaperArtistContribs'), - hasBannerArt: contribsPresent('bannerArtistContribs'), + hasCoverArt: contribsPresent({contribs: 'coverArtistContribs'}), + hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}), + hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), tracks: compositeFrom(`Album.tracks`, [ exitWithoutDependency({dependency: 'trackData', value: []}), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 79d8ae0e..0f47dc90 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,6 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, + withResultOfAvailabilityCheck, withUpdateValueAsDependency, } from '#composite'; @@ -297,15 +298,11 @@ export function singleReference({ // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent(contribsProperty) { - return { - flags: {expose: true}, - expose: { - dependencies: [contribsProperty], - compute: ({[contribsProperty]: contribs}) => - !empty(contribs), - }, - }; +export function contribsPresent({contribs}) { + return compositeFrom(`contribsPresent`, [ + withResultOfAvailabilityCheck({fromDependency: contribs, mode: 'empty'}), + exposeDependency({dependency: '#availability'}), + ]); } // Neat little shortcut for "reversing" the reference lists stored on other -- cgit 1.3.0-6-gf8a5 From 272a2f47102451a277d099d032e6f4d0ad673d80 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 18:20:00 -0300 Subject: data: handle missing expose specially in base This is for better compatibility with an updating base that doesn't transform its update value, but attempts to behave reasonably for non-transforming contexts as well. --- src/data/things/composite.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 2dd92f17..3a63f22d 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -603,7 +603,31 @@ export function compositeFrom(firstArg, secondArg) { : step); if (!expose) { - debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + if (!isBase) { + debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + continue; + } + + if (expectingTransform) { + debug(() => `step #${i+1} (base) - no expose description, returning so-far update value:`, valueSoFar); + if (continuationIfApplicable) { + debug(() => colors.bright(`end composition - raise (inferred - composing)`)); + return continuationIfApplicable(valueSoFar); + } else { + debug(() => colors.bright(`end composition - exit (inferred - not composing)`)); + return valueSoFar; + } + } else { + debug(() => `step #${i+1} (base) - no expose description, nothing to continue with`); + if (continuationIfApplicable) { + debug(() => colors.bright(`end composition - raise (inferred - composing)`)); + return continuationIfApplicable(); + } else { + debug(() => colors.bright(`end composition - exit (inferred - not composing)`)); + return null; + } + } + continue; } -- cgit 1.3.0-6-gf8a5 From b06c194fc02da22564bcb165db33282f411859a3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 18:31:09 -0300 Subject: data, test: filter out empty track sections Also test unmatched track references. --- src/data/things/album.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 9ca662a0..7569eb80 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,6 +1,7 @@ import find from '#find'; -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; import {isDate, isTrackSectionList} from '#validators'; +import {filterMultipleArrays} from '#wiki-data'; import { exitWithoutDependency, @@ -152,20 +153,25 @@ export class Album extends Thing { '#sections.startIndex', ], - transform: (trackSections, { + transform(trackSections, { '#sections.tracks': tracks, '#sections.color': color, '#sections.dateOriginallyReleased': dateOriginallyReleased, '#sections.isDefaultTrackSection': isDefaultTrackSection, '#sections.startIndex': startIndex, - }) => - stitchArrays({ + }) { + filterMultipleArrays( + tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, + tracks => !empty(tracks)); + + return stitchArrays({ tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, - }), + }); + } }, }, ], -- cgit 1.3.0-6-gf8a5 From 14329ec8eedb7ad5dcb6a3308a26686bd381ab36 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 19:04:04 -0300 Subject: data, test: ArtTag.nameShort --- src/data/things/art-tag.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 3d65b578..7e466555 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,4 +1,6 @@ +import {exposeUpdateValueOrContinue} from '#composite'; import {sortAlbumsTracksChronologically} from '#wiki-data'; +import {isName} from '#validators'; import Thing, { color, @@ -19,15 +21,20 @@ export class ArtTag extends Thing { color: color(), isContentWarning: flag(false), - nameShort: { - flags: {update: true, expose: true}, + nameShort: [ + exposeUpdateValueOrContinue(), - expose: { + { dependencies: ['name'], - transform: (value, {name}) => - value ?? name.replace(/ \(.*?\)$/, ''), + compute: ({name}) => + name.replace(/ \([^)]*?\)$/, ''), }, - }, + + { + flags: {update: true, expose: true}, + validate: {isName}, + }, + ], // Update only -- cgit 1.3.0-6-gf8a5 From c4f6c41a248ba9ef4f802cc03c20757d417540e4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 9 Sep 2023 21:08:06 -0300 Subject: data: WIP cached composition nonsense --- src/data/things/album.js | 8 ++++ src/data/things/composite.js | 111 +++++++++++++++++++++++++++++++++++++++---- src/data/things/thing.js | 25 ++++++---- src/upd8.js | 14 +++++- src/util/wiki-data.js | 68 ++++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 7569eb80..b134b78d 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -125,6 +125,14 @@ export class Album extends Thing { intoIndices: '#sections.startIndex', }), + { + dependencies: ['#trackRefs'], + compute: ({'#trackRefs': tracks}, continuation) => { + console.log(tracks); + return continuation(); + } + }, + withResolvedReferenceList({ list: '#trackRefs', data: 'trackData', diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 3a63f22d..26124b56 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {TupleMap} from '#wiki-data'; import { empty, @@ -341,6 +342,8 @@ import { // syntax as for other compositional steps, and it'll work out cleanly! // +const globalCompositeCache = {}; + export function compositeFrom(firstArg, secondArg) { const debug = fn => { if (compositeFrom.debug === true) { @@ -567,8 +570,8 @@ export function compositeFrom(firstArg, secondArg) { return {continuation, continuationStorage}; } - const continuationSymbol = Symbol('continuation symbol'); - const noTransformSymbol = Symbol('no-transform symbol'); + const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); + const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { const expectingTransform = initialValue !== noTransformSymbol; @@ -634,21 +637,83 @@ export function compositeFrom(firstArg, secondArg) { const callingTransformForThisStep = expectingTransform && expose.transform; + let continuationStorage; + const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies]); - const result = + let result; + + const getExpectedEvaluation = () => (callingTransformForThisStep ? (filteredDependencies - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.transform(valueSoFar, continuation)) + ? ['transform', valueSoFar, filteredDependencies] + : ['transform', valueSoFar]) : (filteredDependencies - ? expose.compute(filteredDependencies, continuation) - : expose.compute(continuation))); + ? ['compute', filteredDependencies] + : ['compute'])); + + const naturalEvaluate = () => { + const [name, ...args] = getExpectedEvaluation(); + let continuation; + ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); + return expose[name](...args, continuation); + } + + switch (step.cache) { + // Warning! Highly WIP! + case 'aggressive': { + const hrnow = () => { + const hrTime = process.hrtime(); + return hrTime[0] * 1000000000 + hrTime[1]; + }; + + const [name, ...args] = getExpectedEvaluation(); + + let cache = globalCompositeCache[step.annotation]; + if (!cache) { + cache = globalCompositeCache[step.annotation] = { + transform: new TupleMap(), + compute: new TupleMap(), + times: { + read: [], + evaluate: [], + }, + }; + } + + const tuplefied = args + .flatMap(arg => [ + Symbol.for('compositeFrom: tuplefied arg divider'), + ...(typeof arg !== 'object' || Array.isArray(arg) + ? [arg] + : Object.entries(arg).flat()), + ]); + + const readTime = hrnow(); + const cacheContents = cache[name].get(tuplefied); + cache.times.read.push(hrnow() - readTime); + + if (cacheContents) { + ({result, continuationStorage} = cacheContents); + } else { + const evaluateTime = hrnow(); + result = naturalEvaluate(); + cache.times.evaluate.push(hrnow() - evaluateTime); + cache[name].set(tuplefied, {result, continuationStorage}); + } + + break; + } + + default: { + result = naturalEvaluate(); + break; + } + } if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); @@ -775,6 +840,7 @@ export function compositeFrom(firstArg, secondArg) { if (baseComposes) { if (anyStepsTransform) expose.transform = transformFn; if (anyStepsCompute) expose.compute = computeFn; + if (base.cacheComposition) expose.cache = base.cacheComposition; } else if (baseUpdates) { expose.transform = transformFn; } else { @@ -785,6 +851,35 @@ export function compositeFrom(firstArg, secondArg) { return constructedDescriptor; } +export function displayCompositeCacheAnalysis() { + const showTimes = (cache, key) => { + const times = cache.times[key].slice().sort(); + + const all = times; + const worst10pc = times.slice(-times.length / 10); + const best10pc = times.slice(0, times.length / 10); + const middle50pc = times.slice(times.length / 4, -times.length / 4); + const middle80pc = times.slice(times.length / 10, -times.length / 10); + + const fmt = val => `${(val / 1000).toFixed(2)}ms`.padStart(9); + const avg = times => times.reduce((a, b) => a + b, 0) / times.length; + + const left = ` - ${key}: `; + const indn = ' '.repeat(left.length); + console.log(left + `${fmt(avg(all))} (all ${all.length})`); + console.log(indn + `${fmt(avg(worst10pc))} (worst 10%)`); + console.log(indn + `${fmt(avg(best10pc))} (best 10%)`); + console.log(indn + `${fmt(avg(middle80pc))} (middle 80%)`); + console.log(indn + `${fmt(avg(middle50pc))} (middle 50%)`); + }; + + for (const [annotation, cache] of Object.entries(globalCompositeCache)) { + console.log(`Cached ${annotation}:`); + showTimes(cache, 'evaluate'); + showTimes(cache, 'read'); + } +} + // Evaluates a function with composite debugging enabled, turns debugging // off again, and returns the result of the function. This is mostly syntax // sugar, but also helps avoid unit tests avoid accidentally printing debug diff --git a/src/data/things/thing.js b/src/data/things/thing.js index b1a9a802..19954b19 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -512,7 +512,7 @@ export function withResolvedReferenceList({ throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - return compositeFrom(`withResolvedReferenceList`, [ + const composite = compositeFrom(`withResolvedReferenceList`, [ exitWithoutDependency({ dependency: data, value: [], @@ -526,13 +526,19 @@ export function withResolvedReferenceList({ }), { - mapDependencies: {list, data}, - options: {findFunction}, - - compute: ({list, data, '#options': {findFunction}}, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), + cache: 'aggressive', + annotation: `withResolvedReferenceList.getMatches`, + flags: {expose: true, compose: true}, + + compute: { + mapDependencies: {list, data}, + options: {findFunction}, + + compute: ({list, data, '#options': {findFunction}}, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, }, { @@ -569,6 +575,9 @@ export function withResolvedReferenceList({ }, }, ]); + + console.log(composite.expose); + return composite; } // Check out the info on reverseReferenceList! diff --git a/src/upd8.js b/src/upd8.js index f6091ca2..7f423271 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -38,6 +38,7 @@ import {fileURLToPath} from 'node:url'; import wrap from 'word-wrap'; +import {displayCompositeCacheAnalysis} from '#composite'; import {processLanguageFile} from '#language'; import {isMain, traverse} from '#node-utils'; import bootRepl from '#repl'; @@ -612,6 +613,10 @@ async function main() { // which are only available after the initial linking. sortWikiDataArrays(wikiData); + console.log( + CacheableObject.getUpdateValue(wikiData.albumData[0], 'trackSections'), + wikiData.albumData[0].trackSections); + if (precacheData) { progressCallAll('Caching all data values', Object.entries(wikiData) .filter(([key]) => @@ -625,6 +630,11 @@ async function main() { .map(thing => () => CacheableObject.cacheAllExposedProperties(thing))); } + if (noBuild) { + displayCompositeCacheAnalysis(); + if (precacheData) return; + } + const internalDefaultLanguage = await processLanguageFile( path.join(__dirname, DEFAULT_STRINGS_FILE)); @@ -754,7 +764,9 @@ async function main() { logInfo`Done preloading filesizes!`; - if (noBuild) return; + if (noBuild) { + return; + } const developersComment = `<!--\n` + [ diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 0eab2204..ac652b27 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -874,3 +874,71 @@ export function filterItemsForCarousel(items) { .filter(item => item.artTags.every(tag => !tag.isContentWarning)) .slice(0, maxCarouselLayoutItems + 1); } + +// Ridiculous caching support nonsense + +export class TupleMap { + static maxNestedTupleLength = 25; + + #store = [undefined, null, null, null]; + + #lifetime(value) { + if (Array.isArray(value) && value.length <= TupleMap.maxNestedTupleLength) { + return 'tuple'; + } else if ( + typeof value === 'object' && value !== null || + typeof value === 'function' + ) { + return 'weak'; + } else { + return 'strong'; + } + } + + #getSubstoreShallow(value, store) { + const lifetime = this.#lifetime(value); + const mapIndex = {weak: 1, strong: 2, tuple: 3}[lifetime]; + + let map = store[mapIndex]; + if (map === null) { + map = store[mapIndex] = + (lifetime === 'weak' ? new WeakMap() + : lifetime === 'strong' ? new Map() + : lifetime === 'tuple' ? new TupleMap() + : null); + } + + if (map.has(value)) { + return map.get(value); + } else { + const substore = [undefined, null, null, null]; + map.set(value, substore); + return substore; + } + } + + #getSubstoreDeep(tuple, store = this.#store) { + if (tuple.length === 0) { + return store; + } else { + const [first, ...rest] = tuple; + return this.#getSubstoreDeep(rest, this.#getSubstoreShallow(first, store)); + } + } + + get(tuple) { + const store = this.#getSubstoreDeep(tuple); + return store[0]; + } + + has(tuple) { + const store = this.#getSubstoreDeep(tuple); + return store[0] !== undefined; + } + + set(tuple, value) { + const store = this.#getSubstoreDeep(tuple); + store[0] = value; + return value; + } +} -- cgit 1.3.0-6-gf8a5 From d878ab29f20c0727acafb4b1150d4e31d69c55c0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 11 Sep 2023 10:09:48 -0300 Subject: data, html, infra: supporting changes for sanitizing content --- src/data/things/language.js | 88 +++++++++++++++++++++++++++++++-------------- src/data/things/thing.js | 4 +-- src/util/html.js | 57 ++++++++++++++++++++--------- 3 files changed, 104 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/data/things/language.js b/src/data/things/language.js index 7755c505..cc49b735 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,5 +1,10 @@ import Thing from './thing.js'; +import {Tag} from '#html'; +import {isLanguageCode} from '#validators'; + +import CacheableObject from './cacheable-object.js'; + export class Language extends Thing { static [Thing.getPropertyDescriptors] = ({ validators: { @@ -68,7 +73,7 @@ export class Language extends Thing { // Update only - escapeHTML: Thing.common.externalFunction(), + escapeHTML: Thing.common.externalFunction({expose: true}), // Expose only @@ -140,19 +145,9 @@ export class Language extends Thing { } formatString(key, args = {}) { - if (this.strings && !this.strings_htmlEscaped) { - throw new Error(`HTML-escaped strings unavailable - please ensure escapeHTML function is provided`); - } - - return this.formatStringHelper(this.strings_htmlEscaped, key, args); - } + const strings = this.strings_htmlEscaped; - formatStringNoHTMLEscape(key, args = {}) { - return this.formatStringHelper(this.strings, key, args); - } - - formatStringHelper(strings, key, args = {}) { - if (!strings) { + if (!this.strings) { throw new Error(`Strings unavailable`); } @@ -160,22 +155,25 @@ export class Language extends Thing { throw new Error(`Invalid key ${key} accessed`); } - const template = strings[key]; + const template = this.strings[key]; // Convert the keys on the args dict from camelCase to CONSTANT_CASE. // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut // like, who cares, dude?) Also, this is an array, 8ecause it's handy - // for the iterating we're a8out to do. - const processedArgs = Object.entries(args).map(([k, v]) => [ - k.replace(/[A-Z]/g, '_$&').toUpperCase(), - v, - ]); + // for the iterating we're a8out to do. Also strip HTML from arguments + // that are literal strings - real HTML content should always be proper + // HTML objects (see html.js). + const processedArgs = + Object.entries(args).map(([k, v]) => [ + k.replace(/[A-Z]/g, '_$&').toUpperCase(), + this.#sanitizeStringArg(v), + ]); // Replacement time! Woot. Reduce comes in handy here! - const output = processedArgs.reduce( - (x, [k, v]) => x.replaceAll(`{${k}}`, v), - template - ); + const output = + processedArgs.reduce( + (x, [k, v]) => x.replaceAll(`{${k}}`, v), + template); // Post-processing: if any expected arguments *weren't* replaced, that // is almost definitely an error. @@ -183,7 +181,37 @@ export class Language extends Thing { throw new Error(`Args in ${key} were missing - output: ${output}`); } - return output; + // Last caveat: Wrap the output in an HTML tag so that it doesn't get + // treated as unsanitized HTML if *it* gets passed as an argument to + // *another* formatString call. + return this.#wrapSanitized(output); + } + + // Escapes HTML special characters so they're displayed as-are instead of + // treated by the browser as a tag. This does *not* have an effect on actual + // html.Tag objects, which are treated as sanitized by default (so that they + // can be nested inside strings at all). + #sanitizeStringArg(arg) { + const escapeHTML = this.escapeHTML; + + if (!escapeHTML) { + throw new Error(`escapeHTML unavailable`); + } + + if (typeof arg !== 'string') { + return arg.toString(); + } + + return escapeHTML(arg); + } + + // Wraps the output of a formatting function in a no-name-nor-attributes + // HTML tag, which will indicate to other calls to formatString that this + // content is a string *that may contain HTML* and doesn't need to + // sanitized any further. It'll still .toString() to just the string + // contents, if needed. + #wrapSanitized(output) { + return new Tag(null, null, output); } formatDate(date) { @@ -252,19 +280,25 @@ export class Language extends Thing { // Conjunction list: A, B, and C formatConjunctionList(array) { this.assertIntlAvailable('intl_listConjunction'); - return this.intl_listConjunction.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listConjunction.format( + array.map(item => this.#sanitizeStringArg(item)))); } // Disjunction lists: A, B, or C formatDisjunctionList(array) { this.assertIntlAvailable('intl_listDisjunction'); - return this.intl_listDisjunction.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listDisjunction.format( + array.map(item => this.#sanitizeStringArg(item)))); } // Unit lists: A, B, C formatUnitList(array) { this.assertIntlAvailable('intl_listUnit'); - return this.intl_listUnit.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listUnit.format( + array.map(item => this.#sanitizeStringArg(item)))); } // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c2876f56..5705ee7e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -105,8 +105,8 @@ export default class Thing extends CacheableObject { // External function. These should only be used as dependencies for other // properties, so they're left unexposed. - externalFunction: () => ({ - flags: {update: true}, + externalFunction: ({expose = false} = {}) => ({ + flags: {update: true, expose}, update: {validate: (t) => typeof t === 'function'}, }), diff --git a/src/util/html.js b/src/util/html.js index a311bbba..f0c7bfdf 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -806,24 +806,43 @@ export class Template { } // Null is always an acceptable slot value. - if (value !== null) { - if ('validate' in description) { - description.validate({ - ...commonValidators, - ...validators, - })(value); - } + if (value === null) { + return true; + } + + if ('validate' in description) { + description.validate({ + ...commonValidators, + ...validators, + })(value); + } - if ('type' in description) { - const {type} = description; - if (type === 'html') { - if (!isHTML(value)) { + if ('type' in description) { + switch (description.type) { + case 'html': { + if (!isHTML(value)) throw new TypeError(`Slot expects html (tag, template or blank), got ${typeof value}`); - } - } else { - if (typeof value !== type) { - throw new TypeError(`Slot expects ${type}, got ${typeof value}`); - } + + return true; + } + + case 'string': { + // Tags and templates are valid in string arguments - they'll be + // stringified when exposed to the description's .content() function. + if (isTag(value) || isTemplate(value)) + return true; + + if (typeof value !== 'string') + throw new TypeError(`Slot expects string, got ${typeof value}`); + + return true; + } + + default: { + if (typeof value !== description.type) + throw new TypeError(`Slot expects ${description.type}, got ${typeof value}`); + + return true; } } } @@ -847,6 +866,12 @@ export class Template { return providedValue; } + if (description.type === 'string') { + if (isTag(providedValue) || isTemplate(providedValue)) { + return providedValue.toString(); + } + } + if (providedValue !== null) { return providedValue; } -- cgit 1.3.0-6-gf8a5 From 3eb82ab2e3f9d921095af05cf0bc284f335aaa35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 11 Sep 2023 10:11:44 -0300 Subject: content: misc. changes to handle HTML sanitization --- src/content/dependencies/generateCoverGrid.js | 12 ++++++--- .../generateFooterLocalizationLinks.js | 2 +- src/content/dependencies/generatePageLayout.js | 6 ++--- src/content/dependencies/linkTemplate.js | 2 +- src/content/dependencies/linkThing.js | 2 +- src/data/things/language.js | 29 ++++++++++++++++++++++ 6 files changed, 43 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 9822e1ae..5636e4f3 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -2,7 +2,7 @@ import {stitchArrays} from '#sugar'; export default { contentDependencies: ['generateGridActionLinks'], - extraDependencies: ['html'], + extraDependencies: ['html', 'language'], relations(relation) { return { @@ -20,7 +20,7 @@ export default { actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, - generate(relations, slots, {html}) { + generate(relations, slots, {html, language}) { return ( html.tag('div', {class: 'grid-listing'}, [ stitchArrays({ @@ -42,8 +42,12 @@ export default { ? slots.lazy : false), }), - html.tag('span', {[html.onlyIfContent]: true}, name), - html.tag('span', {[html.onlyIfContent]: true}, info), + + html.tag('span', {[html.onlyIfContent]: true}, + language.sanitize(name)), + + html.tag('span', {[html.onlyIfContent]: true}, + language.sanitize(info)), ], })), diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js index b4970b17..5df83566 100644 --- a/src/content/dependencies/generateFooterLocalizationLinks.js +++ b/src/content/dependencies/generateFooterLocalizationLinks.js @@ -38,7 +38,7 @@ export default { return html.tag('div', {class: 'footer-localization-links'}, language.$('misc.uiLanguage', { - languages: links.join('\n'), + languages: language.formatListWithoutSeparator(links), })); }, }; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 95a5dbec..5377f80d 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -105,7 +105,7 @@ export default { color: {validate: v => v.isColor}, styleRules: { - validate: v => v.sparseArrayOf(v.isString), + validate: v => v.sparseArrayOf(v.isHTML), default: [], }, @@ -183,7 +183,7 @@ export default { } else { aggregate.call(v.validateProperties({ path: v.strictArrayOf(v.isString), - title: v.isString, + title: v.isHTML, }), { path: object.path, title: object.title, @@ -521,7 +521,7 @@ export default { ]), slots.bannerPosition === 'bottom' && slots.banner, footerHTML, - ].filter(Boolean).join('\n'); + ]; const pageHTML = html.tags([ `<!DOCTYPE html>`, diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index ba7c7cda..7206e960 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -16,7 +16,7 @@ export default { path: {validate: v => v.validateArrayItems(v.isString)}, hash: {type: 'string'}, - tooltip: {validate: v => v.isString}, + tooltip: {type: 'string'}, attributes: {validate: v => v.isAttributes}, color: {validate: v => v.isColor}, content: {type: 'html'}, diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js index e3e2608f..643bf4b1 100644 --- a/src/content/dependencies/linkThing.js +++ b/src/content/dependencies/linkThing.js @@ -26,7 +26,7 @@ export default { preferShortName: {type: 'boolean', default: false}, tooltip: { - validate: v => v.oneOf(v.isBoolean, v.isString), + validate: v => v.oneOf(v.isBoolean, v.isHTML), default: false, }, diff --git a/src/data/things/language.js b/src/data/things/language.js index cc49b735..afa9f1ee 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -214,6 +214,28 @@ export class Language extends Thing { return new Tag(null, null, output); } + // Similar to the above internal methods, but this one is public. + // It should be used when embedding content that may not have previously + // been sanitized directly into an HTML tag or template's contents. + // The templating engine usually handles this on its own, as does passing + // a value (sanitized or not) directly as an argument to formatString, + // but if you used a custom validation function ({validate: v => v.isHTML} + // instead of {type: 'string'} / {type: 'html'}) and are embedding the + // contents of a slot directly, it should be manually sanitized with this + // function first. + sanitize(arg) { + const escapeHTML = this.escapeHTML; + + if (!escapeHTML) { + throw new Error(`escapeHTML unavailable`); + } + + return ( + (typeof arg === 'string' + ? new Tag(null, null, escapeHTML(arg)) + : arg)); + } + formatDate(date) { this.assertIntlAvailable('intl_date'); return this.intl_date.format(date); @@ -301,6 +323,13 @@ export class Language extends Thing { array.map(item => this.#sanitizeStringArg(item)))); } + // Lists without separator: A B C + formatListWithoutSeparator(array) { + return this.#wrapSanitized( + array.map(item => this.#sanitizeStringArg(item)) + .join(' ')); + } + // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB formatFileSize(bytes) { if (!bytes) return ''; -- cgit 1.3.0-6-gf8a5 From 4ac038fce1f99ca12a9bca578fb9d0ffd8482ecd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 11 Sep 2023 14:33:47 -0300 Subject: content: generateAlbumSidebarTrackSection: don't pre-encode endash --- src/content/dependencies/generateAlbumSidebarTrackSection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index 2aca6da1..d71b0bdb 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -82,7 +82,7 @@ export default { (data.hasTrackNumbers ? language.$('albumSidebar.trackList.group.withRange', { group: sectionName, - range: `${data.firstTrackNumber}–${data.lastTrackNumber}` + range: `${data.firstTrackNumber}–${data.lastTrackNumber}` }) : language.$('albumSidebar.trackList.group', { group: sectionName, -- cgit 1.3.0-6-gf8a5 From 78115f0be17ee405d3711204aaa53e0597a29826 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 11 Sep 2023 14:34:12 -0300 Subject: thumbs: read win32-style path from cache --- src/gen-thumbs.js | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 741cdff3..a5b550ad 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -168,28 +168,45 @@ getThumbnailsAvailableForDimensions.all = .map(([name, {size}]) => [name, size]) .sort((a, b) => b[1] - a[1]); -export function checkIfImagePathHasCachedThumbnails(imagePath, cache) { +function getCacheEntryForMediaPath(mediaPath, cache) { + // Gets the cache entry for the provided image path, which should always be + // a forward-slashes path (i.e. suitable for display online). Since the cache + // file may have forward or back-slashes, this checks both. + + const entryFromMediaPath = cache[mediaPath]; + if (entryFromMediaPath) return entryFromMediaPath; + + const winPath = mediaPath.split(path.posix.sep).join(path.win32.sep); + const entryFromWinPath = cache[winPath]; + if (entryFromWinPath) return entryFromWinPath; + + return null; +} + +export function checkIfImagePathHasCachedThumbnails(mediaPath, cache) { // Generic utility for checking if the thumbnail cache includes any info for // the provided image path, so that the other functions don't hard-code the // cache format. - return !!cache[imagePath]; + return !!getCacheEntryForMediaPath(mediaPath, cache); } -export function getDimensionsOfImagePath(imagePath, cache) { +export function getDimensionsOfImagePath(mediaPath, cache) { // This function is really generic. It takes the gen-thumbs image cache and // returns the dimensions in that cache, so that other functions don't need // to hard-code the cache format. - if (!cache[imagePath]) { - throw new Error(`Expected imagePath to be included in cache, got ${imagePath}`); + const cacheEntry = getCacheEntryForMediaPath(mediaPath, cache); + + if (!cacheEntry) { + throw new Error(`Expected mediaPath to be included in cache, got ${mediaPath}`); } - const [width, height] = cache[imagePath].slice(1); + const [width, height] = cacheEntry.slice(1); return [width, height]; } -export function getThumbnailEqualOrSmaller(preferred, imagePath, cache) { +export function getThumbnailEqualOrSmaller(preferred, mediaPath, cache) { // This function is totally exclusive to page generation. It's a shorthand // for accessing dimensions from the thumbnail cache, calculating all the // thumbnails available, and selecting the one which is equal to or smaller @@ -197,12 +214,12 @@ export function getThumbnailEqualOrSmaller(preferred, imagePath, cache) { // one which is being thumbnail-ified, this just returns the name of the // selected thumbnail size. - if (!cache[imagePath]) { - throw new Error(`Expected imagePath to be included in cache, got ${imagePath}`); + if (!getCacheEntryForMediaPath(mediaPath, cache)) { + throw new Error(`Expected mediaPath to be included in cache, got ${mediaPath}`); } const {size: preferredSize} = thumbnailSpec[preferred]; - const [width, height] = getDimensionsOfImagePath(imagePath, cache); + const [width, height] = getDimensionsOfImagePath(mediaPath, cache); const available = getThumbnailsAvailableForDimensions([width, height]); const [selected] = available.find(([name, size]) => size <= preferredSize); return selected; -- cgit 1.3.0-6-gf8a5 From b19e165dc8ba13cd0e2d1862e645d34d86142566 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 11 Sep 2023 15:09:19 -0300 Subject: thumbs, infra: expose list of missing image paths --- src/gen-thumbs.js | 2 ++ src/upd8.js | 4 +++- src/write/bind-utilities.js | 2 ++ src/write/build-modes/live-dev-server.js | 2 ++ src/write/build-modes/static-build.js | 2 ++ 5 files changed, 11 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index a5b550ad..b7c192c3 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -689,6 +689,8 @@ export async function verifyImagePaths(mediaPath, {urls, wikiData}) { console.warn(color.yellow(` - `) + file); } } + + return {missing, misplaced}; } // Recursively traverses the provided (extant) media path, filtering so only diff --git a/src/upd8.js b/src/upd8.js index 2ec231c9..b3646c9d 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -677,7 +677,8 @@ async function main() { const urls = generateURLs(urlSpec); - await verifyImagePaths(mediaPath, {urls, wikiData}); + const {missing: missingImagePaths} = + await verifyImagePaths(mediaPath, {urls, wikiData}); const fileSizePreloader = new FileSizePreloader(); @@ -795,6 +796,7 @@ async function main() { defaultLanguage: finalDefaultLanguage, languages, + missingImagePaths, thumbsCache, urls, urlSpec, diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 942cce89..3d4ecc7a 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -25,6 +25,7 @@ export function bindUtilities({ getSizeOfImagePath, language, languages, + missingImagePaths, pagePath, thumbsCache, to, @@ -43,6 +44,7 @@ export function bindUtilities({ html, language, languages, + missingImagePaths, pagePath, thumb, to, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 9889b3f0..730686db 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -58,6 +58,7 @@ export async function go({ defaultLanguage, languages, + missingImagePaths, srcRootPath, thumbsCache, urls, @@ -347,6 +348,7 @@ export async function go({ getSizeOfImagePath, language, languages, + missingImagePaths, pagePath: servePath, thumbsCache, to, diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 82a947c7..79c8defd 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -90,6 +90,7 @@ export async function go({ defaultLanguage, languages, + missingImagePaths, srcRootPath, thumbsCache, urls, @@ -302,6 +303,7 @@ export async function go({ getSizeOfImagePath, language, languages, + missingImagePaths, pagePath, thumbsCache, to, -- cgit 1.3.0-6-gf8a5 From a88d346f9e7c2c9b01795c10761a5ec698e88f94 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 11 Sep 2023 15:11:24 -0300 Subject: content: image: custom-handle images assessed to be missing --- src/content/dependencies/image.js | 42 +++++++++++++++++++++++++++------------ src/strings-default.json | 1 + 2 files changed, 30 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index b5591e6d..64fe8533 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -1,4 +1,4 @@ -import {logWarn} from '#cli'; +import {logInfo, logWarn} from '#cli'; import {empty} from '#sugar'; export default { @@ -10,6 +10,7 @@ export default { 'getThumbnailsAvailableForDimensions', 'html', 'language', + 'missingImagePaths', 'to', ], @@ -63,6 +64,7 @@ export default { getThumbnailsAvailableForDimensions, html, language, + missingImagePaths, to, }) { let originalSrc; @@ -75,8 +77,27 @@ export default { originalSrc = ''; } - const willLink = typeof slots.link === 'string' || slots.link; - const customLink = typeof slots.link === 'string'; + let mediaSrc = null; + if (originalSrc.startsWith(to('media.root'))) { + mediaSrc = + originalSrc + .slice(to('media.root').length) + .replace(/^\//, ''); + } + + const isMissingImageFile = + missingImagePaths.includes(mediaSrc); + + if (isMissingImageFile) { + logInfo`No image file for ${mediaSrc} - build again for list of missing images.`; + } + + const willLink = + !isMissingImageFile && + (typeof slots.link === 'string' || slots.link); + + const customLink = + typeof slots.link === 'string'; const willReveal = slots.reveal && @@ -87,13 +108,16 @@ export default { const idOnImg = willLink ? null : slots.id; const idOnLink = willLink ? slots.id : null; + const classOnImg = willLink ? null : slots.class; const classOnLink = willLink ? slots.class : null; - if (!originalSrc) { + if (!originalSrc || isMissingImageFile) { return prepare( html.tag('div', {class: 'image-text-area'}, - slots.missingSourceContent)); + (html.isBlank(slots.missingSourceContent) + ? language.$(`misc.missingImage`) + : slots.missingSourceContent))); } let reveal = null; @@ -108,14 +132,6 @@ export default { ]; } - let mediaSrc = null; - if (originalSrc.startsWith(to('media.root'))) { - mediaSrc = - originalSrc - .slice(to('media.root').length) - .replace(/^\//, ''); - } - const hasThumbnails = mediaSrc && checkIfImagePathHasCachedThumbnails(mediaSrc); diff --git a/src/strings-default.json b/src/strings-default.json index 8d7756ad..b5e39e97 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -197,6 +197,7 @@ "misc.external.flash.homestuck.page": "{LINK} (page {PAGE})", "misc.external.flash.homestuck.secret": "{LINK} (secret page)", "misc.external.flash.youtube": "{LINK} (on any device)", + "misc.missingImage": "(This image file is missing)", "misc.missingLinkContent": "(Missing link content)", "misc.nav.previous": "Previous", "misc.nav.next": "Next", -- cgit 1.3.0-6-gf8a5 From 8a23eb5888242d6243eb6954d7d68622c24bdbcd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 11 Sep 2023 15:12:13 -0300 Subject: client: hide missing cover image from sticky heading --- src/static/client2.js | 10 ++++++++-- src/upd8.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/static/client2.js b/src/static/client2.js index 8ae9876e..78970410 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -534,11 +534,17 @@ const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky- const {parentElement: contentContainer} = stickyContainer; const stickySubheadingRow = stickyContainer.querySelector('.content-sticky-subheading-row'); const stickySubheading = stickySubheadingRow.querySelector('h2'); - const stickyCoverContainer = stickyContainer.querySelector('.content-sticky-heading-cover-container'); - const stickyCover = stickyCoverContainer?.querySelector('.content-sticky-heading-cover'); + let stickyCoverContainer = stickyContainer.querySelector('.content-sticky-heading-cover-container'); + let stickyCover = stickyCoverContainer?.querySelector('.content-sticky-heading-cover'); const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading')); const contentCover = contentContainer.querySelector('#cover-art-container'); + if (stickyCover.querySelector('.image-text-area')) { + stickyCoverContainer.remove(); + stickyCoverContainer = null; + stickyCover = null; + } + return { contentContainer, contentCover, diff --git a/src/upd8.js b/src/upd8.js index b3646c9d..df172e48 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -80,7 +80,7 @@ import * as buildModes from './write/build-modes/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const CACHEBUST = 20; +const CACHEBUST = 21; let COMMIT; try { -- cgit 1.3.0-6-gf8a5 From 44f1442bf28bac7b07ac25c1ea15c6b3a9d1223a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 11 Sep 2023 15:13:54 -0300 Subject: css: give square images a self-confidence boost --- src/static/site4.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/static/site4.css b/src/static/site4.css index f79c0c2d..ab8976bc 100644 --- a/src/static/site4.css +++ b/src/static/site4.css @@ -558,7 +558,7 @@ a.box img { height: auto; } -a.box .square .image-container { +.square .image-container { width: 100%; height: 100%; } -- cgit 1.3.0-6-gf8a5 From 9dd9d5c328da8ad1d90cd33d4a13efac92104398 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 12 Sep 2023 14:34:20 -0300 Subject: data: more syntax WIP --- src/data/things/composite.js | 65 ++--- src/data/things/thing.js | 591 +++++++++++++++++++++++++++---------------- 2 files changed, 409 insertions(+), 247 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26124b56..32a61033 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -344,7 +344,9 @@ import { const globalCompositeCache = {}; -export function compositeFrom(firstArg, secondArg) { +export function compositeFrom(description) { + const {annotation, steps: composition} = description; + const debug = fn => { if (compositeFrom.debug === true) { const label = @@ -363,13 +365,6 @@ export function compositeFrom(firstArg, secondArg) { } }; - let annotation, composition; - if (typeof firstArg === 'string') { - [annotation, composition] = [firstArg, secondArg]; - } else { - [annotation, composition] = [null, firstArg]; - } - const base = composition.at(-1); const steps = composition.slice(); @@ -974,6 +969,7 @@ export function exposeConstant({ export function withResultOfAvailabilityCheck({ fromUpdateValue, fromDependency, + modeDependency, mode = 'null', into = '#availability', }) { @@ -1026,31 +1022,40 @@ export function withResultOfAvailabilityCheck({ // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! -export function exposeDependencyOrContinue({ - dependency, - mode = 'null', -}) { - return compositeFrom(`exposeDependencyOrContinue`, [ - withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), +export const exposeDependencyOrContinue = + templateCompositeFrom({ + annotation: `exposeDependencyOrContinue`, - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), + inputs: { + dependency: input(), + mode: input.default('null'), }, - { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, - ]); -} + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability'], + compute: (continuation, { + ['#availability']: availability, + }) => + (availability + ? continuation() + : continuation.raise()), + }, + + { + dependencies: [input('#dependency')], + compute: (continuation, { + [input('#dependency')]: dependency, + }) => + continuation.exit(dependency), + }, + ], + }); // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19954b19..5cfeaeb2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -203,15 +203,18 @@ export function externalFunction() { // also existing on this object! // export function contributionList() { - return compositeFrom(`contributionList`, [ - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue'}), - exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({ - value: [], - update: {validate: isContributionList}, - }), - ]); + return compositeFrom({ + annotation: `contributionList`, + + update: {validate: isContributionList}, + + steps: [ + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue'}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({value: []}), + ], + }); } // Artist commentary! Generally present on tracks and albums. @@ -259,7 +262,7 @@ export function additionalFiles() { export function referenceList({ class: thingClass, data, - find, + find: findFunction, }) { if (!thingClass) { throw new TypeError(`Expected a Thing class`); @@ -270,29 +273,40 @@ export function referenceList({ throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return compositeFrom(`referenceList`, [ - withUpdateValueAsDependency(), + return compositeFrom({ + annotation: `referenceList`, - withResolvedReferenceList({ - data, find, - list: '#updateValue', - notFoundMode: 'filter', - }), + update: { + validate: validateReferenceList(referenceType), + }, - exposeDependency({ - dependency: '#resolvedReferenceList', - update: { - validate: validateReferenceList(referenceType), - }, - }), - ]); + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.findFunction': findFunction, + }, + + steps: [ + withUpdateValueAsDependency(), + + withResolvedReferenceList({ + list: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ], + }); } // Corresponding function for a single reference. export function singleReference({ class: thingClass, data, - find, + find: findFunction, }) { if (!thingClass) { throw new TypeError(`Expected a Thing class`); @@ -303,27 +317,56 @@ export function singleReference({ throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return compositeFrom(`singleReference`, [ - withUpdateValueAsDependency(), + return compositeFrom({ + annotation: `singleReference`, - withResolvedReference({ref: '#updateValue', data, find}), + update: { + validate: validateReference(referenceType), + }, - exposeDependency({ - dependency: '#resolvedReference', - update: { - validate: validateReference(referenceType), - }, - }), - ]); + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.findFunction': findFunction, + }, + + steps: [ + withUpdateValueAsDependency(), + + withResolvedReference({ + ref: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReference'}), + ], + }); } // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent({contribs}) { - return compositeFrom(`contribsPresent`, [ - withResultOfAvailabilityCheck({fromDependency: contribs, mode: 'empty'}), - exposeDependency({dependency: '#availability'}), - ]); +export function contribsPresent({ + contribs, +}) { + return compositeFrom({ + annotation: `contribsPresent`, + + mapDependencies: { + '#composition.contribs': contribs, + }, + + steps: [ + withResultOfAvailabilityCheck({ + fromDependency: '#composition.contribs', + mode: 'empty', + }), + + exposeDependency({dependency: '#availability'}), + ], + }); } // Neat little shortcut for "reversing" the reference lists stored on other @@ -332,10 +375,23 @@ export function contribsPresent({contribs}) { // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. export function reverseReferenceList({data, list}) { - return compositeFrom(`reverseReferenceList`, [ - withReverseReferenceList({data, list}), - exposeDependency({dependency: '#reverseReferenceList'}), - ]); + return compositeFrom({ + annotation: `reverseReferenceList`, + + mapDependencies: { + '#composition.data': data, + '#composition.list': list, + }, + + steps: [ + withReverseReferenceList({ + data: '#composition.data', + list: '#composition.list', + }), + + exposeDependency({dependency: '#reverseReferenceList'}), + ], + }); } // General purpose wiki data constructor, for properties like artistData, @@ -353,39 +409,51 @@ export function wikiData(thingClass) { // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. export function commentatorArtists() { - return compositeFrom(`commentatorArtists`, [ - exitWithoutDependency({dependency: 'commentary', mode: 'falsy', value: []}), - - { - dependencies: ['commentary'], - compute: ({commentary}, continuation) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/<i>(?<who>.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), + return compositeFrom({ + annotation: `commentatorArtists`, + + constantDependencies: { + '#composition.findFunction': find.artists, }, - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - into: '#artists', - find: find.artist, - }), + steps: [ + exitWithoutDependency({ + dependency: 'commentary', + mode: 'falsy', + value: [], + }), - { - flags: {expose: true}, + { + dependencies: ['commentary'], + compute: ({commentary}, continuation) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/<i>(?<who>.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), + }, - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + into: '#artists', + find: '#composition.findFunction', + }), + + { + flags: {expose: true}, + + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), + }, }, - }, - ]); + ], + }); } // Compositional utilities @@ -398,39 +466,54 @@ export function withResolvedContribs({ from, into = '#resolvedContribs', }) { - return compositeFrom(`withResolvedContribs`, [ - raiseWithoutDependency({ - dependency: from, - mode: 'empty', - map: {into}, - raise: {into: []}, - }), - - withPropertiesFromList({ - list: from, - properties: ['who', 'what'], - prefix: '#contribs', - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - into: '#contribs.who', - find: find.artist, - notFoundMode: 'null', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - mapContinuation: {into}, - compute({'#contribs.who': who, '#contribs.what': what}, continuation) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - into: stitchArrays({who, what}), - }); - }, + return compositeFrom({ + annotation: `withResolvedContribs`, + + mapDependencies: { + '#composition.from': from, }, - ]); + + mapContinuation: { + '#composition.into': into, + }, + + constantDependencies: { + '#composition.findFunction': find.artist, + '#composition.notFoundMode': 'null', + }, + + steps: [ + raiseWithoutDependency({ + dependency: '#composition.from', + raise: {'#composition.into': []}, + mode: 'empty', + }), + + withPropertiesFromList({ + list: '#composition.from', + prefix: '#contribs', + properties: ['who', 'what'], + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + into: '#contribs.who', + find: '#composition.findFunction', + notFoundMode: '#composition.notFoundMode', + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + compute({'#contribs.who': who, '#contribs.what': what}, continuation) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + '#composition.into': stitchArrays({who, what}), + }); + }, + }, + ], + }); } // Shorthand for exiting if the contribution list (usually a property's update @@ -440,14 +523,37 @@ export function exitWithoutContribs({ contribs, value = null, }) { - return compositeFrom(`exitWithoutContribs`, [ - withResolvedContribs({from: contribs}), - exitWithoutDependency({ - dependency: '#resolvedContribs', - mode: 'empty', - value, - }), - ]); + return compositeFrom({ + annotation: `exitWithoutContribs`, + + mapDependencies: { + '#composition.contribs': contribs, + }, + + constantDependencies: { + '#composition.value': value, + }, + + steps: [ + withResolvedContribs({from: '#composition.contribs'}), + + withResultOfAvailabilityCheck({ + fromDependency: '#resolvedContribs', + mode: 'empty', + }), + + { + dependencies: ['#availability', '#composition.value'], + compute: ({ + '#availability': availability, + '#composition.value': value, + }, continuation) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], + }); } // Resolves a reference by using the provided find function to match it @@ -461,39 +567,54 @@ export function withResolvedReference({ data, find: findFunction, into = '#resolvedReference', - notFoundMode = 'null', + notFoundMode, }) { - if (!['exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be exit or null`); - } - - return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency({ - dependency: ref, - map: {into}, - raise: {into: null}, - }), - - exitWithoutDependency({ - dependency: data, - }), - - { - options: {findFunction, notFoundMode}, - mapDependencies: {ref, data}, - mapContinuation: {match: into}, + return compositeFrom({ + annotation: `withResolvedReference`, + + mapDependencies: { + '#composition.ref': ref, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, + }, - compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); + constantDependencies: { + '#composition.notFoundMode': 'null', + }, - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } + mapContinuation: { + '#composition.into': into, + }, - return continuation.raise({match}); + steps: [ + raiseWithoutDependency({ + dependency: '#composition.ref', + raise: {'#composition.into': null}, + }), + + exitWithoutDependency({ + dependency: '#composition.data', + }), + + { + compute({ + '#composition.ref': ref, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, + }, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raise({match}); + }, }, - }, - ]); + ], + }); } // Resolves a list of references, with each reference matched with provided @@ -505,79 +626,93 @@ export function withResolvedReferenceList({ list, data, find: findFunction, + notFoundMode, into = '#resolvedReferenceList', - notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - const composite = compositeFrom(`withResolvedReferenceList`, [ - exitWithoutDependency({ - dependency: data, - value: [], - }), - - raiseWithoutDependency({ - dependency: list, - mode: 'empty', - map: {into}, - raise: {into: []}, - }), - - { - cache: 'aggressive', - annotation: `withResolvedReferenceList.getMatches`, - flags: {expose: true, compose: true}, - - compute: { - mapDependencies: {list, data}, - options: {findFunction}, - - compute: ({list, data, '#options': {findFunction}}, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), - }, + return compositeFrom({ + annotation: `withResolvedReferenceList`, + + mapDependencies: { + '#composition.list': list, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, }, - { - dependencies: ['#matches'], - mapContinuation: {into}, + constantDependencies: { + '#composition.notFoundMode': 'filter', + }, - compute: ({'#matches': matches}, continuation) => - (matches.every(match => match) - ? continuation.raise({into: matches}) - : continuation()), + mapContinuation: { + '#composition.into': into, }, - { - dependencies: ['#matches'], - options: {notFoundMode}, - mapContinuation: {into}, - - compute({ - '#matches': matches, - '#options': {notFoundMode}, - }, continuation) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({into: matches}); - - case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({into: matches}); - } + steps: [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + raiseWithoutDependency({ + dependency: '#composition.list', + raise: {'#composition.into': []}, + mode: 'empty', + }), + + { + dependencies: [ + '#composition.list', + '#composition.data', + '#composition.findFunction', + ], + + compute: ({ + '#composition.list': list, + '#composition.data': data, + '#composition.findFunction': findFunction, + }, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, + + { + dependencies: ['#matches'], + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({'#continuation.into': matches}) + : continuation()), }, - }, - ]); - console.log(composite.expose); - return composite; + { + dependencies: ['#matches', '#composition.notFoundMode'], + compute({ + '#matches': matches, + '#composition.notFoundMode': notFoundMode, + }, continuation) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({'#continuation.into': matches}); + + case 'null': + matches = matches.map(match => match ?? null); + return continuation.raise({'#continuation.into': matches}); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } + }, + }, + ], + }); } // Check out the info on reverseReferenceList! @@ -587,22 +722,44 @@ export function withReverseReferenceList({ list: refListProperty, into = '#reverseReferenceList', }) { - return compositeFrom(`withReverseReferenceList`, [ - exitWithoutDependency({ - dependency: data, - value: [], - }), - - { - dependencies: ['this'], - mapDependencies: {data}, - mapContinuation: {into}, - options: {refListProperty}, - - compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => - continuation({ - into: data.filter(thing => thing[refListProperty].includes(thisThing)), - }), + return compositeFrom({ + annotation: `withReverseReferenceList`, + + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.refListProperty': refListProperty, + }, + + mapContinuation: { + '#composition.into': into, }, - ]); + + steps: [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + { + dependencies: [ + 'this', + '#composition.data', + '#composition.refListProperty', + ], + + compute: ({ + this: thisThing, + '#composition.data': data, + '#composition.refListProperty': refListProperty, + }, continuation) => + continuation({ + '#composition.into': + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ], + }); } -- cgit 1.3.0-6-gf8a5 From 113157085c7bd6f23bd7c08ad6cd2d94673d7033 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 12 Sep 2023 14:47:15 -0300 Subject: content: artist..{TracksChunkedList,ChunkItem}: avoid double sanitize No further changes needed for artwork and flash chunked lists, where contributions consist of only one item (which might be null). --- .../generateArtistInfoPageChunkItem.js | 4 ++-- .../generateArtistInfoPageTracksChunkedList.js | 25 ++++++++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js index 36f0ebcc..9f99513d 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js @@ -5,7 +5,7 @@ export default { content: {type: 'html'}, otherArtistLinks: {validate: v => v.strictArrayOf(v.isHTML)}, - contribution: {type: 'string'}, + contribution: {type: 'html'}, rerelease: {type: 'boolean'}, }, @@ -30,7 +30,7 @@ export default { options.artists = language.formatConjunctionList(slots.otherArtistLinks); } - if (slots.contribution) { + if (!html.isBlank(slots.contribution)) { parts.push('withContribution'); options.contribution = slots.contribution; } diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index 0566f713..654f759c 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -1,4 +1,4 @@ -import {accumulateSum, stitchArrays} from '#sugar'; +import {accumulateSum, empty, stitchArrays} from '#sugar'; import { chunkByProperties, @@ -16,7 +16,7 @@ export default { 'linkTrack', ], - extraDependencies: ['language'], + extraDependencies: ['html', 'language'], query(artist) { const tracksAsArtistAndContributor = @@ -122,11 +122,16 @@ export default { trackContributions: query.chunks.map(({chunk}) => - chunk.map(({contribs}) => - contribs - .filter(({who}) => who === artist) - .filter(({what}) => what) - .map(({what}) => what))), + chunk + .map(({contribs}) => + contribs + .filter(({who}) => who === artist) + .filter(({what}) => what) + .map(({what}) => what)) + .map(contributions => + (empty(contributions) + ? null + : contributions))), trackRereleases: query.chunks.map(({chunk}) => @@ -134,7 +139,7 @@ export default { }; }, - generate(data, relations, {language}) { + generate(data, relations, {html, language}) { return relations.chunkedList.slots({ chunks: stitchArrays({ @@ -192,7 +197,9 @@ export default { rerelease, contribution: - language.formatUnitList(contribution), + (contribution + ? language.formatUnitList(contribution) + : html.blank()), content: (duration -- cgit 1.3.0-6-gf8a5 From 0bb9482518badd10b6e2c3e8e2ba99367b6f6fd1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 12 Sep 2023 15:14:13 -0300 Subject: thumbs: return correct function signature, yes, yes --- src/gen-thumbs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index b7c192c3..18d1964d 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -673,7 +673,7 @@ export async function verifyImagePaths(mediaPath, {urls, wikiData}) { if (empty(missing) && empty(misplaced)) { logInfo`All image paths are good - nice! None are missing or misplaced.`; - return; + return {missing, misplaced}; } if (!empty(missing)) { -- cgit 1.3.0-6-gf8a5 From 88ae3f19a38782ca1396b8bc131d1adffb9699e2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 14 Sep 2023 08:54:34 -0300 Subject: data: update syntax for essential compositional utilities Also withPropertyFromObject because some commits were messed up along the way... WIP as usual. --- src/data/things/composite.js | 422 ++++++++++++++++++++----------------------- 1 file changed, 198 insertions(+), 224 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 32a61033..3e766b2c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -953,11 +953,10 @@ export function exposeConstant({ }; } -// Checks the availability of a dependency or the update value and provides -// the result to later steps under '#availability' (by default). This is -// mainly intended for use by the more specific utilities, which you should -// consider using instead. Customize {mode} to select one of these modes, -// or leave unset and default to 'null': +// Checks the availability of a dependency and provides the result to later +// steps under '#availability' (by default). This is mainly intended for use +// by the more specific utilities, which you should consider using instead. +// Customize {mode} to select one of these modes, or default to 'null': // // * 'null': Check that the value isn't null (and not undefined either). // * 'empty': Check that the value is neither null nor an empty array. @@ -966,274 +965,249 @@ export function exposeConstant({ // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! // -export function withResultOfAvailabilityCheck({ - fromUpdateValue, - fromDependency, - modeDependency, - mode = 'null', - into = '#availability', -}) { - if (!['null', 'empty', 'falsy'].includes(mode)) { - throw new TypeError(`Expected mode to be null, empty, or falsy`); - } - if (fromUpdateValue && fromDependency) { - throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); - } +const availabilityCheckMode = { + validate: oneOf('null', 'empty', 'falsy'), + defaultValue: 'null', +}; - if (!fromUpdateValue && !fromDependency) { - throw new TypeError(`Missing dependency name (or fromUpdateValue)`); - } +export const withResultOfAvailabilityCheck = templateCompositeFrom({ + annotation: `withResultOfAvailabilityCheck`, - const checkAvailability = (value, mode) => { - switch (mode) { - case 'null': return value !== null && value !== undefined; - case 'empty': return !empty(value); - case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); - default: return false; - } - }; + inputs: { + from: input(), + mode: input(availabilityCheckMode), + }, - if (fromDependency) { - return { - annotation: `withResultOfAvailabilityCheck.fromDependency`, - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {from: fromDependency}, - mapContinuation: {into}, - options: {mode}, - compute: ({from, '#options': {mode}}, continuation) => - continuation({into: checkAvailability(from, mode)}), - }, - }; - } else { - return { - annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, - flags: {expose: true, compose: true}, - expose: { - mapContinuation: {into}, - options: {mode}, - transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {into: checkAvailability(value, mode)}), - }, - }; - } -} + outputs: { + into: '#availability', + }, -// Exposes a dependency as it is, or continues if it's unavailable. -// See withResultOfAvailabilityCheck for {mode} options! -export const exposeDependencyOrContinue = - templateCompositeFrom({ - annotation: `exposeDependencyOrContinue`, + steps: [ + { + dependencies: [input('from'), input('mode')], - inputs: { - dependency: input(), - mode: input.default('null'), - }, + compute: (continuation, { + [input('from')]: dependency, + [input('mode')]: mode, + }) => { + let availability; - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability'], - compute: (continuation, { - ['#availability']: availability, - }) => - (availability - ? continuation() - : continuation.raise()), - }, + switch (mode) { + case 'null': + availability = value !== null && value !== undefined; + break; + + case 'empty': + availability = !empty(value); + break; + + case 'falsy': + availability = !!value && (!Array.isArray(value) || !empty(value)); + break; + } - { - dependencies: [input('#dependency')], - compute: (continuation, { - [input('#dependency')]: dependency, - }) => - continuation.exit(dependency), + return continuation({into: availability}); }, - ], - }); + }, + ], +}); -// Exposes the update value of an {update: true} property as it is, -// or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! -export function exposeUpdateValueOrContinue({ - mode = 'null', -} = {}) { - return compositeFrom(`exposeUpdateValueOrContinue`, [ +// Exposes a dependency as it is, or continues if it's unavailable. +// See withResultOfAvailabilityCheck for {mode} options! +export const exposeDependencyOrContinue = templateCompositeFrom({ + annotation: `exposeDependencyOrContinue`, + + inputs: { + dependency: input(), + mode: input(availabilityCheckMode), + }, + + steps: () => [ withResultOfAvailabilityCheck({ - fromUpdateValue: true, - mode, + from: input('dependency'), + mode: input('mode'), }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('dependency')], + compute: (continuation, { + ['#availability']: availability, + [input('dependency')]: dependency, + }) => (availability - ? continuation() - : continuation.raise()), + ? continuation.exit(dependency) + : continuation()), }, + ], +}); - { - transform: (value, continuation) => - continuation.exit(value), - }, - ]); -} +// Exposes the update value of an {update: true} property as it is, +// or continues if it's unavailable. See withResultOfAvailabilityCheck +// for {mode} options! +export const exposeUpdateValueOrContinue = templateCompositeFrom({ + annotation: `exposeUpdateValueOrContinue`, -// Early exits if an availability check has failed. -// This is for internal use only - use `exitWithoutDependency` or -// `exitWithoutUpdateValue` instead. -export function exitIfAvailabilityCheckFailed({ - availability = '#availability', - value = null, -} = {}) { - return compositeFrom(`exitIfAvailabilityCheckFailed`, [ - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + inputs: { + mode: input(availabilityCheckMode), + }, - { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, - ]); -} + steps: () => [ + exposeDependencyOrContinue({ + dependency: input.updateValue(), + mode: input('mode'), + }), + ], +}); // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutDependency({ - dependency, - mode = 'null', - value = null, -}) { - return compositeFrom(`exitWithoutDependency`, [ - withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - exitIfAvailabilityCheckFailed({value}), - ]); -} +export const exitWithoutDependency = templateCompositeFrom({ + annotation: `exitWithoutDependency`, -// Early exits if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutUpdateValue({ - mode = 'null', - value = null, -} = {}) { - return compositeFrom(`exitWithoutUpdateValue`, [ - withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - exitIfAvailabilityCheckFailed({value}), - ]); -} + inputs: { + dependency: input.required(), + mode: input(availabilityCheckMode), + value: input({defaultValue: null}), + }, -// Raises if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutDependency({ - dependency, - mode = 'null', - map = {}, - raise = {}, -}) { - return compositeFrom(`raiseWithoutDependency`, [ - withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + steps: [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('value')], + continuation: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => (availability - ? continuation.raise() - : continuation()), + ? continuation() + : continuation.exit(value)), }, + ], +}); - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); -} +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export const exitWithoutUpdateValue = templateCompositeFrom({ + annotation: `exitWithoutUpdateValue`, + + inputs: { + mode: input(availabilityCheckMode), + value: input({defaultValue: null}), + }, + + steps: [ + exitWithoutDependency({ + dependency: input.updateValue(), + mode: input('mode'), + }), + ], +}); -// Raises if this property's update value isn't available. +// Raises if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutUpdateValue({ - mode = 'null', - map = {}, - raise = {}, -} = {}) { - return compositeFrom(`raiseWithoutUpdateValue`, [ - withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), +export const raiseOutputWithoutDependency = templateCompositeFrom({ + annotation: `raiseOutputWithoutDependency`, + + inputs: { + dependency: input.required(), + mode: input(availabilityCheckMode), + output: input({defaultValue: {}}), + }, + + steps: [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => (availability - ? continuation.raise() - : continuation()), + ? continuation() + : continuation.raiseOutputAbove(output)), }, + ], +}); - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); -} +// Raises if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ + annotation: `raiseOutputWithoutUpdateValue`, -// Turns an updating property's update value into a dependency, so it can be -// conveniently passed to other functions. -export function withUpdateValueAsDependency({ - into = '#updateValue', -} = {}) { - return { - annotation: `withUpdateValueAsDependency`, - flags: {expose: true, compose: true}, + inputs: { + mode: input(availabilityCheckMode), + output: input({defaultValue: {}}), + }, - expose: { - mapContinuation: {into}, - transform: (value, continuation) => - continuation(value, {into: value}), + steps: [ + withResultOfAvailabilityCheck({ + from: input.updateValue(), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => + (availability + ? continuation() + : continuation.raiseOutputAbove(output)), }, - }; -} + ], +}); // Gets a property of some object (in a dependency) and provides that value. // If the object itself is null, or the object doesn't have the listed property, // the provided dependency will also be null. -export function withPropertyFromObject({ - object, - property, - into = null, -}) { - into ??= - (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`); +export const withPropertyFromObject = templateCompositeFrom({ + annotation: `withPropertyFromObject`, - return { - annotation: `withPropertyFromObject`, - flags: {expose: true, compose: true}, + inputs: { + object: input({type: 'object', null: true}), + property: input.required({type: 'string'}), + } - expose: { - mapDependencies: {object}, - mapContinuation: {into}, - options: {property}, + outputs: { + into: { + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + default: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => + (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`), + }, + }, - compute: ({object, '#options': {property}}, continuation) => - (object === null || object === undefined + steps: [ + { + dependencies: [input('object'), input('property')], + compute: (continuation, { + [input('object')]: object, + [input('property')]: property, + }) => + (object === null ? continuation({into: null}) : continuation({into: object[property] ?? null})), }, - }; -} + ], +}); // Gets the listed properties from some object, providing each property's value // as a dependency prefixed with the same name as the object (by default). -- cgit 1.3.0-6-gf8a5 From 194676f45f54d09a3ad247e9ba4e2b3ba2e56db4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 15 Sep 2023 20:02:44 -0300 Subject: data: experimental templateCompositeFrom implementation --- src/data/things/composite.js | 374 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 3e766b2c..091faa3a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -7,6 +7,7 @@ import { empty, filterProperties, openAggregate, + decorateErrorWithIndex, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -344,6 +345,379 @@ import { const globalCompositeCache = {}; +export function input(nameOrDescription) { + if (typeof nameOrDescription === 'string') { + return Symbol.for(`hsmusic.composite.input:${nameOrDescription}`); + } else { + return { + symbol: Symbol.for('hsmusic.composite.input'), + shape: 'input', + value: nameOrDescription, + }; + } +} + +input.symbol = Symbol.for('hsmusic.composite.input'); + +input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); +input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); +input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); +input.staticValue = name => Symbol.for(`hsmusic.composite.input.staticValue:${name}`); + +function isInputToken(token) { + if (typeof token === 'object') { + return token.symbol === Symbol.for('hsmusic.composite.input'); + } else if (typeof token === 'symbol') { + return token.description.startsWith('hsmusic.composite.input'); + } else { + return false; + } +} + +function getInputTokenShape(token) { + if (!isInputToken(token)) { + throw new TypeError(`Expected an input token, got ${token}`); + } + + if (typeof token === 'object') { + return token.shape; + } else { + return token.description.match(/hsmusic\.composite\.(input.*?)(:|$)/)[1]; + } +} + +function getInputTokenValue(token) { + if (!isInputToken(token)) { + throw new TypeError(`Expected an input token, got ${token}`); + } + + if (typeof token === 'object') { + return token.value; + } else { + return token.description.match(/hsmusic\.composite\.input.*?:(.*)/)?.[1] ?? null; + } +} + +export function templateCompositeFrom(description) { + const compositeName = + (description.annotation + ? description.annotation + : `unnamed composite`); + + const descriptionAggregate = openAggregate({message: `Errors in description for ${compositeName}`}); + + if ('steps' in description) { + if (Array.isArray(description.steps)) { + descriptionAggregate.push(new TypeError(`Wrap steps array in a function`)); + } else if (typeof description.steps !== 'function') { + descriptionAggregate.push(new TypeError(`Expected steps to be a function (returning an array)`)); + } + } + + descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; + + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); + continue; + } + + if (getInputTokenShape(value) !== 'input') { + wrongCallsToInput.push(name); + } + } + + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); + } + + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input(), got ${shape}`)); + } + }); + + descriptionAggregate.nest({message: `Errors in output descriptions for ${compositeName}`}, ({map, push}) => { + const wrongType = []; + const notPrivate = []; + + const missingDependenciesDefault = []; + const wrongDependenciesType = []; + const wrongDefaultType = []; + + for (const [name, value] of Object.entries(description.outputs ?? {})) { + if (typeof value === 'object') { + if (!('dependencies' in value && 'default' in value)) { + missingDependenciesDefault.push(name); + continue; + } + + if (!Array.isArray(value.dependencies)) { + wrongDependenciesType.push(name); + } + + if (typeof value.default !== 'function') { + wrongDefaultType.push(name); + } + + continue; + } + + if (typeof value !== 'string') { + wrongType.push(name); + continue; + } + + if (!value.startsWith('#')) { + notPrivate.push(name); + continue; + } + } + + for (const name of wrongType) { + const type = typeof description.outputs[name]; + push(new Error(`${name}: Expected string, got ${type}`)); + } + + for (const name of notPrivate) { + const into = description.outputs[name]; + push(new Error(`${name}: Expected "#" at start, got ${into}`)); + } + + for (const name of missingDependenciesDefault) { + push(new Error(`${name}: Expected both dependencies & default`)); + } + + for (const name of wrongDependenciesType) { + const {dependencies} = description.outputs[name]; + push(new Error(`${name}: Expected dependencies to be array, got ${dependencies}`)); + } + + for (const name of wrongDefaultType) { + const type = typeof description.outputs[name].default; + push(new Error(`${name}: Expected default to be function, got ${type}`)); + } + + for (const [name, value] of Object.entries(description.outputs ?? {})) { + if (typeof value !== 'object') continue; + + map( + description.outputs[name].dependencies, + decorateErrorWithIndex(dependency => { + if (!isInputToken(dependency)) { + throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${dependency}`); + } + + const shape = getInputTokenShape(dependency); + if (shape !== 'input.staticValue' && shape !== 'input.staticDependency') { + throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${shape}`); + } + }), + {message: `${name}: Errors in dependencies`}); + } + }); + + descriptionAggregate.close(); + + const expectedInputNames = + (description.inputs + ? Object.keys(description.inputs) + : []); + + const expectedOutputNames = + (description.outputs + ? Object.keys(description.outputs) + : []); + + return (inputOptions = {}) => { + const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); + + const providedInputNames = Object.keys(inputOptions); + + const misplacedInputNames = + providedInputNames + .filter(name => !expectedInputNames.includes(name)); + + const missingInputNames = + expectedInputNames + .filter(name => !providedInputNames.includes(name)) + .filter(name => { + const inputDescription = description.inputs[name].value; + if (!inputDescription) return true; + if ('defaultValue' in inputDescription) return false; + if ('defaultDependency' in inputDescription) return false; + if (inputDescription.null === true) return false; + return true; + }); + + const wrongTypeInputNames = []; + const wrongInputCallInputNames = []; + + for (const [name, value] of Object.entries(inputOptions)) { + if (misplacedInputNames.includes(name)) { + continue; + } + + if (typeof value !== 'string' && !isInputToken(value)) { + wrongTypeInputNames.push(name); + continue; + } + } + + if (!empty(misplacedInputNames)) { + inputOptionsAggregate.push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); + } + + if (!empty(missingInputNames)) { + inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); + } + + for (const name of wrongTypeInputNames) { + const type = typeof inputOptions[name]; + inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); + } + + inputOptionsAggregate.close(); + + const outputOptions = {}; + + const instantiatedTemplate = { + symbol: templateCompositeFrom.symbol, + + outputs(providedOptions) { + const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositeName}`}); + + const misplacedOutputNames = []; + const wrongTypeOutputNames = []; + const notPrivateOutputNames = []; + + for (const [name, value] of Object.entries(providedOptions)) { + if (!expectedOutputNames.includes(name)) { + misplacedOutputNames.push(name); + continue; + } + + if (typeof value !== 'string') { + wrongTypeOutputNames.push(name); + continue; + } + + if (!value.startsWith('#')) { + notPrivateOutputNames.push(name); + continue; + } + } + + if (!empty(misplacedOutputNames)) { + outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames}`)); + } + + for (const name of wrongTypeOutputNames) { + const type = typeof providedOptions[name]; + outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); + } + + for (const name of notPrivateOutputNames) { + const into = providedOptions[name]; + outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); + } + + outputOptionsAggregate.close(); + + Object.assign(outputOptions, providedOptions); + return instantiatedTemplate; + }, + + toDescription() { + const finalDescription = {}; + + if ('annotation' in description) { + finalDescription.annotation = description.annotation; + } + + if ('update' in description) { + finalDescription.update = description.update; + } + + if ('inputs' in description) { + const finalInputs = {}; + + for (const [name, description_] of Object.entries(description.inputs)) { + const description = description_; + if (name in inputOptions) { + if (typeof inputOptions[name] === 'string') { + finalInputs[name] = input.dependency(inputOptions[name]); + } else { + finalInputs[name] = inputOptions[name]; + } + } else if (description.defaultValue) { + finalInputs[name] = input.value(defaultValue); + } else if (description.defaultDependency) { + finalInputs[name] = input.dependency(defaultValue); + } else { + finalInputs[name] = input.value(null); + } + } + + finalDescription.inputs = finalInputs; + } + + if ('outputs' in description) { + const finalOutputs = {}; + + for (const [name, defaultDependency] of Object.entries(description.outputs)) { + if (name in outputOptions) { + finalOutputs[name] = outputOptions[name]; + } else { + finalOutputs[name] = defaultDependency; + } + } + + finalDescription.outputs = finalOutputs; + } + + if ('steps' in description) { + finalDescription.steps = description.steps; + } + + return finalDescription; + }, + + toResolvedComposition() { + const ownDescription = instantiatedTemplate.toDescription(); + + const finalDescription = {...ownDescription}; + + const aggregate = openAggregate({message: `Errors resolving ${compositeName}`}); + + const steps = ownDescription.steps(); + + const resolvedSteps = + aggregate.map( + steps, + decorateErrorWithIndex(step => + (step.symbol === templateCompositeFrom.symbol + ? step.toResolvedComposition() + : step)), + {message: `Errors resolving steps`}); + + aggregate.close(); + + finalDescription.steps = resolvedSteps; + + return finalDescription; + }, + }; + + return instantiatedTemplate; + }; +} + +templateCompositeFrom.symbol = Symbol(); + export function compositeFrom(description) { const {annotation, steps: composition} = description; -- cgit 1.3.0-6-gf8a5 From 7cd3bdc4998ae1fc1b9ab4bb721d2727f64511e1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 15 Sep 2023 20:03:25 -0300 Subject: data: miscellaneous composite template updates --- src/data/things/album.js | 10 +- src/data/things/composite.js | 23 +- src/data/things/homepage-layout.js | 11 +- src/data/things/thing.js | 543 ++++++++++++++++++------------------- src/data/things/track.js | 93 +++---- 5 files changed, 323 insertions(+), 357 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index b134b78d..805d177d 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -12,7 +12,6 @@ import { withFlattenedArray, withPropertiesFromList, withUnflattenedArray, - withUpdateValueAsDependency, } from '#composite'; import Thing, { @@ -103,16 +102,15 @@ export class Album extends Thing { exitWithoutDependency({dependency: 'trackData', value: []}), exitWithoutUpdateValue({value: [], mode: 'empty'}), - withUpdateValueAsDependency({into: '#sections'}), - withPropertiesFromList({ - list: '#sections', - properties: [ + list: input.updateValue(), + prefix: input.value('#sections'), + properties: input.value([ 'tracks', 'dateOriginallyReleased', 'isDefaultTrackSection', 'color', - ], + ]), }), fillMissingListItems({list: '#sections.tracks', value: []}), diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 091faa3a..cd713169 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { @@ -1357,7 +1358,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ into: '#availability', }, - steps: [ + steps: () => [ { dependencies: [input('from'), input('mode')], @@ -1440,12 +1441,12 @@ export const exitWithoutDependency = templateCompositeFrom({ annotation: `exitWithoutDependency`, inputs: { - dependency: input.required(), + dependency: input(), mode: input(availabilityCheckMode), - value: input({defaultValue: null}), + value: input({null: true}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), mode: input('mode'), @@ -1474,7 +1475,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ value: input({defaultValue: null}), }, - steps: [ + steps: () => [ exitWithoutDependency({ dependency: input.updateValue(), mode: input('mode'), @@ -1488,12 +1489,12 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ annotation: `raiseOutputWithoutDependency`, inputs: { - dependency: input.required(), + dependency: input(), mode: input(availabilityCheckMode), output: input({defaultValue: {}}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), mode: input('mode'), @@ -1522,7 +1523,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ output: input({defaultValue: {}}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input.updateValue(), mode: input('mode'), @@ -1549,8 +1550,8 @@ export const withPropertyFromObject = templateCompositeFrom({ inputs: { object: input({type: 'object', null: true}), - property: input.required({type: 'string'}), - } + property: input({type: 'string'}), + }, outputs: { into: { @@ -1569,7 +1570,7 @@ export const withPropertyFromObject = templateCompositeFrom({ }, }, - steps: [ + steps: () => [ { dependencies: [input('object'), input('property')], compute: (continuation, { diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index b509c1e2..1d86f4d0 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -3,7 +3,6 @@ import find from '#find'; import { compositeFrom, exposeDependency, - withUpdateValueAsDependency, } from '#composite'; import { @@ -115,22 +114,20 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { : continuation(value)), }, - withUpdateValueAsDependency(), - withResolvedReference({ - ref: '#updateValue', + ref: input.updateValue(), data: 'groupData', - find: find.group, + find: input.value(find.group), }), exposeDependency({ dependency: '#resolvedReference', - update: { + update: input.value({ validate: oneOf( is('new-releases', 'new-additions'), validateReference(Group[Thing.referenceType])), - }, + }), }), ]), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5cfeaeb2..d1a8fdc1 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -7,6 +7,7 @@ import {colors} from '#cli'; import find from '#find'; import {empty, stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; +import {oneOf} from '#validators'; import { compositeFrom, @@ -14,10 +15,11 @@ import { exposeConstant, exposeDependency, exposeDependencyOrContinue, - raiseWithoutDependency, + input, + raiseOutputWithoutDependency, + templateCompositeFrom, withResultOfAvailabilityCheck, withPropertiesFromList, - withUpdateValueAsDependency, } from '#composite'; import { @@ -208,7 +210,7 @@ export function contributionList() { update: {validate: isContributionList}, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedContribs({from: '#updateValue'}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), @@ -288,7 +290,7 @@ export function referenceList({ '#composition.findFunction': findFunction, }, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedReferenceList({ @@ -332,7 +334,7 @@ export function singleReference({ '#composition.findFunction': findFunction, }, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedReference({ @@ -358,7 +360,7 @@ export function contribsPresent({ '#composition.contribs': contribs, }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ fromDependency: '#composition.contribs', mode: 'empty', @@ -383,7 +385,7 @@ export function reverseReferenceList({data, list}) { '#composition.list': list, }, - steps: [ + steps: () => [ withReverseReferenceList({ data: '#composition.data', list: '#composition.list', @@ -416,7 +418,7 @@ export function commentatorArtists() { '#composition.findFunction': find.artists, }, - steps: [ + steps: () => [ exitWithoutDependency({ dependency: 'commentary', mode: 'falsy', @@ -462,99 +464,97 @@ export function commentatorArtists() { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({ - from, - into = '#resolvedContribs', -}) { - return compositeFrom({ - annotation: `withResolvedContribs`, - - mapDependencies: { - '#composition.from': from, - }, - - mapContinuation: { - '#composition.into': into, - }, - - constantDependencies: { - '#composition.findFunction': find.artist, - '#composition.notFoundMode': 'null', - }, - - steps: [ - raiseWithoutDependency({ - dependency: '#composition.from', - raise: {'#composition.into': []}, - mode: 'empty', - }), - - withPropertiesFromList({ - list: '#composition.from', - prefix: '#contribs', - properties: ['who', 'what'], - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - into: '#contribs.who', - find: '#composition.findFunction', - notFoundMode: '#composition.notFoundMode', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - compute({'#contribs.who': who, '#contribs.what': what}, continuation) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - '#composition.into': stitchArrays({who, what}), - }); - }, +export const withResolvedContribs = templateCompositeFrom({ + annotation: `withResolvedContribs`, + + inputs: { + // todo: validate + from: input(), + + findFunction: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('exit', 'filter', 'null'), + defaultValue: 'null', + }), + }, + + outputs: { + into: '#resolvedContribs', + }, + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('from'), + mode: input.value('empty'), + output: input.value({into: []}), + }), + + withPropertiesFromList({ + list: input('from'), + properties: input.value(['who', 'what']), + prefix: input.value('#contribs'), + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + into: '#contribs.who', + find: input('find'), + notFoundMode: input('notFoundMode'), + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + + compute(continuation, { + ['#contribs.who']: who, + ['#contribs.what']: what, + }) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + '#composition.into': stitchArrays({who, what}), + }); }, - ], - }); -} + }, + ], +}); // Shorthand for exiting if the contribution list (usually a property's update // value) resolves to empty - ensuring that the later computed results are only // returned if these contributions are present. -export function exitWithoutContribs({ - contribs, - value = null, -}) { - return compositeFrom({ - annotation: `exitWithoutContribs`, - - mapDependencies: { - '#composition.contribs': contribs, +export const exitWithoutContribs = templateCompositeFrom({ + annotation: `exitWithoutContribs`, + + inputs: { + // todo: validate + contribs: input(), + + value: input({null: true}), + }, + + steps: () => [ + withResolvedContribs({ + from: input('contribs'), + }), + + withResultOfAvailabilityCheck({ + from: '#resolvedContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), }, - - constantDependencies: { - '#composition.value': value, - }, - - steps: [ - withResolvedContribs({from: '#composition.contribs'}), - - withResultOfAvailabilityCheck({ - fromDependency: '#resolvedContribs', - mode: 'empty', - }), - - { - dependencies: ['#availability', '#composition.value'], - compute: ({ - '#availability': availability, - '#composition.value': value, - }, continuation) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], - }); -} + ], +}); // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the @@ -562,204 +562,187 @@ export function exitWithoutContribs({ // function doesn't match anything for the reference. Otherwise, the data // object is provided on the output dependency; or null, if the reference // doesn't match anything or itself was null to begin with. -export function withResolvedReference({ - ref, - data, - find: findFunction, - into = '#resolvedReference', - notFoundMode, -}) { - return compositeFrom({ - annotation: `withResolvedReference`, - - mapDependencies: { - '#composition.ref': ref, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, - - constantDependencies: { - '#composition.notFoundMode': 'null', - }, - - mapContinuation: { - '#composition.into': into, - }, - - steps: [ - raiseWithoutDependency({ - dependency: '#composition.ref', - raise: {'#composition.into': null}, - }), - - exitWithoutDependency({ - dependency: '#composition.data', - }), - - { - compute({ - '#composition.ref': ref, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } - - return continuation.raise({match}); - }, +export const withResolvedReference = templateCompositeFrom({ + annotation: `withResolvedReference`, + + inputs: { + // todo: validate + ref: input(), + + // todo: validate + data: input(), + + find: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('null', 'exit'), + defaultValue: 'null', + }), + }, + + outputs: { + into: '#resolvedReference', + }, + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('ref'), + output: input.value({into: null}), + }), + + exitWithoutDependency({ + dependency: input('data'), + }), + + { + dependencies: [ + input('ref'), + input('data'), + input('find'), + input('notFoundMode'), + ], + + compute({ + [input('ref')]: ref, + [input('data')]: data, + [input('find')]: findFunction, + [input('notFoundMode')]: notFoundMode, + }, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raise({match}); }, - ], - }); -} + }, + ], +}); // Resolves a list of references, with each reference matched with provided // data in the same way as withResolvedReference. This will early exit if the // data dependency is null (even if the reference list is empty). By default // it will filter out references which don't match, but this can be changed // to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). -export function withResolvedReferenceList({ - list, - data, - find: findFunction, - notFoundMode, - into = '#resolvedReferenceList', -}) { - if (!['filter', 'exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); - } - - return compositeFrom({ - annotation: `withResolvedReferenceList`, - - mapDependencies: { - '#composition.list': list, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, - - constantDependencies: { - '#composition.notFoundMode': 'filter', +export const withResolvedReferenceList = templateCompositeFrom({ + annotation: `withResolvedReferenceList`, + + inputs: { + // todo: validate + list: input(), + + // todo: validate + data: input(), + + find: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('exit', 'filter', 'null'), + defaultValue: 'filter', + }), + }, + + outputs: { + into: '#resolvedReferenceList', + }, + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({into: []}), + }), + + { + dependencies: [input('list'), input('data'), input('find')], + compute: ({ + [input('list')]: list, + [input('data')]: data, + [input('find')]: findFunction, + }, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), }, - mapContinuation: { - '#composition.into': into, + { + dependencies: ['#matches'], + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({'#continuation.into': matches}) + : continuation()), }, - steps: [ - exitWithoutDependency({ - dependency: '#composition.data', - value: [], - }), - - raiseWithoutDependency({ - dependency: '#composition.list', - raise: {'#composition.into': []}, - mode: 'empty', - }), - - { - dependencies: [ - '#composition.list', - '#composition.data', - '#composition.findFunction', - ], - - compute: ({ - '#composition.list': list, - '#composition.data': data, - '#composition.findFunction': findFunction, - }, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), + { + dependencies: ['#matches', input('notFoundMode')], + compute({ + ['#matches']: matches, + [input('notFoundMode')]: notFoundMode, + }, continuation) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({'#continuation.into': matches}); + + case 'null': + matches = matches.map(match => match ?? null); + return continuation.raise({'#continuation.into': matches}); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } }, - - { - dependencies: ['#matches'], - compute: ({'#matches': matches}, continuation) => - (matches.every(match => match) - ? continuation.raise({'#continuation.into': matches}) - : continuation()), - }, - - { - dependencies: ['#matches', '#composition.notFoundMode'], - compute({ - '#matches': matches, - '#composition.notFoundMode': notFoundMode, - }, continuation) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({'#continuation.into': matches}); - - case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({'#continuation.into': matches}); - - default: - throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); - } - }, - }, - ], - }); -} + }, + ], +}); // Check out the info on reverseReferenceList! // This is its composable form. -export function withReverseReferenceList({ - data, - list: refListProperty, - into = '#reverseReferenceList', -}) { - return compositeFrom({ - annotation: `withReverseReferenceList`, - - mapDependencies: { - '#composition.data': data, - }, - - constantDependencies: { - '#composition.refListProperty': refListProperty, - }, - - mapContinuation: { - '#composition.into': into, +export const withReverseReferenceList = templateCompositeFrom({ + annotation: `withReverseReferenceList`, + + inputs: { + // todo: validate + data: input(), + + list: input({type: 'string'}), + }, + + outputs: { + into: '#reverseReferenceList', + }, + + steps: () => [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + { + dependencies: [ + 'this', + '#composition.data', + '#composition.refListProperty', + ], + + compute: ({ + this: thisThing, + '#composition.data': data, + '#composition.refListProperty': refListProperty, + }, continuation) => + continuation({ + '#composition.into': + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), }, - - steps: [ - exitWithoutDependency({ - dependency: '#composition.data', - value: [], - }), - - { - dependencies: [ - 'this', - '#composition.data', - '#composition.refListProperty', - ], - - compute: ({ - this: thisThing, - '#composition.data': data, - '#composition.refListProperty': refListProperty, - }, continuation) => - continuation({ - '#composition.into': - data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ], - }); -} + ], +}); diff --git a/src/data/things/track.js b/src/data/things/track.js index a8d59023..ccfbc357 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -11,9 +11,11 @@ import { exposeDependency, exposeDependencyOrContinue, exposeUpdateValueOrContinue, + input, + raiseOutputWithoutDependency, + templateCompositeFrom, withPropertyFromObject, withResultOfAvailabilityCheck, - withUpdateValueAsDependency, } from '#composite'; import { @@ -21,6 +23,7 @@ import { isContributionList, isDate, isFileExtension, + oneOf, } from '#validators'; import CacheableObject from './cacheable-object.js'; @@ -139,8 +142,10 @@ export class Track extends Thing { artistContribs: [ inheritFromOriginalRelease({property: 'artistContribs'}), - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue', into: '#artistContribs'}), + withResolvedContribs({ + from: input.updateValue(), + }).outputs({into: '#artistContribs'}), + exposeDependencyOrContinue({dependency: '#artistContribs'}), withPropertyFromAlbum({property: 'artistContribs'}), @@ -161,8 +166,10 @@ export class Track extends Thing { coverArtistContribs: [ exitWithoutUniqueCoverArt(), - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}), + withResolvedContribs({ + from: input.updateValue(), + }).outputs({into: '#coverArtistContribs'}), + exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), @@ -302,6 +309,7 @@ export class Track extends Thing { } } +/* // Early exits with a value inherited from the original release, if // this track is a rerelease, and otherwise continues with no further // dependencies provided. If allowOverride is true, then the continuation @@ -327,77 +335,55 @@ function inheritFromOriginalRelease({ }, ]); } +*/ // Gets the track's album. This will early exit if albumData is missing. // By default, if there's no album whose list of tracks includes this track, // the output dependency will be null; set {notFoundMode: 'exit'} to early // exit instead. -function withAlbum({ - into = '#album', - notFoundMode = 'null', -} = {}) { - return compositeFrom(`withAlbum`, [ - withResultOfAvailabilityCheck({ - fromDependency: 'albumData', - mode: 'empty', - into: '#albumDataAvailability', +export const withAlbum = templateCompositeFrom({ + annotation: `Track.withAlbum`, + + inputs: { + notFoundMode: input({ + validate: oneOf('exit', 'null'), + defaultValue: 'null', }), + }, - { - dependencies: ['#albumDataAvailability'], - options: {notFoundMode}, - mapContinuation: {into}, + outputs: { + into: '#album', + }, - compute: ({ - '#albumDataAvailability': albumDataAvailability, - '#options': {notFoundMode}, - }, continuation) => - (albumDataAvailability - ? continuation() - : (notFoundMode === 'exit' - ? continuation.exit(null) - : continuation.raise({into: null}))), - }, + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'albumData', + mode: input.value('empty'), + output: input.value({into: null}), + }), { dependencies: ['this', 'albumData'], - compute: ({this: track, albumData}, continuation) => + compute: (continuation, {this: track, albumData}) => continuation({ '#album': albumData.find(album => album.tracks.includes(track)), }), }, - withResultOfAvailabilityCheck({ - fromDependency: '#album', - mode: 'null', - into: '#albumAvailability', + raiseOutputWithoutDependency({ + dependency: '#album', + output: input.value({into: null}), }), - { - dependencies: ['#albumAvailability'], - options: {notFoundMode}, - mapContinuation: {into}, - - compute: ({ - '#albumAvailability': albumAvailability, - '#options': {notFoundMode}, - }, continuation) => - (albumAvailability - ? continuation() - : (notFoundMode === 'exit' - ? continuation.exit(null) - : continuation.raise({into: null}))), - }, - { dependencies: ['#album'], - mapContinuation: {into}, - compute: ({'#album': album}, continuation) => + compute: (continuation, {'#album': album}) => continuation({into: album}), }, - ]); -} + ], +}); +/* // Gets a single property from this track's album, providing it as the same // property name prefixed with '#album.' (by default). If the track's album // isn't available, then by default, the property will be provided as null; @@ -571,3 +557,4 @@ function trackReverseReferenceList({ }, ]); } +*/ -- cgit 1.3.0-6-gf8a5 From b4dd9d3f288130acdd9fefa2321b4b547f348b32 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 16 Sep 2023 13:03:26 -0300 Subject: data: more WIP syntax updates --- src/data/things/album.js | 1 + src/data/things/composite.js | 14 +- src/data/things/homepage-layout.js | 1 + src/data/things/thing.js | 72 ++++++--- src/data/things/track.js | 299 ++++++++++++++++++++++--------------- 5 files changed, 244 insertions(+), 143 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 805d177d..c0042ae2 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -8,6 +8,7 @@ import { exitWithoutUpdateValue, exposeDependency, exposeUpdateValueOrContinue, + input, fillMissingListItems, withFlattenedArray, withPropertiesFromList, diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cd713169..f2ca2c7c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -420,7 +420,7 @@ export function templateCompositeFrom(description) { const missingCallsToInput = []; const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.inputs)) { + for (const [name, value] of Object.entries(description.inputs ?? {})) { if (!isInputToken(value)) { missingCallsToInput.push(name); continue; @@ -533,7 +533,7 @@ export function templateCompositeFrom(description) { ? Object.keys(description.outputs) : []); - return (inputOptions = {}) => { + const instantiate = (inputOptions = {}) => { const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); const providedInputNames = Object.keys(inputOptions); @@ -593,7 +593,7 @@ export function templateCompositeFrom(description) { const misplacedOutputNames = []; const wrongTypeOutputNames = []; - const notPrivateOutputNames = []; + // const notPrivateOutputNames = []; for (const [name, value] of Object.entries(providedOptions)) { if (!expectedOutputNames.includes(name)) { @@ -606,10 +606,12 @@ export function templateCompositeFrom(description) { continue; } + /* if (!value.startsWith('#')) { notPrivateOutputNames.push(name); continue; } + */ } if (!empty(misplacedOutputNames)) { @@ -621,10 +623,12 @@ export function templateCompositeFrom(description) { outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); } + /* for (const name of notPrivateOutputNames) { const into = providedOptions[name]; outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); } + */ outputOptionsAggregate.close(); @@ -715,6 +719,10 @@ export function templateCompositeFrom(description) { return instantiatedTemplate; }; + + instantiate.inputs = instantiate; + + return instantiate; } templateCompositeFrom.symbol = Symbol(); diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 1d86f4d0..007e0236 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -3,6 +3,7 @@ import find from '#find'; import { compositeFrom, exposeDependency, + input, } from '#composite'; import { diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d1a8fdc1..45e91238 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -35,6 +35,7 @@ import { isFileExtension, isName, isString, + isType, isURL, validateArrayItems, validateInstanceOf, @@ -211,8 +212,7 @@ export function contributionList() { update: {validate: isContributionList}, steps: () => [ - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue'}), + withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), exposeConstant({value: []}), ], @@ -261,27 +261,61 @@ export function additionalFiles() { // 'artist' or 'track', but this utility keeps from having to hard-code the // string in multiple places by referencing the value saved on the class // instead. +export const referenceList = templateCompositeFrom({ + annotation: `referenceList`, + + compose: false, + + inputs: { + class: input({ + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, + }), + + find: input({type: 'function'}), + + // todo: validate + data: input(), + }, + + update: { + dependencies: [ + input.staticValue('class'), + ], + + compute({ + [input.staticValue('class')]: thingClass, + }) { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; + }, + }, + + steps: () => [ + withResolvedReferenceList({ + list: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ], +}) export function referenceList({ class: thingClass, data, find: findFunction, }) { - if (!thingClass) { - throw new TypeError(`Expected a Thing class`); - } - - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - return compositeFrom({ annotation: `referenceList`, - update: { - validate: validateReferenceList(referenceType), - }, - mapDependencies: { '#composition.data': data, }, @@ -292,14 +326,6 @@ export function referenceList({ steps: () => [ withUpdateValueAsDependency(), - - withResolvedReferenceList({ - list: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), ], }); } diff --git a/src/data/things/track.js b/src/data/things/track.js index ccfbc357..870b9913 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -5,7 +5,6 @@ import find from '#find'; import {empty} from '#sugar'; import { - compositeFrom, exitWithoutDependency, exposeConstant, exposeDependency, @@ -15,7 +14,6 @@ import { raiseOutputWithoutDependency, templateCompositeFrom, withPropertyFromObject, - withResultOfAvailabilityCheck, } from '#composite'; import { @@ -142,9 +140,9 @@ export class Track extends Thing { artistContribs: [ inheritFromOriginalRelease({property: 'artistContribs'}), - withResolvedContribs({ - from: input.updateValue(), - }).outputs({into: '#artistContribs'}), + withResolvedContribs + .inputs({from: input.updateValue()}) + .outputs({into: '#artistContribs'}), exposeDependencyOrContinue({dependency: '#artistContribs'}), @@ -166,9 +164,9 @@ export class Track extends Thing { coverArtistContribs: [ exitWithoutUniqueCoverArt(), - withResolvedContribs({ - from: input.updateValue(), - }).outputs({into: '#coverArtistContribs'}), + withResolvedContribs + .inputs({from: input.updateValue()}) + .outputs({into: '#coverArtistContribs'}), exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), @@ -271,12 +269,12 @@ export class Track extends Thing { // the "Tracks - by Times Referenced" listing page (or other data // processing). referencedByTracks: trackReverseReferenceList({ - property: 'referencedTracks', + list: 'referencedTracks', }), // For the same reasoning, exclude re-releases from sampled tracks too. sampledByTracks: trackReverseReferenceList({ - property: 'sampledTracks', + list: 'sampledTracks', }), featuredInFlashes: reverseReferenceList({ @@ -309,33 +307,44 @@ export class Track extends Thing { } } -/* // Early exits with a value inherited from the original release, if // this track is a rerelease, and otherwise continues with no further // dependencies provided. If allowOverride is true, then the continuation // will also be called if the original release exposed the requested // property as null. -function inheritFromOriginalRelease({ - property: originalProperty, - allowOverride = false, -}) { - return compositeFrom(`inheritFromOriginalRelease`, [ +export const inheritFromOriginalRelease = templateCompositeFrom({ + annotation: `Track.inheritFromOriginalRelease`, + + inputs: { + property: input({type: 'string'}), + allowOverride: input({type: 'boolean', defaultValue: false}), + }, + + steps: () => [ withOriginalRelease(), { - dependencies: ['#originalRelease'], - compute({'#originalRelease': originalRelease}, continuation) { - if (!originalRelease) return continuation.raise(); + dependencies: [ + '#originalRelease', + input('property'), + input('allowOverride'), + ], + + compute: (continuation, { + ['#originalRelease']: originalRelease, + [input('property')]: originalProperty, + [input('allowOverride')]: allowOverride, + }) => { + if (!originalRelease) return continuation(); const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation.raise(); + if (allowOverride && value === null) return continuation(); return continuation.exit(value); }, }, - ]); -} -*/ + ], +}); // Gets the track's album. This will early exit if albumData is missing. // By default, if there's no album whose list of tracks includes this track, @@ -383,64 +392,95 @@ export const withAlbum = templateCompositeFrom({ ], }); -/* // Gets a single property from this track's album, providing it as the same // property name prefixed with '#album.' (by default). If the track's album // isn't available, then by default, the property will be provided as null; // set {notFoundMode: 'exit'} to early exit instead. -function withPropertyFromAlbum({ - property, - into = '#album.' + property, - notFoundMode = 'null', -}) { - return compositeFrom(`withPropertyFromAlbum`, [ - withAlbum({notFoundMode}), - withPropertyFromObject({object: '#album', property, into}), - ]); -} +export const withPropertyFromAlbum = templateCompositeFrom({ + annotation: `withPropertyFromAlbum`, + + inputs: { + property: input({type: 'string'}), + + notFoundMode: input({ + validate: oneOf('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: { + into: { + dependencies: [input.staticValue('property')], + default: ({ + [input.staticValue('property')]: property, + }) => '#album.' + property, + }, + }, + + steps: () => [ + withAlbum({ + notFoundMode: input('notFoundMode'), + }), + + withPropertyFromObject + .inputs({object: '#album', property: input('property')}) + .outputs({into: 'into'}), + ], +}); // Gets the track section containing this track from its album's track list. // If notFoundMode is set to 'exit', this will early exit if the album can't be // found or if none of its trackSections includes the track for some reason. -function withContainingTrackSection({ - into = '#trackSection', - notFoundMode = 'null', -} = {}) { - if (!['exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be exit or null`); - } +export const withContainingTrackSection = templateCompositeFrom({ + annotation: `withContainingTrackSection`, + + inputs: { + notFoundMode: input({ + validate: oneOf('exit', 'null'), + defaultValue: 'null', + }), + }, - return compositeFrom(`withContainingTrackSection`, [ - withPropertyFromAlbum({property: 'trackSections', notFoundMode}), + outputs: { + into: '#trackSection', + }, + + steps: () => [ + withPropertyFromAlbum({ + property: input.value('trackSections'), + notFoundMode: input('notFoundMode'), + }), { - dependencies: ['this', '#album.trackSections'], - options: {notFoundMode}, - mapContinuation: {into}, - - compute({ - this: track, - '#album.trackSections': trackSections, - '#options': {notFoundMode}, - }, continuation) { + dependencies: [ + input.myself(), + input('notFoundMode'), + '#album.trackSections', + ], + + compute(continuation, { + [input.myself()]: track, + [input('notFoundMode')]: notFoundMode, + ['#album.trackSections']: trackSections, + }) { if (!trackSections) { - return continuation.raise({into: null}); + return continuation({into: null}); } const trackSection = trackSections.find(({tracks}) => tracks.includes(track)); if (trackSection) { - return continuation.raise({into: trackSection}); + return continuation({into: trackSection}); } else if (notFoundMode === 'exit') { return continuation.exit(null); } else { - return continuation.raise({into: null}); + return continuation({into: null}); } }, }, - ]); -} + ], +}); // Just includes the original release of this track as a dependency. // If this track isn't a rerelease, then it'll provide null, unless the @@ -448,29 +488,40 @@ function withContainingTrackSection({ // itself. Note that this will early exit if the original release is // specified by reference and that reference doesn't resolve to anything. // Outputs to '#originalRelease' by default. -function withOriginalRelease({ - into = '#originalRelease', - selfIfOriginal = false, -} = {}) { - return compositeFrom(`withOriginalRelease`, [ - withResolvedReference({ - ref: 'originalReleaseTrack', - data: 'trackData', - into: '#originalRelease', - find: find.track, - notFoundMode: 'exit', - }), +export const withOriginalRelease = templateCompositeFrom({ + annotation: `withOriginalRelease`, + + inputs: { + selfIfOriginal: input({type: 'boolean', defaultValue: false}), + }, + + outputs: { + into: '#originalRelease', + }, + + steps: () => [ + withResolvedReference + .inputs({ + ref: 'originalReleaseTrack', + data: 'trackData', + find: input.value(find.track), + notFoundMode: input.value('exit'), + }) + .outputs({into: '#originalRelease'}), { - dependencies: ['this', '#originalRelease'], - options: {selfIfOriginal}, - mapContinuation: {into}, - compute: ({ - this: track, - '#originalRelease': originalRelease, - '#options': {selfIfOriginal}, - }, continuation) => - continuation.raise({ + dependencies: [ + input.myself(), + input('selfIfOriginal'), + '#originalRelease', + ], + + compute: (continuation, { + [input.myself()]: track, + [input('selfIfOriginal')]: selfIfOriginal, + ['#originalRelease']: originalRelease, + }) => + continuation({ into: (originalRelease ?? (selfIfOriginal @@ -478,83 +529,97 @@ function withOriginalRelease({ : null)), }), }, - ]); -} + ], +}); // The algorithm for checking if a track has unique cover art is used in a // couple places, so it's defined in full as a compositional step. -function withHasUniqueCoverArt({ - into = '#hasUniqueCoverArt', -} = {}) { - return compositeFrom(`withHasUniqueCoverArt`, [ +export const withHasUniqueCoverArt = templateCompositeFrom({ + annotation: 'withHasUniqueCoverArt', + + outputs: { + into: '#hasUniqueCoverArt', + }, + + steps: () => [ { dependencies: ['disableUniqueCoverArt'], - mapContinuation: {into}, - compute: ({disableUniqueCoverArt}, continuation) => + compute: (continuation, {disableUniqueCoverArt}) => (disableUniqueCoverArt - ? continuation.raise({into: false}) + ? continuation.raiseOutput({into: false}) : continuation()), }, - withResolvedContribs({ - from: 'coverArtistContribs', - into: '#coverArtistContribs', - }), + withResolvedContribs + .inputs({from: 'coverArtistContribs'}) + .outputs({into: '#coverArtistContribs'}), { dependencies: ['#coverArtistContribs'], - mapContinuation: {into}, - compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => + compute: (continuation, { + ['#coverArtistContribs']: contribsFromTrack, + }) => (empty(contribsFromTrack) ? continuation() - : continuation.raise({into: true})), + : continuation.raiseOutput({into: true})), }, withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), { dependencies: ['#album.trackCoverArtistContribs'], - mapContinuation: {into}, - compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => - (empty(contribsFromAlbum) - ? continuation.raise({into: false}) - : continuation.raise({into: true})), + compute: (continuation, { + ['#album.trackCoverArtistContribs']: contribsFromAlbum, + }) => + continuation({ + into: !empty(contribsFromAlbum), + }), }, - ]); -} + ], +}); // Shorthand for checking if the track has unique cover art and exposing a // fallback value if it isn't. -function exitWithoutUniqueCoverArt({ - value = null, -} = {}) { - return compositeFrom(`exitWithoutUniqueCoverArt`, [ +export const exitWithoutUniqueCoverArt = templateCompositeFrom({ + annotation: `exitWithoutUniqueCoverArt`, + + inputs: { + value: input({null: true}), + }, + + steps: () => [ withHasUniqueCoverArt(), + exitWithoutDependency({ dependency: '#hasUniqueCoverArt', mode: 'falsy', - value, + value: input('value'), }), - ]); -} + ], +}); + +export const trackReverseReferenceList = templateCompositeFrom({ + annotation: `trackReverseReferenceList`, + + inputs: { + list: input({type: 'string'}), + }, -function trackReverseReferenceList({ - property: refListProperty, -}) { - return compositeFrom(`trackReverseReferenceList`, [ + steps: () => [ withReverseReferenceList({ data: 'trackData', - list: refListProperty, + list: input('list'), }), { flags: {expose: true}, expose: { dependencies: ['#reverseReferenceList'], - compute: ({'#reverseReferenceList': reverseReferenceList}) => + compute: ({ + ['#reverseReferenceList']: reverseReferenceList, + }) => reverseReferenceList.filter(track => !track.originalReleaseTrack), }, }, - ]); -} -*/ + ], +}); -- cgit 1.3.0-6-gf8a5 From f3a855d772d51749c6f9d50632dc74792f902b29 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 16 Sep 2023 21:42:55 -0300 Subject: client: fix sticky headings not working on pages w/o cover --- src/static/client2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/static/client2.js b/src/static/client2.js index 78970410..d9afcb03 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -539,7 +539,7 @@ const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky- const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading')); const contentCover = contentContainer.querySelector('#cover-art-container'); - if (stickyCover.querySelector('.image-text-area')) { + if (stickyCover?.querySelector('.image-text-area')) { stickyCoverContainer.remove(); stickyCoverContainer = null; stickyCover = null; -- cgit 1.3.0-6-gf8a5 From fd102ee597e2ad2ba8f0950ce1a16fd34029963d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 18 Sep 2023 13:26:18 -0300 Subject: data: MORE composite wip --- src/data/things/composite.js | 44 +++---- src/data/things/thing.js | 265 +++++++++++++++++++------------------------ src/data/things/track.js | 45 +++++--- 3 files changed, 170 insertions(+), 184 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f2ca2c7c..c33fc03c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1349,7 +1349,7 @@ export function exposeConstant({ // for values like zero and the empty string! // -const availabilityCheckMode = { +const availabilityCheckModeInput = { validate: oneOf('null', 'empty', 'falsy'), defaultValue: 'null', }; @@ -1359,7 +1359,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ inputs: { from: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, outputs: { @@ -1403,7 +1403,7 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, steps: () => [ @@ -1432,7 +1432,7 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, steps: () => [ @@ -1450,7 +1450,7 @@ export const exitWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), value: input({null: true}), }, @@ -1479,7 +1479,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ annotation: `exitWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), value: input({defaultValue: null}), }, @@ -1498,7 +1498,7 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), output: input({defaultValue: {}}), }, @@ -1527,7 +1527,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ annotation: `raiseOutputWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), output: input({defaultValue: {}}), }, @@ -1562,19 +1562,21 @@ export const withPropertyFromObject = templateCompositeFrom({ }, outputs: { - into: { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - default: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`), + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + compute: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => { + return ( + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value')); }, }, diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 45e91238..a5f0b78d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import find from '#find'; -import {empty, stitchArrays, unique} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import {oneOf} from '#validators'; @@ -253,6 +253,18 @@ export function additionalFiles() { }; } +const thingClassInput = { + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, +}; + // A reference list! Keep in mind this is for general references to wiki // objects of (usually) other Thing subclasses, not specifically leitmotif // references in tracks (although that property uses referenceList too!). @@ -267,18 +279,7 @@ export const referenceList = templateCompositeFrom({ compose: false, inputs: { - class: input({ - validate(thingClass) { - isType(thingClass, 'function'); - - if (!Object.hasOwn(thingClass, Thing.referenceType)) { - throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); - } - - return true; - }, - }), - + class: input(thingClassInput), find: input({type: 'function'}), // todo: validate @@ -300,127 +301,100 @@ export const referenceList = templateCompositeFrom({ steps: () => [ withResolvedReferenceList({ - list: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', + list: input.updateValue(), + data: input('data'), + find: input('find'), }), exposeDependency({dependency: '#resolvedReferenceList'}), ], -}) -export function referenceList({ - class: thingClass, - data, - find: findFunction, -}) { - return compositeFrom({ - annotation: `referenceList`, - - mapDependencies: { - '#composition.data': data, - }, - - constantDependencies: { - '#composition.findFunction': findFunction, - }, - - steps: () => [ - withUpdateValueAsDependency(), - ], - }); -} +}); // Corresponding function for a single reference. -export function singleReference({ - class: thingClass, - data, - find: findFunction, -}) { - if (!thingClass) { - throw new TypeError(`Expected a Thing class`); - } +export const singleReference = templateCompositeFrom({ + annotation: `singleReference`, - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } + compose: false, - return compositeFrom({ - annotation: `singleReference`, + inputs: { + class: input(thingClassInput), + find: input({type: 'function'}), - update: { - validate: validateReference(referenceType), - }, + // todo: validate + data: input(), + }, - mapDependencies: { - '#composition.data': data, - }, + update: { + dependencies: [ + input.staticValue('class'), + ], - constantDependencies: { - '#composition.findFunction': findFunction, + compute({ + [input.staticValue('class')]: thingClass, + }) { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; }, + }, - steps: () => [ - withUpdateValueAsDependency(), - - withResolvedReference({ - ref: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', - }), + steps: () => [ + withResolvedReference({ + ref: input.updateValue(), + data: input('data'), + find: input('findFunction'), + }), - exposeDependency({dependency: '#resolvedReference'}), - ], - }); -} + exposeDependency({dependency: '#resolvedReference'}), + ], +}); // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent({ - contribs, -}) { - return compositeFrom({ - annotation: `contribsPresent`, +export const contribsPresent = templateCompositeFrom({ + annotation: `contribsPresent`, - mapDependencies: { - '#composition.contribs': contribs, - }, + compose: false, - steps: () => [ - withResultOfAvailabilityCheck({ - fromDependency: '#composition.contribs', - mode: 'empty', - }), + inputs: { + contribs: input({type: 'string'}), + }, - exposeDependency({dependency: '#availability'}), - ], - }); -} + steps: () => [ + withResultOfAvailabilityCheck({ + fromDependency: input('contribs'), + mode: input.value('empty'), + }), + + exposeDependency({dependency: '#availability'}), + ], +}); // Neat little shortcut for "reversing" the reference lists stored on other // things - for example, tracks specify a "referenced tracks" property, and // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. -export function reverseReferenceList({data, list}) { - return compositeFrom({ - annotation: `reverseReferenceList`, +export const reverseReferenceList = templateCompositeFrom({ + annotation: `reverseReferenceList`, - mapDependencies: { - '#composition.data': data, - '#composition.list': list, - }, + compose: false, - steps: () => [ - withReverseReferenceList({ - data: '#composition.data', - list: '#composition.list', - }), + inputs: { + // todo: validate + data: input(), - exposeDependency({dependency: '#reverseReferenceList'}), - ], - }); -} + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseReferenceList({ + data: input('data'), + list: input('list'), + }), + + exposeDependency({dependency: '#reverseReferenceList'}), + ], +}); // General purpose wiki data constructor, for properties like artistData, // trackData, etc. @@ -436,53 +410,50 @@ export function wikiData(thingClass) { // This one's kinda tricky: it parses artist "references" from the // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. -export function commentatorArtists() { - return compositeFrom({ - annotation: `commentatorArtists`, +export const commentatorArtists = templateCompositeFrom({ + annotation: `commentatorArtists`, + + compose: false, + + steps: () => [ + exitWithoutDependency({ + dependency: 'commentary', + mode: input.value('falsy'), + value: input.value([]), + }), - constantDependencies: { - '#composition.findFunction': find.artists, + { + dependencies: ['commentary'], + compute: (continuation, {commentary}) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/<i>(?<who>.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), }, - steps: () => [ - exitWithoutDependency({ - dependency: 'commentary', - mode: 'falsy', - value: [], - }), - - { - dependencies: ['commentary'], - compute: ({commentary}, continuation) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/<i>(?<who>.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), - }, + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + find: input.value(find.artist), + }).outputs({ + '#resolvedReferenceList': '#artists', + }), + + { + flags: {expose: true}, - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - into: '#artists', - find: '#composition.findFunction', - }), - - { - flags: {expose: true}, - - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), - }, + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), }, - ], - }); -} + }, + ], +}); // Compositional utilities diff --git a/src/data/things/track.js b/src/data/things/track.js index 870b9913..b41dbb5b 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -140,9 +140,11 @@ export class Track extends Thing { artistContribs: [ inheritFromOriginalRelease({property: 'artistContribs'}), - withResolvedContribs - .inputs({from: input.updateValue()}) - .outputs({into: '#artistContribs'}), + withResolvedContribs({ + from: input.updateValue(), + }).outputs({ + '#resolvedContribs': '#artistContribs', + }), exposeDependencyOrContinue({dependency: '#artistContribs'}), @@ -164,9 +166,11 @@ export class Track extends Thing { coverArtistContribs: [ exitWithoutUniqueCoverArt(), - withResolvedContribs - .inputs({from: input.updateValue()}) - .outputs({into: '#coverArtistContribs'}), + withResolvedContribs({ + from: input.updateValue(), + }).outputs({ + '#resolvedContribs': '#coverArtistContribs', + }), exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), @@ -400,7 +404,7 @@ export const withPropertyFromAlbum = templateCompositeFrom({ annotation: `withPropertyFromAlbum`, inputs: { - property: input({type: 'string'}), + property: input.staticValue({type: 'string'}), notFoundMode: input({ validate: oneOf('exit', 'null'), @@ -409,12 +413,10 @@ export const withPropertyFromAlbum = templateCompositeFrom({ }, outputs: { - into: { - dependencies: [input.staticValue('property')], - default: ({ - [input.staticValue('property')]: property, - }) => '#album.' + property, - }, + dependencies: [input.staticValue('property')], + compute: ({ + [input.staticValue('property')]: property, + }) => ['#album.' + property], }, steps: () => [ @@ -422,9 +424,20 @@ export const withPropertyFromAlbum = templateCompositeFrom({ notFoundMode: input('notFoundMode'), }), - withPropertyFromObject - .inputs({object: '#album', property: input('property')}) - .outputs({into: 'into'}), + withPropertyFromObject({ + object: '#album', + property: input('property'), + }), + + { + dependencies: ['#value', input.staticValue('property')], + compute: (continuation, { + ['#value']: value, + [input.staticValue('property')]: property, + }) => continuation({ + ['#album.' + property]: value, + }), + }, ], }); -- cgit 1.3.0-6-gf8a5 From fdd8f355bfe0992fc340f800297df524276b1946 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 18 Sep 2023 16:05:05 -0300 Subject: data: Track.alwaysReferencedByDirectory flag & field --- src/data/things/track.js | 35 +++++++++++++++++++++++++++++++++++ src/data/yaml.js | 2 ++ src/find.js | 24 ++++++++++++------------ 3 files changed, 49 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index e176acb4..14510d96 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -36,6 +36,41 @@ export class Track extends Thing { urls: Thing.common.urls(), dateFirstReleased: Thing.common.simpleDate(), + // 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. + alwaysReferenceByDirectory: { + flags: {update: true, expose: true}, + + // Deliberately defaults to null - this will fall back to false in most + // cases. + update: {validate: isBoolean, default: null}, + + expose: { + dependencies: ['name', 'originalReleaseTrackByRef', 'trackData'], + + transform(value, { + name, + originalReleaseTrackByRef, + trackData, + [Track.instance]: thisTrack, + }) { + if (value !== null) return value; + + const original = + find.track( + originalReleaseTrackByRef, + trackData.filter(track => track !== thisTrack), + {quiet: true}); + + if (!original) return false; + + return name === original.name; + } + }, + }, + artistContribsByRef: Thing.common.contribsByRef(), contributorContribsByRef: Thing.common.contribsByRef(), coverArtistContribsByRef: Thing.common.contribsByRef(), diff --git a/src/data/yaml.js b/src/data/yaml.js index 35943199..07e0a3d2 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -338,6 +338,8 @@ export const processTrackDocument = makeProcessDocument(T.Track, { coverArtFileExtension: 'Cover Art File Extension', hasCoverArt: 'Has Cover Art', + alwaysReferenceByDirectory: 'Always Reference By Directory', + lyrics: 'Lyrics', commentary: 'Commentary', additionalFiles: 'Additional Files', diff --git a/src/find.js b/src/find.js index b8230800..966629e3 100644 --- a/src/find.js +++ b/src/find.js @@ -80,17 +80,19 @@ function matchDirectory(ref, data) { } function matchName(ref, data, mode) { - const matches = data.filter( - ({name}) => name.toLowerCase() === ref.toLowerCase() - ); + const matches = + data + .filter(({name}) => name.toLowerCase() === ref.toLowerCase()) + .filter(thing => + (Object.hasOwn(thing, 'alwaysReferenceByDirectory') + ? !thing.alwaysReferenceByDirectory + : true)); if (matches.length > 1) { - return warnOrThrow( - mode, + return warnOrThrow(mode, `Multiple matches for reference "${ref}". Please resolve:\n` + - matches.map((match) => `- ${inspect(match)}\n`).join('') + - `Returning null for this reference.` - ); + matches.map(match => `- ${inspect(match)}\n`).join('') + + `Returning null for this reference.`); } if (matches.length === 0) { @@ -100,10 +102,8 @@ function matchName(ref, data, mode) { const thing = matches[0]; if (ref !== thing.name) { - warnOrThrow( - mode, - `Bad capitalization: ${color.red(ref)} -> ${color.green(thing.name)}` - ); + warnOrThrow(mode, + `Bad capitalization: ${color.red(ref)} -> ${color.green(thing.name)}`); } return thing; -- cgit 1.3.0-6-gf8a5 From 33558828e70e4dd942bec1fbdb8aea3819ed8a19 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 19 Sep 2023 10:16:52 -0300 Subject: data: declare {update} in higher-context locations --- src/data/things/album.js | 9 ++-- src/data/things/composite.js | 92 ++++++++++++++++++++++++++------------ src/data/things/homepage-layout.js | 29 ++++++------ src/data/things/track.js | 33 +++++--------- 4 files changed, 97 insertions(+), 66 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index c0042ae2..ec133a34 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -53,11 +53,12 @@ export class Album extends Thing { coverArtDate: [ exitWithoutContribs({contribs: 'coverArtistContribs'}), - exposeUpdateValueOrContinue(), - exposeDependency({ - dependency: 'date', - update: {validate: isDate}, + + exposeUpdateValueOrContinue({ + validate: input.value(isDate), }), + + exposeDependency({dependency: 'date'}), ], coverArtFileExtension: [ diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c33fc03c..011f307e 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1279,34 +1279,21 @@ export function debugComposite(fn) { // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. -// Since this serves as a base, specify a value for {update} to indicate -// that the property as a whole updates (and some previous compositional -// step works with that update value). Set {update: true} to only enable -// the update flag, or set update to an object to specify a descriptor -// (e.g. for custom value validation). // // Please note that this *doesn't* verify that the dependency exists, so // if you provide the wrong name or it hasn't been set by a previous // compositional step, the property will be exposed as undefined instead // of null. // -export function exposeDependency({ - dependency, - update = false, -}) { +export function exposeDependency({dependency}) { return { annotation: `exposeDependency`, - flags: {expose: true, update: !!update}, + flags: {expose: true}, expose: { mapDependencies: {dependency}, compute: ({dependency}) => dependency, }, - - update: - (typeof update === 'object' - ? update - : null), }; } @@ -1314,25 +1301,16 @@ export function exposeDependency({ // is typically the base of a composition serving as a particular property // descriptor. It generally follows steps which will conditionally early // exit with some other value, with the exposeConstant base serving as the -// fallback default value. Like exposeDependency, set {update} to true or -// an object to indicate that the property as a whole updates. -export function exposeConstant({ - value, - update = false, -}) { +// fallback default value. +export function exposeConstant({value}) { return { annotation: `exposeConstant`, - flags: {expose: true, update: !!update}, + flags: {expose: true}, expose: { options: {value}, compute: ({'#options': {value}}) => value, }, - - update: - (typeof update === 'object' - ? update - : null), }; } @@ -1427,12 +1405,24 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! +// for {mode} options! Also provide {validate} here to conveniently +// set a custom validation check for this property's update value. export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { mode: input(availabilityCheckModeInput), + validate: input({type: 'function', null: true}), + }, + + update: { + dependencies: [input.staticValue('validate')], + compute: ({ + [input.staticValue('validate')]: validate, + }) => + (validate + ? {validate} + : {}), }, steps: () => [ @@ -1757,6 +1747,52 @@ export function fillMissingListItems({ } } +// Filters particular values out of a list. Note that this will always +// completely skip over null, but can be used to filter out any other +// primitive or object value. +export const excludeFromList = templateCompositeFrom({ + annotation: `excludeFromList`, + + inputs: { + list: input(), + + item: input({null: true}), + items: input({validate: isArray, null: true}), + }, + + outputs: { + dependencies: [input.staticDependency('list')], + compute: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], + }, + + steps: [ + { + dependencies: [ + input.staticDependency('list'), + input('list'), + input('item'), + input('items'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: listName, + [input('list')]: listContents, + [input('item')]: excludeItem, + [input('items')]: excludeItems, + }) => continuation({ + [listName ?? '#list']: + listContents.filter(item => { + if (excludeItem !== null && item === excludeItem) return false; + if (!empty(excludeItems) && exclueItems.includes(item)) return false; + return true; + }), + }), + }, + ], +}); + // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 007e0236..677a2756 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -109,10 +109,21 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { sourceGroup: compositeFrom(`HomepageLayoutAlbumsRow.sourceGroup`, [ { - transform: (value, continuation) => - (value === 'new-releases' || value === 'new-additions' - ? value - : continuation(value)), + flags: {expose: true, update: true, compose: true}, + + update: { + validate: + oneOf( + is('new-releases', 'new-additions'), + validateReference(Group[Thing.referenceType])), + }, + + expose: { + transform: (value, continuation) => + (value === 'new-releases' || value === 'new-additions' + ? value + : continuation(value)), + }, }, withResolvedReference({ @@ -121,15 +132,7 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { find: input.value(find.group), }), - exposeDependency({ - dependency: '#resolvedReference', - update: input.value({ - validate: - oneOf( - is('new-releases', 'new-additions'), - validateReference(Group[Thing.referenceType])), - }), - }), + exposeDependency({dependency: '#resolvedReference'}), ]), sourceAlbums: referenceList({ diff --git a/src/data/things/track.js b/src/data/things/track.js index 28caf1de..135e6d1f 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -121,15 +121,13 @@ export class Track extends Thing { coverArtFileExtension: [ exitWithoutUniqueCoverArt(), - exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue({ + validate: input.value(isFileExtension), + }), withPropertyFromAlbum({property: 'trackCoverArtFileExtension'}), exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - - exposeConstant({ - value: 'jpg', - update: {validate: isFileExtension}, - }), + exposeConstant({value: 'jpg'}), ], // Date of cover art release. Like coverArtFileExtension, this represents @@ -140,13 +138,12 @@ export class Track extends Thing { withHasUniqueCoverArt(), exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), - exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue({ + validate: input.value(isDate), + }), withPropertyFromAlbum({property: 'trackArtDate'}), - exposeDependency({ - dependency: '#album.trackArtDate', - update: {validate: isDate}, - }), + exposeDependency({dependency: '#album.trackArtDate'}), ], commentary: commentary(), @@ -175,7 +172,7 @@ export class Track extends Thing { inheritFromOriginalRelease({property: 'artistContribs'}), withResolvedContribs({ - from: input.updateValue(), + from: input.updateValue({validate: isContributionList}), }).outputs({ '#resolvedContribs': '#artistContribs', }), @@ -183,10 +180,7 @@ export class Track extends Thing { exposeDependencyOrContinue({dependency: '#artistContribs'}), withPropertyFromAlbum({property: 'artistContribs'}), - exposeDependency({ - dependency: '#album.artistContribs', - update: {validate: isContributionList}, - }), + exposeDependency({dependency: '#album.artistContribs'}), ], contributorContribs: [ @@ -201,7 +195,7 @@ export class Track extends Thing { exitWithoutUniqueCoverArt(), withResolvedContribs({ - from: input.updateValue(), + from: input.updateValue({validate: isContributionList}), }).outputs({ '#resolvedContribs': '#coverArtistContribs', }), @@ -209,10 +203,7 @@ export class Track extends Thing { exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), - exposeDependency({ - dependency: '#album.trackCoverArtistContribs', - update: {validate: isContributionList}, - }), + exposeDependency({dependency: '#album.trackCoverArtistContribs'}), ], referencedTracks: [ -- cgit 1.3.0-6-gf8a5 From b85404e0b438a0c014b26b9cbfd12475cea6cb33 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 19 Sep 2023 10:36:25 -0300 Subject: content: linkThing: sanitize name before passing to linkTemplate --- src/content/dependencies/linkThing.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js index 643bf4b1..e661ca7c 100644 --- a/src/content/dependencies/linkThing.js +++ b/src/content/dependencies/linkThing.js @@ -1,6 +1,6 @@ export default { contentDependencies: ['linkTemplate'], - extraDependencies: ['html'], + extraDependencies: ['html', 'language'], relations(relation) { return { @@ -41,7 +41,7 @@ export default { hash: {type: 'string'}, }, - generate(data, relations, slots, {html}) { + generate(data, relations, slots, {html, language}) { const path = [data.pathKey, data.directory]; const name = @@ -51,7 +51,7 @@ export default { const content = (html.isBlank(slots.content) - ? name + ? language.sanitize(name) : slots.content); let color = null; -- cgit 1.3.0-6-gf8a5 From 2099c258fc1833ac459c8a43ca0cae9ae6150ad0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 19 Sep 2023 12:41:20 -0300 Subject: content: listTracksWithExtra: handle dateless albums cleanly --- src/content/dependencies/listTracksWithExtra.js | 12 ++++++++---- src/strings-default.json | 9 ++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/listTracksWithExtra.js b/src/content/dependencies/listTracksWithExtra.js index 73d25e3d..c9f80f35 100644 --- a/src/content/dependencies/listTracksWithExtra.js +++ b/src/content/dependencies/listTracksWithExtra.js @@ -65,10 +65,14 @@ export default { stitchArrays({ albumLink: relations.albumLinks, date: data.dates, - }).map(({albumLink, date}) => ({ - album: albumLink, - date: language.formatDate(date), - })), + }).map(({albumLink, date}) => + (date + ? { + stringsKey: 'withDate', + album: albumLink, + date: language.formatDate(date), + } + : {album: albumLink})), chunkRows: relations.trackLinks diff --git a/src/strings-default.json b/src/strings-default.json index b5e39e97..0ad7a516 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -437,15 +437,18 @@ "listingPage.listTracks.inFlashes.byFlash.chunk.item": "{TRACK} (from {ALBUM})", "listingPage.listTracks.withLyrics.title": "Tracks - with Lyrics", "listingPage.listTracks.withLyrics.title.short": "...with Lyrics", - "listingPage.listTracks.withLyrics.chunk.title": "{ALBUM} ({DATE})", + "listingPage.listTracks.withLyrics.chunk.title": "{ALBUM}", + "listingPage.listTracks.withLyrics.chunk.title.withDate": "{ALBUM} ({DATE})", "listingPage.listTracks.withLyrics.chunk.item": "{TRACK}", "listingPage.listTracks.withSheetMusicFiles.title": "Tracks - with Sheet Music Files", "listingPage.listTracks.withSheetMusicFiles.title.short": "...with Sheet Music Files", - "listingPage.listTracks.withSheetMusicFiles.chunk.title": "{ALBUM} ({DATE})", + "listingPage.listTracks.withSheetMusicFiles.chunk.title": "{ALBUM}", + "listingPage.listTracks.withSheetMusicFiles.chunk.title.withDate": "{ALBUM} ({DATE})", "listingPage.listTracks.withSheetMusicFiles.chunk.item": "{TRACK}", "listingPage.listTracks.withMidiProjectFiles.title": "Tracks - with MIDI & Project Files", "listingPage.listTracks.withMidiProjectFiles.title.short": "...with MIDI & Project Files", - "listingPage.listTracks.withMidiProjectFiles.chunk.title": "{ALBUM} ({DATE})", + "listingPage.listTracks.withMidiProjectFiles.chunk.title": "{ALBUM}", + "listingPage.listTracks.withMidiProjectFiles.chunk.title.withDate": "{ALBUM} ({DATE})", "listingPage.listTracks.withMidiProjectFiles.chunk.item": "{TRACK}", "listingPage.listTags.byName.title": "Tags - by Name", "listingPage.listTags.byName.title.short": "...by Name", -- cgit 1.3.0-6-gf8a5 From 0e613cf94a3fade7050fc2e50e8bfbab8d532ad2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 19 Sep 2023 13:21:08 -0300 Subject: thumbs: probably fix using wrong convert command?? --- src/gen-thumbs.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 18d1964d..65f0a4da 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -250,7 +250,11 @@ async function getImageMagickVersion(binary) { allData += data.toString(); }); - await promisifyProcess(proc, false); + try { + await promisifyProcess(proc, false); + } catch (error) { + return null; + } if (!allData.match(/ImageMagick/i)) { return null; -- cgit 1.3.0-6-gf8a5 From 9745cf89d808bc2e4698af346ddbc53e1ae08a2c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 19 Sep 2023 13:25:49 -0300 Subject: SO A BIT EMBARRASSING --- src/gen-thumbs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 65f0a4da..34eed9c1 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -286,7 +286,7 @@ async function getSpawnMagick(tool) { } if (fn === null && await commandExists('magick')) { - version = await getImageMagickVersion(fn); + version = await getImageMagickVersion('magick'); if (version !== null) { fn = (args) => spawn('magick', [tool, ...args]); description = `magick ${tool}`; -- cgit 1.3.0-6-gf8a5 From a53d53e1f95d84b23cffb9a5d43f1a70a412b6fe Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 19 Sep 2023 17:23:05 -0300 Subject: page: don't generate redirect from directory to same directory --- src/page/artist-alias.js | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/page/artist-alias.js b/src/page/artist-alias.js index 1da2af41..d2305229 100644 --- a/src/page/artist-alias.js +++ b/src/page/artist-alias.js @@ -7,6 +7,12 @@ export function targets({wikiData}) { export function pathsForTarget(aliasArtist) { const {aliasedArtist} = aliasArtist; + // Don't generate a redirect page if this aliased name resolves to the same + // directory as the original artist! See issue #280. + if (aliasArtist.directory === aliasedArtist.directory) { + return []; + } + return [ { type: 'redirect', -- cgit 1.3.0-6-gf8a5 From 86679ee48eee7e1000b2b2f35e4c3d1a8d1be143 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 20 Sep 2023 13:01:04 -0300 Subject: data: update a bunch of template composite validation --- src/data/things/composite.js | 290 +++++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 135 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 011f307e..98b04a7e 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -416,110 +416,65 @@ export function templateCompositeFrom(description) { } } - descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { - const missingCallsToInput = []; - const wrongCallsToInput = []; - - for (const [name, value] of Object.entries(description.inputs ?? {})) { - if (!isInputToken(value)) { - missingCallsToInput.push(name); - continue; - } - - if (getInputTokenShape(value) !== 'input') { - wrongCallsToInput.push(name); - } - } - - for (const name of missingCallsToInput) { - push(new Error(`${name}: Missing call to input()`)); + validateInputs: + if ('inputs' in description) { + if (Array.isArray(description.inputs)) { + descriptionAggregate.push(new Error(`Expected inputs to be object, got array`)); + break validateInputs; + } else if (typeof description.inputs !== 'object') { + descriptionAggregate.push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + break validateInputs; } - for (const name of wrongCallsToInput) { - const shape = getInputTokenShape(description.inputs[name]); - push(new Error(`${name}: Expected call to input(), got ${shape}`)); - } - }); - - descriptionAggregate.nest({message: `Errors in output descriptions for ${compositeName}`}, ({map, push}) => { - const wrongType = []; - const notPrivate = []; - - const missingDependenciesDefault = []; - const wrongDependenciesType = []; - const wrongDefaultType = []; + descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.outputs ?? {})) { - if (typeof value === 'object') { - if (!('dependencies' in value && 'default' in value)) { - missingDependenciesDefault.push(name); + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); continue; } - if (!Array.isArray(value.dependencies)) { - wrongDependenciesType.push(name); + if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { + wrongCallsToInput.push(name); } - - if (typeof value.default !== 'function') { - wrongDefaultType.push(name); - } - - continue; } - if (typeof value !== 'string') { - wrongType.push(name); - continue; + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); } - if (!value.startsWith('#')) { - notPrivate.push(name); - continue; + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); } - } - - for (const name of wrongType) { - const type = typeof description.outputs[name]; - push(new Error(`${name}: Expected string, got ${type}`)); - } - - for (const name of notPrivate) { - const into = description.outputs[name]; - push(new Error(`${name}: Expected "#" at start, got ${into}`)); - } - - for (const name of missingDependenciesDefault) { - push(new Error(`${name}: Expected both dependencies & default`)); - } - - for (const name of wrongDependenciesType) { - const {dependencies} = description.outputs[name]; - push(new Error(`${name}: Expected dependencies to be array, got ${dependencies}`)); - } + }); + } - for (const name of wrongDefaultType) { - const type = typeof description.outputs[name].default; - push(new Error(`${name}: Expected default to be function, got ${type}`)); + validateOutputs: + if ('outputs' in description) { + if ( + !Array.isArray(description.outputs) && + typeof description.outputs !== 'function' + ) { + descriptionAggregate.push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + break validateOutputs; } - for (const [name, value] of Object.entries(description.outputs ?? {})) { - if (typeof value !== 'object') continue; - - map( - description.outputs[name].dependencies, - decorateErrorWithIndex(dependency => { - if (!isInputToken(dependency)) { - throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${dependency}`); - } - - const shape = getInputTokenShape(dependency); - if (shape !== 'input.staticValue' && shape !== 'input.staticDependency') { - throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${shape}`); + if (Array.isArray(description.outputs)) { + descriptionAggregate.map( + description.outputs, + decorateErrorWithIndex(value => { + if (typeof value !== 'string') { + throw new Error(`${value}: Expected string, got ${typeof value}`) + } else if (!value.startsWith('#')) { + throw new Error(`${value}: Expected "#" at start`); } }), - {message: `${name}: Errors in dependencies`}); + {message: `Errors in output descriptions for ${compositeName}`}); } - }); + } descriptionAggregate.close(); @@ -772,66 +727,130 @@ export function compositeFrom(description) { ? base.flags.compose : true); - if (!baseExposes) { - aggregate.push(new TypeError(`All steps, including base, must expose`)); - } + // TODO: Check description.compose ?? true instead. + const compositionNests = baseComposes; const exposeDependencies = new Set(); + const updateDescription = {}; - let anyStepsCompute = false; - let anyStepsTransform = false; + // Steps default to exposing if using a shorthand syntax where flags aren't + // specified at all. + const stepsExpose = + steps + .map(step => + (step.flags + ? step.flags.expose ?? false + : true)); + + // Steps default to composing if using a shorthand syntax where flags aren't + // specified at all - *and* aren't the base (final step), unless the whole + // composition is nestable. + const stepsCompose = + steps + .map((step, index, {length}) => + (step.flags + ? step.flags.compose ?? false + : (index === length - 1 + ? compositionNests + : true))); + + // Steps don't update unless the corresponding flag is explicitly set. + const stepsUpdate = + steps + .map(step => + (step.flags + ? step.flags.update ?? false + : false)); + + // The expose description for a step is just the entire step object, when + // using the shorthand syntax where {flags: {expose: true}} is left implied. + const stepExposeDescriptions = + steps + .map((step, index) => + (stepsExpose[index] + ? (step.flags + ? step.expose ?? null + : step) + : null)); + + // The update description for a step, if present at all, is always set + // explicitly. + const stepUpdateDescriptions = + steps + .map((step, index) => + (stepsUpdate[index] + ? step.update ?? null + : null)); + + // Indicates presence of a {compute} function on the expose description. + const stepsCompute = + stepExposeDescriptions + .map(expose => !!expose?.compute); + + // Indicates presence of a {transform} function on the expose description. + const stepsTransform = + stepExposeDescriptions + .map(expose => !!expose?.transform); + + const anyStepsExpose = + stepsExpose.includes(true); + + const anyStepsUpdate = + stepsUpdate.includes(true); + + const anyStepsCompute = + stepsCompute.includes(true); + + const anyStepsTransform = + stepsTransform.includes(true); + + const stepEntries = stitchArrays({ + step: steps, + expose: stepExposeDescriptions, + update: stepUpdateDescriptions, + stepComposes: stepsCompose, + stepComputes: stepsCompute, + stepTransforms: stepsTransform, + }); - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; + for (let i = 0; i < stepEntries.length; i++) { + const { + step, + expose, + update, + stepComposes, + stepComputes, + stepTransforms, + } = stepEntries[i]; + + const isBase = i === stepEntries.length - 1; const message = `Errors in step #${i + 1}` + (isBase ? ` (base)` : ``) + (step.annotation ? ` (${step.annotation})` : ``); aggregate.nest({message}, ({push}) => { - if (step.flags) { - let flagsErrored = false; - - if (!step.flags.compose && !isBase) { - push(new TypeError(`All steps but base must compose`)); - flagsErrored = true; - } - - if (!step.flags.expose) { - push(new TypeError(`All steps must expose`)); - flagsErrored = true; - } - - if (flagsErrored) { - return; - } + if (isBase && stepComposes !== compositionNests) { + return push(new TypeError( + (compositionNests + ? `Base must compose, this composition is nestable` + : `Base must not compose, this composition isn't nestable`))); + } else if (!isBase && !stepComposes) { + return push(new TypeError( + (compositionNests + ? `All steps must compose` + : `All steps (except base) must compose`))); } - const expose = - (step.flags - ? step.expose - : step); - - const stepComputes = !!expose?.compute; - const stepTransforms = !!expose?.transform; - if ( - stepTransforms && !stepComputes && - !baseUpdates && !baseComposes + !compositionNests && !anyStepsUpdate && + stepTransforms && !stepComputes ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - return; - } - - if (stepComputes) { - anyStepsCompute = true; - } - - if (stepTransforms) { - anyStepsTransform = true; + return push(new TypeError( + `Steps which only transform can't be used in a composition that doesn't update`)); } + /* // Unmapped dependencies are exposed on the final composition only if // they're "public", i.e. pointing to update values of other properties // on the CacheableObject. @@ -849,6 +868,7 @@ export function compositeFrom(description) { for (const dependency of Object.values(expose?.mapDependencies ?? {})) { exposeDependencies.add(dependency); } + */ }); } @@ -1194,13 +1214,13 @@ export function compositeFrom(description) { } constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, + update: anyStepsUpdate, + expose: anyStepsExpose, + compose: compositionNests, }; - if (baseUpdates) { - constructedDescriptor.update = base.update; + if (constructedDescriptor.update) { + constructedDescriptor.update = updateDescription; } if (baseExposes) { -- cgit 1.3.0-6-gf8a5 From 8db50e29b5a1cfddfddf499129b697ecabfadcb0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 20 Sep 2023 13:01:25 -0300 Subject: data: moar WIP composite syntax! --- src/data/things/composite.js | 86 +++++++++++++++++++++++++------------------ src/data/things/thing.js | 69 +++++++++++++++++----------------- src/data/things/track.js | 73 +++++++++++++++++++----------------- src/data/things/validators.js | 4 +- 4 files changed, 129 insertions(+), 103 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 98b04a7e..2e85374f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,7 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; -import {oneOf} from '#validators'; +import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { @@ -1193,7 +1193,7 @@ export function compositeFrom(description) { case 'raiseAbove': debug(() => colors.bright(`end composition - raiseAbove`)); - return continuationIfApplicable.raise(...continuationArgs); + return continuationIfApplicable.raiseOutput(...continuationArgs); case 'continuation': if (isBase) { @@ -1360,9 +1360,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ mode: input(availabilityCheckModeInput), }, - outputs: { - into: '#availability', - }, + outputs: ['#availability'], steps: () => [ { @@ -1388,7 +1386,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ break; } - return continuation({into: availability}); + return continuation({'#availability': availability}); }, }, ], @@ -1571,35 +1569,56 @@ export const withPropertyFromObject = templateCompositeFrom({ property: input({type: 'string'}), }, - outputs: { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - compute: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => { - return ( - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value')); - }, + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => { + return [ + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value'), + ]; }, steps: () => [ { - dependencies: [input('object'), input('property')], + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + compute: (continuation, { + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => continuation({ + '#output': + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value'), + }), + }, + + { + dependencies: [ + '#output', + input('object'), + input('property'), + ], + compute: (continuation, { + ['#output']: output, [input('object')]: object, [input('property')]: property, - }) => - (object === null - ? continuation({into: null}) - : continuation({into: object[property] ?? null})), + }) => continuation({ + [output]: + (object === null + ? null + : object[property] ?? null), + }), }, ], }); @@ -1780,14 +1799,11 @@ export const excludeFromList = templateCompositeFrom({ items: input({validate: isArray, null: true}), }, - outputs: { - dependencies: [input.staticDependency('list')], - compute: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - }, + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], - steps: [ + steps: () => [ { dependencies: [ input.staticDependency('list'), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a5f0b78d..cff2f498 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -476,15 +476,15 @@ export const withResolvedContribs = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedContribs', - }, + outputs: ['#resolvedContribs'], steps: () => [ raiseOutputWithoutDependency({ dependency: input('from'), mode: input.value('empty'), - output: input.value({into: []}), + output: input.value({ + ['#resolvedContribs']: [], + }), }), withPropertiesFromList({ @@ -496,9 +496,10 @@ export const withResolvedContribs = templateCompositeFrom({ withResolvedReferenceList({ list: '#contribs.who', data: 'artistData', - into: '#contribs.who', find: input('find'), notFoundMode: input('notFoundMode'), + }).outputs({ + ['#resolvedReferenceList']: '#contribs.who', }), { @@ -510,7 +511,7 @@ export const withResolvedContribs = templateCompositeFrom({ }) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ - '#composition.into': stitchArrays({who, what}), + ['#resolvedContribs']: stitchArrays({who, what}), }); }, }, @@ -577,14 +578,14 @@ export const withResolvedReference = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedReference', - }, + outputs: ['#resolvedReference'], steps: () => [ raiseOutputWithoutDependency({ dependency: input('ref'), - output: input.value({into: null}), + output: input.value({ + ['#resolvedReference']: null, + }), }), exitWithoutDependency({ @@ -611,7 +612,9 @@ export const withResolvedReference = templateCompositeFrom({ return continuation.exit(null); } - return continuation.raise({match}); + return continuation.raiseOutput({ + ['#resolvedReference']: match, + }); }, }, ], @@ -640,9 +643,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedReferenceList', - }, + outputs: ['#resolvedReferenceList'], steps: () => [ exitWithoutDependency({ @@ -653,7 +654,9 @@ export const withResolvedReferenceList = templateCompositeFrom({ raiseOutputWithoutDependency({ dependency: input('list'), mode: input.value('empty'), - output: input.value({into: []}), + output: input.value({ + ['#resolvedReferenceList']: [], + }), }), { @@ -672,7 +675,9 @@ export const withResolvedReferenceList = templateCompositeFrom({ dependencies: ['#matches'], compute: ({'#matches': matches}, continuation) => (matches.every(match => match) - ? continuation.raise({'#continuation.into': matches}) + ? continuation.raiseOutput({ + ['#resolvedReferenceList']: matches, + }) : continuation()), }, @@ -687,12 +692,16 @@ export const withResolvedReferenceList = templateCompositeFrom({ return continuation.exit([]); case 'filter': - matches = matches.filter(match => match); - return continuation.raise({'#continuation.into': matches}); + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.filter(match => match), + }); case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({'#continuation.into': matches}); + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.map(match => match ?? null), + }); default: throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); @@ -714,30 +723,24 @@ export const withReverseReferenceList = templateCompositeFrom({ list: input({type: 'string'}), }, - outputs: { - into: '#reverseReferenceList', - }, + outputs: ['#reverseReferenceList'], steps: () => [ exitWithoutDependency({ - dependency: '#composition.data', + dependency: input('data'), value: [], }), { - dependencies: [ - 'this', - '#composition.data', - '#composition.refListProperty', - ], + dependencies: [input.myself(), input('data'), input('list')], compute: ({ - this: thisThing, - '#composition.data': data, - '#composition.refListProperty': refListProperty, + [input.myself()]: thisThing, + [input('data')]: data, + [input('list')]: refListProperty, }, continuation) => continuation({ - '#composition.into': + ['#reverseReferenceList']: data.filter(thing => thing[refListProperty].includes(thisThing)), }), }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 135e6d1f..37b36287 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -103,7 +103,7 @@ export class Track extends Thing { { dependencies: ['name', '#originalRelease.name'], - compute({name, '#originalRelease.name': originalName}) => + compute: ({name, '#originalRelease.name': originalName}) => name === originalName, }, ], @@ -389,34 +389,40 @@ export const withAlbum = templateCompositeFrom({ }), }, - outputs: { - into: '#album', - }, + outputs: ['#album'], steps: () => [ raiseOutputWithoutDependency({ dependency: 'albumData', mode: input.value('empty'), - output: input.value({into: null}), + output: input.value({ + ['#album']: null, + }), }), { - dependencies: ['this', 'albumData'], - compute: (continuation, {this: track, albumData}) => + dependencies: [input.myself(), 'albumData'], + compute: (continuation, { + [input.myself()]: track, + ['albumData']: albumData, + }) => continuation({ - '#album': albumData.find(album => album.tracks.includes(track)), + ['#album']: + albumData.find(album => album.tracks.includes(track)), }), }, raiseOutputWithoutDependency({ dependency: '#album', - output: input.value({into: null}), + output: input.value({ + ['#album']: null, + }), }), { dependencies: ['#album'], compute: (continuation, {'#album': album}) => - continuation({into: album}), + continuation.raiseOutput({'#album': album}), }, ], }); @@ -437,12 +443,9 @@ export const withPropertyFromAlbum = templateCompositeFrom({ }), }, - outputs: { - dependencies: [input.staticValue('property')], - compute: ({ - [input.staticValue('property')]: property, - }) => ['#album.' + property], - }, + outputs: ({ + [input.staticValue('property')]: property, + }) => ['#album.' + property], steps: () => [ withAlbum({ @@ -479,9 +482,7 @@ export const withContainingTrackSection = templateCompositeFrom({ }), }, - outputs: { - into: '#trackSection', - }, + outputs: ['#trackSection'], steps: () => [ withPropertyFromAlbum({ @@ -502,18 +503,24 @@ export const withContainingTrackSection = templateCompositeFrom({ ['#album.trackSections']: trackSections, }) { if (!trackSections) { - return continuation({into: null}); + return continuation.raiseOutput({ + ['#trackSection']: null, + }); } const trackSection = trackSections.find(({tracks}) => tracks.includes(track)); if (trackSection) { - return continuation({into: trackSection}); + return continuation.raiseOutput({ + ['#trackSection']: trackSection, + }); } else if (notFoundMode === 'exit') { return continuation.exit(null); } else { - return continuation({into: null}); + return continuation.raiseOutput({ + ['#trackSection']: null, + }); } }, }, @@ -536,9 +543,7 @@ export const withOriginalRelease = templateCompositeFrom({ data: input({defaultDependency: 'trackData'}), }, - outputs: { - into: '#originalRelease', - }, + outputs: ['#originalRelease'], steps: () => [ withResolvedReference({ @@ -547,7 +552,7 @@ export const withOriginalRelease = templateCompositeFrom({ find: input.value(find.track), notFoundMode: input.value('exit'), }).outputs({ - '#resolvedReference': '#originalRelease', + ['#resolvedReference']: '#originalRelease', }), { @@ -563,7 +568,7 @@ export const withOriginalRelease = templateCompositeFrom({ ['#originalRelease']: originalRelease, }) => continuation({ - into: + ['#originalRelease']: (originalRelease ?? (selfIfOriginal ? track @@ -578,9 +583,7 @@ export const withOriginalRelease = templateCompositeFrom({ export const withHasUniqueCoverArt = templateCompositeFrom({ annotation: 'withHasUniqueCoverArt', - outputs: { - into: '#hasUniqueCoverArt', - }, + outputs: ['#hasUniqueCoverArt'], steps: () => [ { @@ -602,7 +605,10 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ }) => (empty(contribsFromTrack) ? continuation() - : continuation.raiseOutput({into: true})), + : continuation.raiseOutput({ + ['#hasUniqueCoverArt']: + true, + })), }, withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), @@ -612,8 +618,9 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ compute: (continuation, { ['#album.trackCoverArtistContribs']: contribsFromAlbum, }) => - continuation({ - into: !empty(contribsFromAlbum), + continuation.raiseOutput({ + ['#hasUniqueCoverArt']: + !empty(contribsFromAlbum), }), }, ], diff --git a/src/data/things/validators.js b/src/data/things/validators.js index f0d1d9fd..cd4c2b46 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -9,11 +9,11 @@ function inspect(value) { // Basic types (primitives) -function a(noun) { +export function a(noun) { return /[aeiou]/.test(noun[0]) ? `an ${noun}` : `a ${noun}`; } -function isType(value, type) { +export function isType(value, type) { if (typeof value !== type) throw new TypeError(`Expected ${a(type)}, got ${typeof value}`); -- cgit 1.3.0-6-gf8a5 From e0cec3ff368175341526ff1b3c849f82e377b286 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 20 Sep 2023 17:33:27 -0300 Subject: data: work together validation internals --- src/data/things/composite.js | 70 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 2e85374f..fbdc52f5 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,10 +5,11 @@ import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { + decorateErrorWithIndex, empty, filterProperties, openAggregate, - decorateErrorWithIndex, + stitchArrays, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -361,6 +362,8 @@ export function input(nameOrDescription) { input.symbol = Symbol.for('hsmusic.composite.input'); input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); + input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); @@ -400,6 +403,35 @@ function getInputTokenValue(token) { } } +function getStaticInputMetadata(inputOptions) { + const metadata = {}; + + for (const [name, token] of Object.entries(inputOptions)) { + if (typeof token === 'string') { + metadata[input.staticDependency(name)] = token; + metadata[input.staticValue(name)] = null; + } else if (isInputToken(token)) { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + + metadata[input.staticDependency(name)] = + (tokenShape === 'input.dependency' + ? tokenValue + : null); + + metadata[input.staticValue(name)] = + (tokenShape === 'input.value' + ? tokenValue + : null); + } else { + metadata[input.staticDependency(name)] = null; + metadata[input.staticValue(name)] = null; + } + } + + return metadata; +} + export function templateCompositeFrom(description) { const compositeName = (description.annotation @@ -483,11 +515,6 @@ export function templateCompositeFrom(description) { ? Object.keys(description.inputs) : []); - const expectedOutputNames = - (description.outputs - ? Object.keys(description.outputs) - : []); - const instantiate = (inputOptions = {}) => { const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); @@ -538,6 +565,13 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.close(); + const expectedOutputNames = + (Array.isArray(description.outputs) + ? description.outputs + : typeof description.outputs === 'function' + ? description.outputs(getStaticInputMetadata(inputOptions)) + : []); + const outputOptions = {}; const instantiatedTemplate = { @@ -570,7 +604,7 @@ export function templateCompositeFrom(description) { } if (!empty(misplacedOutputNames)) { - outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames}`)); + outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); } for (const name of wrongTypeOutputNames) { @@ -703,6 +737,12 @@ export function compositeFrom(description) { } }; + if (!Array.isArray(composition)) { + throw new TypeError( + `Expected steps to be array, got ${typeof composition}` + + (annotation ? ` (${annotation})` : '')); + } + const base = composition.at(-1); const steps = composition.slice(); @@ -714,17 +754,17 @@ export function compositeFrom(description) { const baseExposes = (base.flags - ? base.flags.expose + ? base.flags.expose ?? false : true); const baseUpdates = (base.flags - ? base.flags.update + ? base.flags.update ?? false : false); const baseComposes = (base.flags - ? base.flags.compose + ? base.flags.compose ?? false : true); // TODO: Check description.compose ?? true instead. @@ -850,6 +890,12 @@ export function compositeFrom(description) { `Steps which only transform can't be used in a composition that doesn't update`)); } + if (update) { + // TODO: This is a dumb assign statement, and it could probably do more + // interesting things, like combining validation functions. + Object.assign(updateDescription, update); + } + /* // Unmapped dependencies are exposed on the final composition only if // they're "public", i.e. pointing to update values of other properties @@ -1028,8 +1074,6 @@ export function compositeFrom(description) { return null; } } - - continue; } const callingTransformForThisStep = @@ -1821,7 +1865,7 @@ export const excludeFromList = templateCompositeFrom({ [listName ?? '#list']: listContents.filter(item => { if (excludeItem !== null && item === excludeItem) return false; - if (!empty(excludeItems) && exclueItems.includes(item)) return false; + if (!empty(excludeItems) && excludeItems.includes(item)) return false; return true; }), }), -- cgit 1.3.0-6-gf8a5 From cc4bf401f4d1df63ce33191ae82af6327c7da568 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 20 Sep 2023 17:33:50 -0300 Subject: data: fix many validation errors --- src/data/things/album.js | 70 +++++++++++++---------- src/data/things/artist.js | 5 +- src/data/things/composite.js | 113 +++++++++++++++++++++++-------------- src/data/things/flash.js | 9 +-- src/data/things/group.js | 9 +-- src/data/things/homepage-layout.js | 8 +-- src/data/things/index.js | 7 ++- src/data/things/thing.js | 8 +-- src/data/things/track.js | 52 ++++++++++------- src/data/things/wiki-info.js | 5 +- 10 files changed, 172 insertions(+), 114 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index ec133a34..44af5cbf 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -10,9 +10,9 @@ import { exposeUpdateValueOrContinue, input, fillMissingListItems, - withFlattenedArray, + withFlattenedList, withPropertiesFromList, - withUnflattenedArray, + withUnflattenedList, } from '#composite'; import Thing, { @@ -101,8 +101,15 @@ export class Album extends Thing { additionalFiles: additionalFiles(), trackSections: [ - exitWithoutDependency({dependency: 'trackData', value: []}), - exitWithoutUpdateValue({value: [], mode: 'empty'}), + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + exitWithoutUpdateValue({ + mode: input.value('empty'), + value: input.value([]), + }), withPropertiesFromList({ list: input.updateValue(), @@ -119,32 +126,27 @@ export class Album extends Thing { fillMissingListItems({list: '#sections.isDefaultTrackSection', value: false}), fillMissingListItems({list: '#sections.color', dependency: 'color'}), - withFlattenedArray({ - from: '#sections.tracks', - into: '#trackRefs', - intoIndices: '#sections.startIndex', + withFlattenedList({ + list: '#sections.tracks', + }).outputs({ + ['#flattenedList']: '#trackRefs', + ['#flattenedIndices']: '#sections.startIndex', }), - { - dependencies: ['#trackRefs'], - compute: ({'#trackRefs': tracks}, continuation) => { - console.log(tracks); - return continuation(); - } - }, - withResolvedReferenceList({ list: '#trackRefs', data: 'trackData', - notFoundMode: 'null', - find: find.track, - into: '#tracks', + notFoundMode: input.value('null'), + find: input.value(find.track), + }).outputs({ + ['#resolvedReferenceList']: '#tracks', }), - withUnflattenedArray({ - from: '#tracks', - fromIndices: '#sections.startIndex', - into: '#sections.tracks', + withUnflattenedList({ + list: '#tracks', + indices: '#sections.startIndex', + }).outputs({ + ['#unflattenedList']: '#sections.tracks', }), { @@ -191,14 +193,14 @@ export class Album extends Thing { bannerArtistContribs: contributionList(), groups: referenceList({ - class: Group, - find: find.group, + class: input.value(Group), + find: input.value(find.group), data: 'groupData', }), artTags: referenceList({ - class: ArtTag, - find: find.artTag, + class: input.value(ArtTag), + find: input.value(find.artTag), data: 'artTagData', }), @@ -218,8 +220,16 @@ export class Album extends Thing { hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), tracks: [ - exitWithoutDependency({dependency: 'trackData', value: []}), - exitWithoutDependency({dependency: 'trackSections', mode: 'empty', value: []}), + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + exitWithoutDependency({ + dependency: 'trackSections', + mode: input.value('empty'), + value: input.value([]), + }), { dependencies: ['trackSections'], @@ -233,7 +243,7 @@ export class Album extends Thing { withResolvedReferenceList({ list: '#trackRefs', data: 'trackData', - find: find.track, + find: input.value(find.track), }), exposeDependency({dependency: '#resolvedReferenceList'}), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 7a9dbd3c..085e5663 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -1,3 +1,4 @@ +import {input} from '#composite'; import find from '#find'; import {isName, validateArrayItems} from '#validators'; @@ -35,8 +36,8 @@ export class Artist extends Thing { isAlias: flag(), aliasedArtist: singleReference({ - class: Artist, - find: find.artist, + class: input.value(Artist), + find: input.value(find.artist), data: 'artistData', }), diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fbdc52f5..83879c54 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,9 +1,15 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; -import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; +import { + isArray, + isWholeNumber, + oneOf, + validateArrayItems, +} from '#validators'; + import { decorateErrorWithIndex, empty, @@ -1876,72 +1882,93 @@ export const excludeFromList = templateCompositeFrom({ // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. -export function withFlattenedArray({ - from, - into = '#flattenedArray', - intoIndices = '#flattenedIndices', -}) { - return { - annotation: `withFlattenedArray`, - flags: {expose: true, compose: true}, +export const withFlattenedList = templateCompositeFrom({ + annotation: `withFlattenedList`, - expose: { - mapDependencies: {from}, - mapContinuation: {into, intoIndices}, + inputs: { + list: input({type: 'array'}), + }, - compute({from: sourceArray}, continuation) { - const into = sourceArray.flat(); - const intoIndices = []; + outputs: ['#flattenedList', '#flattenedIndices'], + steps: () => [ + { + dependencies: [input('list')], + compute(continuation, { + [input('list')]: sourceList, + }) { + const flattenedList = sourceList.flat(); + const indices = []; let lastEndIndex = 0; for (const {length} of sourceArray) { - intoIndices.push(lastEndIndex); + indices.push(lastEndIndex); lastEndIndex += length; } - return continuation({into, intoIndices}); + return continuation({ + ['#flattenedList']: flattenedList, + ['#flattenedIndices']: indices, + }); }, }, - }; -} + ], +}); // After mapping the contents of a flattened array in-place (being careful to // retain the original indices by replacing unmatched results with null instead // of filtering them out), this function allows for recombining them. It will // filter out null and undefined items by default (pass {filter: false} to // disable this). -export function withUnflattenedArray({ - from, - fromIndices = '#flattenedIndices', - into = '#unflattenedArray', - filter = true, -}) { - return { - annotation: `withUnflattenedArray`, - flags: {expose: true, compose: true}, +export const withUnflattenedList = templateCompositeFrom({ + annotation: `withUnflattenedList`, - expose: { - mapDependencies: {from, fromIndices}, - mapContinuation: {into}, - compute({from, fromIndices}, continuation) { - const arrays = []; + inputs: { + list: input({ + type: 'array', + defaultDependency: '#flattenedList', + }), + + indices: input({ + validate: validateArrayItems(isWholeNumber), + defaultDependency: '#flattenedIndices', + }), + + filter: input({ + type: 'boolean', + defaultValue: true, + }), + }, - for (let i = 0; i < fromIndices.length; i++) { - const startIndex = fromIndices[i]; + outputs: ['#unflattenedList'], + + steps: () => [ + { + dependencies: [input('list'), input('indices')], + compute({ + [input('list')]: list, + [input('indices')]: indices, + [input('filter')]: filter, + }) { + const unflattenedList = []; + + for (let i = 0; i < indices.length; i++) { + const startIndex = indices[i]; const endIndex = - (i === fromIndices.length - 1 - ? from.length - : fromIndices[i + 1]); + (i === indices.length - 1 + ? list.length + : indices[i + 1]); - const values = from.slice(startIndex, endIndex); - arrays.push( + const values = list.slice(startIndex, endIndex); + unflattenedList.push( (filter ? values.filter(value => value !== null && value !== undefined) : values)); } - return continuation({into: arrays}); + return continuation({ + ['#unflattenedList']: unflattenedList, + }); }, }, - }; -} + ], +}); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index eb16d29e..c3f90260 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -1,3 +1,4 @@ +import {input} from '#composite'; import find from '#find'; import { @@ -61,8 +62,8 @@ export class Flash extends Thing { contributorContribs: contributionList(), featuredTracks: referenceList({ - class: Track, - find: find.track, + class: input.value(Track), + find: input.value(find.track), data: 'trackData', }), @@ -133,9 +134,9 @@ export class FlashAct extends Thing { }, flashes: referenceList({ - class: Flash, + class: input.value(Flash), + find: input.value(find.flash), data: 'flashData', - find: find.flash, }), // Update only diff --git a/src/data/things/group.js b/src/data/things/group.js index f53fa48e..0b117801 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,3 +1,4 @@ +import {input} from '#composite'; import find from '#find'; import Thing, { @@ -24,8 +25,8 @@ export class Group extends Thing { urls: urls(), featuredAlbums: referenceList({ - class: Album, - find: find.album, + class: input.value(Album), + find: input.value(find.album), data: 'albumData', }), @@ -87,8 +88,8 @@ export class GroupCategory extends Thing { color: color(), groups: referenceList({ - class: Group, - find: find.group, + class: input.value(Group), + find: input.value(find.group), data: 'groupData', }), diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 677a2756..bade280c 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -107,7 +107,7 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }, }, - sourceGroup: compositeFrom(`HomepageLayoutAlbumsRow.sourceGroup`, [ + sourceGroup: [ { flags: {expose: true, update: true, compose: true}, @@ -133,11 +133,11 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }), exposeDependency({dependency: '#resolvedReference'}), - ]), + ], sourceAlbums: referenceList({ - class: Album, - find: find.album, + class: input.value(Album), + find: input.value(find.album), data: 'albumData', }), diff --git a/src/data/things/index.js b/src/data/things/index.js index 4d8d9d1f..f908653d 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -135,7 +135,12 @@ function evaluatePropertyDescriptors() { for (const [key, value] of Object.entries(results)) { if (Array.isArray(value)) { - results[key] = compositeFrom(`${constructor.name}.${key}`, value); + results[key] = compositeFrom({ + annotation: `${constructor.name}.${key}`, + compose: false, + steps: value, + }); + continue; } } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index cff2f498..a75ff3e1 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -211,7 +211,7 @@ export function contributionList() { update: {validate: isContributionList}, - steps: () => [ + steps: [ withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), exposeConstant({value: []}), @@ -468,8 +468,6 @@ export const withResolvedContribs = templateCompositeFrom({ // todo: validate from: input(), - findFunction: input({type: 'function'}), - notFoundMode: input({ validate: oneOf('exit', 'filter', 'null'), defaultValue: 'null', @@ -496,7 +494,7 @@ export const withResolvedContribs = templateCompositeFrom({ withResolvedReferenceList({ list: '#contribs.who', data: 'artistData', - find: input('find'), + find: input.value(find.artist), notFoundMode: input('notFoundMode'), }).outputs({ ['#resolvedReferenceList']: '#contribs.who', @@ -728,7 +726,7 @@ export const withReverseReferenceList = templateCompositeFrom({ steps: () => [ exitWithoutDependency({ dependency: input('data'), - value: [], + value: input.value([]), }), { diff --git a/src/data/things/track.js b/src/data/things/track.js index 37b36287..05b762b9 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -6,6 +6,7 @@ import {empty} from '#sugar'; import { exitWithoutDependency, + excludeFromList, exposeConstant, exposeDependency, exposeDependencyOrContinue, @@ -17,6 +18,7 @@ import { } from '#composite'; import { + isBoolean, isColor, isContributionList, isDate, @@ -136,7 +138,11 @@ export class Track extends Thing { // is specified, this value is null. coverArtDate: [ withHasUniqueCoverArt(), - exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), + + exitWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: input.value('falsy'), + }), exposeUpdateValueOrContinue({ validate: input.value(isDate), @@ -154,8 +160,8 @@ export class Track extends Thing { midiProjectFiles: additionalFiles(), originalReleaseTrack: singleReference({ - class: Track, - find: find.track, + class: input.value(Track), + find: input.value(find.track), data: 'trackData', }), @@ -163,8 +169,8 @@ export class Track extends Thing { // util.inspect display, if it isn't indirectly available (by way of being // included in an album's track list). dataSourceAlbum: singleReference({ - class: Album, - find: find.album, + class: input.value(Album), + find: input.value(find.album), data: 'albumData', }), @@ -208,25 +214,27 @@ export class Track extends Thing { referencedTracks: [ inheritFromOriginalRelease({property: 'referencedTracks'}), + referenceList({ - class: Track, - find: find.track, + class: input.value(Track), + find: input.value(find.track), data: 'trackData', }), ], sampledTracks: [ inheritFromOriginalRelease({property: 'sampledTracks'}), + referenceList({ - class: Track, - find: find.track, + class: input.value(Track), + find: input.value(find.track), data: 'trackData', }), ], artTags: referenceList({ - class: ArtTag, - find: find.artTag, + class: input.value(ArtTag), + find: input.value(find.artTag), data: 'artTagData', }), @@ -266,8 +274,14 @@ export class Track extends Thing { ], otherReleases: [ - exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), - withOriginalRelease({selfIfOriginal: true}), + exitWithoutDependency({ + dependency: 'trackData', + mode: input.value('empty'), + }), + + withOriginalRelease({ + selfIfOriginal: input.value(true), + }), { flags: {expose: true}, @@ -594,14 +608,14 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ : continuation()), }, - withResolvedContribs - .inputs({from: 'coverArtistContribs'}) - .outputs({into: '#coverArtistContribs'}), + withResolvedContribs({ + from: 'coverArtistContribs', + }), { - dependencies: ['#coverArtistContribs'], + dependencies: ['#resolvedContribs'], compute: (continuation, { - ['#coverArtistContribs']: contribsFromTrack, + ['#resolvedContribs']: contribsFromTrack, }) => (empty(contribsFromTrack) ? continuation() @@ -640,7 +654,7 @@ export const exitWithoutUniqueCoverArt = templateCompositeFrom({ exitWithoutDependency({ dependency: '#hasUniqueCoverArt', - mode: 'falsy', + mode: input.value('falsy'), value: input('value'), }), ], diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 7c2de324..c764b528 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -1,3 +1,4 @@ +import {input} from '#composite'; import find from '#find'; import {isLanguageCode, isName, isURL} from '#validators'; @@ -45,8 +46,8 @@ export class WikiInfo extends Thing { }, divideTrackListsByGroups: referenceList({ - class: Group, - find: find.group, + class: input.value(Group), + find: input.value(find.group), data: 'groupData', }), -- cgit 1.3.0-6-gf8a5 From a2704c0992beb4ddfeb67813d4f8adac0ae6af7d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 20 Sep 2023 18:30:48 -0300 Subject: data: syntax fixes --- src/data/things/album.js | 2 +- src/data/things/art-tag.js | 11 ++++------- src/data/things/composite.js | 10 +++++----- src/data/things/thing.js | 20 +++++++++++--------- 4 files changed, 21 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 44af5cbf..20a1a5b3 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -233,7 +233,7 @@ export class Album extends Thing { { dependencies: ['trackSections'], - compute: ({trackSections}, continuation) => + compute: (continuation, {trackSections}) => continuation({ '#trackRefs': trackSections .flatMap(section => section.tracks ?? []), diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 7e466555..ba3cbd0d 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,4 +1,4 @@ -import {exposeUpdateValueOrContinue} from '#composite'; +import {exposeUpdateValueOrContinue, input} from '#composite'; import {sortAlbumsTracksChronologically} from '#wiki-data'; import {isName} from '#validators'; @@ -22,18 +22,15 @@ export class ArtTag extends Thing { isContentWarning: flag(false), nameShort: [ - exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue({ + validate: input.value(isName), + }), { dependencies: ['name'], compute: ({name}) => name.replace(/ \([^)]*?\)$/, ''), }, - - { - flags: {update: true, expose: true}, - validate: {isName}, - }, ], // Update only diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 83879c54..e2dbc70b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1693,7 +1693,7 @@ export function withPropertiesFromObject({ mapDependencies: {object}, options: {prefix, properties}, - compute: ({object, '#options': {prefix, properties}}, continuation) => + compute: (continuation, {object, '#options': {prefix, properties}}) => continuation( Object.fromEntries( properties.map(property => [ @@ -1729,7 +1729,7 @@ export function withPropertyFromList({ mapContinuation: {into}, options: {property}, - compute({list, '#options': {property}}, continuation) { + compute(continuation, {list, '#options': {property}}) { if (list === undefined || empty(list)) { return continuation({into: []}); } @@ -1765,7 +1765,7 @@ export function withPropertiesFromList({ mapDependencies: {list}, options: {prefix, properties}, - compute({list, '#options': {prefix, properties}}, continuation) { + compute(continuation, {list, '#options': {prefix, properties}}) { const lists = Object.fromEntries( properties.map(property => [`${prefix}.${property}`, []])); @@ -1811,7 +1811,7 @@ export function fillMissingListItems({ mapDependencies: {list, dependency}, mapContinuation: {into}, - compute: ({list, dependency}, continuation) => + compute: (continuation, {list, dependency}) => continuation({ into: list.map(item => item ?? dependency), }), @@ -1827,7 +1827,7 @@ export function fillMissingListItems({ mapContinuation: {into}, options: {value}, - compute: ({list, '#options': {value}}, continuation) => + compute: (continuation, {list, '#options': {value}}) => continuation({ into: list.map(item => item ?? value), }), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a75ff3e1..265cfe18 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -209,6 +209,8 @@ export function contributionList() { return compositeFrom({ annotation: `contributionList`, + compose: false, + update: {validate: isContributionList}, steps: [ @@ -598,12 +600,12 @@ export const withResolvedReference = templateCompositeFrom({ input('notFoundMode'), ], - compute({ + compute(continuation, { [input('ref')]: ref, [input('data')]: data, [input('find')]: findFunction, [input('notFoundMode')]: notFoundMode, - }, continuation) { + }) { const match = findFunction(ref, data, {mode: 'quiet'}); if (match === null && notFoundMode === 'exit') { @@ -659,11 +661,11 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: [input('list'), input('data'), input('find')], - compute: ({ + compute: (continuation, { [input('list')]: list, [input('data')]: data, [input('find')]: findFunction, - }, continuation) => + }) => continuation({ '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), }), @@ -671,7 +673,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: ['#matches'], - compute: ({'#matches': matches}, continuation) => + compute: (continuation, {'#matches': matches}) => (matches.every(match => match) ? continuation.raiseOutput({ ['#resolvedReferenceList']: matches, @@ -681,10 +683,10 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: ['#matches', input('notFoundMode')], - compute({ + compute(continuation, { ['#matches']: matches, [input('notFoundMode')]: notFoundMode, - }, continuation) { + }) { switch (notFoundMode) { case 'exit': return continuation.exit([]); @@ -732,11 +734,11 @@ export const withReverseReferenceList = templateCompositeFrom({ { dependencies: [input.myself(), input('data'), input('list')], - compute: ({ + compute: (continuation, { [input.myself()]: thisThing, [input('data')]: data, [input('list')]: refListProperty, - }, continuation) => + }) => continuation({ ['#reverseReferenceList']: data.filter(thing => thing[refListProperty].includes(thisThing)), -- cgit 1.3.0-6-gf8a5 From 66544e6730bd79c9cb1c50d89421f9a08329e27d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 20 Sep 2023 18:31:30 -0300 Subject: data: make composite work --- src/data/things/composite.js | 195 +++++++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 74 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e2dbc70b..aa383db9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -16,6 +16,7 @@ import { filterProperties, openAggregate, stitchArrays, + unique, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -638,6 +639,10 @@ export function templateCompositeFrom(description) { finalDescription.annotation = description.annotation; } + if ('compose' in description) { + finalDescription.compose = description.compose; + } + if ('update' in description) { finalDescription.update = description.update; } @@ -700,7 +705,7 @@ export function templateCompositeFrom(description) { steps, decorateErrorWithIndex(step => (step.symbol === templateCompositeFrom.symbol - ? step.toResolvedComposition() + ? compositeFrom(step.toResolvedComposition()) : step)), {message: `Errors resolving steps`}); @@ -723,7 +728,7 @@ export function templateCompositeFrom(description) { templateCompositeFrom.symbol = Symbol(); export function compositeFrom(description) { - const {annotation, steps: composition} = description; + const {annotation} = description; const debug = fn => { if (compositeFrom.debug === true) { @@ -743,12 +748,37 @@ export function compositeFrom(description) { } }; - if (!Array.isArray(composition)) { + if (!Array.isArray(description.steps)) { throw new TypeError( - `Expected steps to be array, got ${typeof composition}` + + `Expected steps to be array, got ${typeof description.steps}` + (annotation ? ` (${annotation})` : '')); } + const composition = + description.steps.map(step => + ('toResolvedComposition' in step + ? compositeFrom(step.toResolvedComposition()) + : step)); + + const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + + // These dependencies were all provided by the composition which this one is + // nested inside, so input('name')-shaped tokens are going to be evaluated + // in the context of the containing composition. + const dependenciesFromInputs = + Object.values(description.inputs ?? {}) + .map(token => { + switch (getInputTokenShape(token)) { + case 'input.dependency': + return getInputTokenValue(token); + case 'input': + return token; + default: + return null; + } + }) + .filter(Boolean); + const base = composition.at(-1); const steps = composition.slice(); @@ -758,23 +788,8 @@ export function compositeFrom(description) { (annotation ? ` (${annotation})` : ''), }); - const baseExposes = - (base.flags - ? base.flags.expose ?? false - : true); - - const baseUpdates = - (base.flags - ? base.flags.update ?? false - : false); - - const baseComposes = - (base.flags - ? base.flags.compose ?? false - : true); - // TODO: Check description.compose ?? true instead. - const compositionNests = baseComposes; + const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); const updateDescription = {}; @@ -838,6 +853,18 @@ export function compositeFrom(description) { stepExposeDescriptions .map(expose => !!expose?.transform); + const dependenciesFromSteps = + unique( + stepExposeDescriptions + .flatMap(expose => expose?.dependencies ?? []) + .map(dependency => + (typeof dependency === 'string' + ? dependency + : getInputTokenShape(dependency) === 'input.dependency' + ? getInputTokenValue(dependency) + : null)) + .filter(Boolean)); + const anyStepsExpose = stepsExpose.includes(true); @@ -924,39 +951,12 @@ export function compositeFrom(description) { }); } - if (!baseComposes && !baseUpdates && !anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute`)); + if (!compositionNests && !anyStepsUpdate && !anyStepsCompute) { + aggregate.push(new TypeError(`Expected at least one step to compute or update`)); } aggregate.close(); - function _filterDependencies(availableDependencies, { - dependencies, - mapDependencies, - options, - }) { - if (!dependencies && !mapDependencies && !options) { - return null; - } - - const filteredDependencies = - (dependencies - ? filterProperties(availableDependencies, dependencies) - : {}); - - if (mapDependencies) { - for (const [into, from] of Object.entries(mapDependencies)) { - filteredDependencies[into] = availableDependencies[from] ?? null; - } - } - - if (options) { - filteredDependencies['#options'] = options; - } - - return filteredDependencies; - } - function _assignDependencies(continuationAssignment, {mapContinuation}) { if (!mapContinuation) { return continuationAssignment; @@ -998,7 +998,7 @@ export function compositeFrom(description) { return continuationSymbol; }; - if (baseComposes) { + if (compositionNests) { const makeRaiseLike = returnWith => (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { @@ -1033,6 +1033,31 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; + // console.log('input description:', description.inputs); + const inputValues = + ('inputs' in description + ? Object.fromEntries(Object.entries(description.inputs) + .map(([name, token]) => { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { + case 'input.dependency': + return [input(name), initialDependencies[tokenValue]]; + case 'input.value': + return [input(name), tokenValue]; + case 'input.updateValue': + return [input(name), initialValue]; + case 'myself': + return [input(name), myself]; + case 'input': + return [input(name), initialDependencies[token]]; + default: + throw new TypeError(`Unexpected input shape ${tokenShape}`); + } + })) + : {}); + // console.log('input values:', inputValues); + if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); } else { @@ -1087,7 +1112,12 @@ export function compositeFrom(description) { let continuationStorage; - const filteredDependencies = _filterDependencies(availableDependencies, expose); + const filteredDependencies = + filterProperties({ + ...availableDependencies, + ...inputMetadata, + ...inputValues, + }, expose.dependencies); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, @@ -1106,9 +1136,17 @@ export function compositeFrom(description) { const naturalEvaluate = () => { const [name, ...args] = getExpectedEvaluation(); - let continuation; - ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); - return expose[name](...args, continuation); + + if (isBase && !compositionNests) { + return expose[name](...args); + } else { + let continuation; + + ({continuation, continuationStorage} = + _prepareContinuation(callingTransformForThisStep)); + + return expose[name](continuation, ...args); + } } switch (step.cache) { @@ -1166,7 +1204,7 @@ export function compositeFrom(description) { if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - if (baseComposes) { + if (compositionNests) { throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } @@ -1183,7 +1221,7 @@ export function compositeFrom(description) { debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); debug(() => colors.bright(`end composition - exit (explicit)`)); - if (baseComposes) { + if (composiitonNests) { return continuationIfApplicable.exit(providedValue); } else { return providedValue; @@ -1273,26 +1311,35 @@ export function compositeFrom(description) { constructedDescriptor.update = updateDescription; } - if (baseExposes) { + if (anyStepsExpose) { const expose = constructedDescriptor.expose = {}; - expose.dependencies = Array.from(exposeDependencies); - - const transformFn = - (value, initialDependencies, continuationIfApplicable) => - _computeOrTransform(value, initialDependencies, continuationIfApplicable); - - const computeFn = - (initialDependencies, continuationIfApplicable) => - _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - - if (baseComposes) { - if (anyStepsTransform) expose.transform = transformFn; - if (anyStepsCompute) expose.compute = computeFn; - if (base.cacheComposition) expose.cache = base.cacheComposition; - } else if (baseUpdates) { - expose.transform = transformFn; + + expose.dependencies = + unique([ + ...dependenciesFromInputs, + ...dependenciesFromSteps, + ]); + + if (compositionNests) { + if (anyStepsTransform) { + expose.transform = (value, continuation, dependencies) => + _computeOrTransform(value, dependencies, continuation); + } + + if (anyStepsCompute) { + expose.compute = (continuation, dependencies) => + _computeOrTransform(noTransformSymbol, dependencies, continuation); + } + + if (base.cacheComposition) { + expose.cache = base.cacheComposition; + } + } else if (anyStepsUpdate) { + expose.transform = (value, dependencies) => + _computeOrTransform(value, dependencies, null); } else { - expose.compute = computeFn; + expose.compute = (dependencies) => + _computeOrTransform(noTransformSymbol, dependencies, null); } } -- cgit 1.3.0-6-gf8a5 From 572b5465f9ce1e992e0384aa92461ec11dbaabff Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 11:04:33 -0300 Subject: data: make composites work --- src/data/things/composite.js | 232 ++++++++++++++++++++++++------------------- src/data/things/index.js | 4 +- 2 files changed, 130 insertions(+), 106 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index aa383db9..cbbe6f8f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -651,7 +651,10 @@ export function templateCompositeFrom(description) { const finalInputs = {}; for (const [name, description_] of Object.entries(description.inputs)) { - const description = description_; + // TODO: Validate inputOptions[name] against staticValue, staticDependency shapes + const description = getInputTokenValue(description_); + const tokenShape = getInputTokenShape(description_); + if (name in inputOptions) { if (typeof inputOptions[name] === 'string') { finalInputs[name] = input.dependency(inputOptions[name]); @@ -659,9 +662,9 @@ export function templateCompositeFrom(description) { finalInputs[name] = inputOptions[name]; } } else if (description.defaultValue) { - finalInputs[name] = input.value(defaultValue); + finalInputs[name] = input.value(description.defaultValue); } else if (description.defaultDependency) { - finalInputs[name] = input.dependency(defaultValue); + finalInputs[name] = input.dependency(description.defaultDependency); } else { finalInputs[name] = input.value(null); } @@ -673,11 +676,11 @@ export function templateCompositeFrom(description) { if ('outputs' in description) { const finalOutputs = {}; - for (const [name, defaultDependency] of Object.entries(description.outputs)) { + for (const name of expectedOutputNames) { if (name in outputOptions) { finalOutputs[name] = outputOptions[name]; } else { - finalOutputs[name] = defaultDependency; + finalOutputs[name] = name; } } @@ -740,7 +743,7 @@ export function compositeFrom(description) { if (Array.isArray(result)) { console.log(label, ...result.map(value => (typeof value === 'object' - ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) + ? inspect(value, {depth: 1, colors: true, compact: true, breakLength: Infinity}) : value))); } else { console.log(label, result); @@ -762,16 +765,37 @@ export function compositeFrom(description) { const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + function _mapDependenciesToOutputs(providedDependencies) { + if (!description.outputs) { + return {}; + } + + if (!providedDependencies) { + return {}; + } + + return ( + Object.fromEntries( + Object.entries(description.outputs) + .map(([continuationName, outputName]) => [ + outputName, + providedDependencies[continuationName], + ]))); + } + // These dependencies were all provided by the composition which this one is // nested inside, so input('name')-shaped tokens are going to be evaluated // in the context of the containing composition. const dependenciesFromInputs = Object.values(description.inputs ?? {}) .map(token => { - switch (getInputTokenShape(token)) { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { case 'input.dependency': - return getInputTokenValue(token); + return tokenValue; case 'input': + case 'input.updateValue': return token; default: return null; @@ -779,6 +803,9 @@ export function compositeFrom(description) { }) .filter(Boolean); + const anyInputsUseUpdateValue = + dependenciesFromInputs.includes(input.updateValue()); + const base = composition.at(-1); const steps = composition.slice(); @@ -857,14 +884,30 @@ export function compositeFrom(description) { unique( stepExposeDescriptions .flatMap(expose => expose?.dependencies ?? []) - .map(dependency => - (typeof dependency === 'string' - ? dependency - : getInputTokenShape(dependency) === 'input.dependency' - ? getInputTokenValue(dependency) - : null)) + .map(dependency => { + if (typeof dependency === 'string') + return (dependency.startsWith('#') ? null : dependency); + + const tokenShape = getInputTokenShape(dependency); + const tokenValue = getInputTokenValue(dependency); + switch (tokenShape) { + case 'input.dependency': + return (tokenValue.startsWith('#') ? null : tokenValue); + case 'input.myself': + return 'this'; + default: + return null; + } + }) .filter(Boolean)); + const anyStepsUseUpdateValue = + stepExposeDescriptions + .some(expose => + (expose?.dependencies + ? expose.dependencies.includes(input.updateValue()) + : false)); + const anyStepsExpose = stepsExpose.includes(true); @@ -877,6 +920,14 @@ export function compositeFrom(description) { const anyStepsTransform = stepsTransform.includes(true); + const compositionExposes = + anyStepsExpose; + + const compositionUpdates = + anyInputsUseUpdateValue || + anyStepsUseUpdateValue || + anyStepsUpdate; + const stepEntries = stitchArrays({ step: steps, expose: stepExposeDescriptions, @@ -916,7 +967,7 @@ export function compositeFrom(description) { } if ( - !compositionNests && !anyStepsUpdate && + !compositionNests && !compositionUpdates && stepTransforms && !stepComputes ) { return push(new TypeError( @@ -928,26 +979,6 @@ export function compositeFrom(description) { // interesting things, like combining validation functions. Object.assign(updateDescription, update); } - - /* - // Unmapped dependencies are exposed on the final composition only if - // they're "public", i.e. pointing to update values of other properties - // on the CacheableObject. - for (const dependency of expose?.dependencies ?? []) { - if (typeof dependency === 'string' && dependency.startsWith('#')) { - continue; - } - - exposeDependencies.add(dependency); - } - - // Mapped dependencies are always exposed on the final composition. - // These are explicitly for reading values which are named outside of - // the current compositional step. - for (const dependency of Object.values(expose?.mapDependencies ?? {})) { - exposeDependencies.add(dependency); - } - */ }); } @@ -957,20 +988,6 @@ export function compositeFrom(description) { aggregate.close(); - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } - - const assignDependencies = {}; - - for (const [from, into] of Object.entries(mapContinuation)) { - assignDependencies[into] = continuationAssignment[from] ?? null; - } - - return assignDependencies; - } - function _prepareContinuation(callingTransformForThisStep) { const continuationStorage = { returnedWith: null, @@ -1013,8 +1030,8 @@ export function compositeFrom(description) { return continuationSymbol; }); - continuation.raise = makeRaiseLike('raise'); - continuation.raiseAbove = makeRaiseLike('raiseAbove'); + continuation.raiseOutput = makeRaiseLike('raiseOutput'); + continuation.raiseOutputAbove = makeRaiseLike('raiseOutputAbove'); } return {continuation, continuationStorage}; @@ -1023,7 +1040,7 @@ export function compositeFrom(description) { const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + function _computeOrTransform(initialValue, continuationIfApplicable, initialDependencies) { const expectingTransform = initialValue !== noTransformSymbol; let valueSoFar = @@ -1033,7 +1050,6 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; - // console.log('input description:', description.inputs); const inputValues = ('inputs' in description ? Object.fromEntries(Object.entries(description.inputs) @@ -1046,9 +1062,12 @@ export function compositeFrom(description) { case 'input.value': return [input(name), tokenValue]; case 'input.updateValue': - return [input(name), initialValue]; - case 'myself': - return [input(name), myself]; + if (!expectingTransform) { + throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); + } + return [input(name), valueSoFar]; + case 'input.myself': + return [input(name), initialDependencies['this']]; case 'input': return [input(name), initialDependencies[token]]; default: @@ -1056,7 +1075,6 @@ export function compositeFrom(description) { } })) : {}); - // console.log('input values:', inputValues); if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); @@ -1117,36 +1135,51 @@ export function compositeFrom(description) { ...availableDependencies, ...inputMetadata, ...inputValues, - }, expose.dependencies); + ... + (callingTransformForThisStep + ? {[input.updateValue()]: valueSoFar} + : {}), + [input.myself()]: initialDependencies['this'], + }, expose.dependencies ?? []); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + `with dependencies:`, filteredDependencies, + ...callingTransformForThisStep ? [`from value:`, valueSoFar] : []]); let result; const getExpectedEvaluation = () => (callingTransformForThisStep ? (filteredDependencies - ? ['transform', valueSoFar, filteredDependencies] - : ['transform', valueSoFar]) + ? ['transform', valueSoFar, continuationSymbol, filteredDependencies] + : ['transform', valueSoFar, continuationSymbol]) : (filteredDependencies - ? ['compute', filteredDependencies] - : ['compute'])); + ? ['compute', continuationSymbol, filteredDependencies] + : ['compute', continuationSymbol])); const naturalEvaluate = () => { - const [name, ...args] = getExpectedEvaluation(); + const [name, ...argsLayout] = getExpectedEvaluation(); + + let args; if (isBase && !compositionNests) { - return expose[name](...args); + args = + argsLayout.filter(arg => arg !== continuationSymbol); } else { let continuation; ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); - return expose[name](continuation, ...args); + args = + argsLayout.map(arg => + (arg === continuationSymbol + ? continuation + : arg)); } + + return expose[name](...args); } switch (step.cache) { @@ -1221,7 +1254,7 @@ export function compositeFrom(description) { debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); debug(() => colors.bright(`end composition - exit (explicit)`)); - if (composiitonNests) { + if (compositionNests) { return continuationIfApplicable.exit(providedValue); } else { return providedValue; @@ -1230,36 +1263,24 @@ export function compositeFrom(description) { const {providedValue, providedDependencies} = continuationStorage; - const continuingWithValue = - (expectingTransform - ? (callingTransformForThisStep - ? providedValue ?? null - : valueSoFar ?? null) - : undefined); - - const continuingWithDependencies = - (providedDependencies - ? _assignDependencies(providedDependencies, expose) - : null); - const continuationArgs = []; - if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); - if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); + if (expectingTransform) { + continuationArgs.push( + (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null)); + } debug(() => { const base = `step #${i+1} - result: ` + returnedWith; const parts = []; if (callingTransformForThisStep) { - if (continuingWithValue === undefined) { - parts.push(`(no value)`); - } else { - parts.push(`value:`, providedValue); - } + parts.push('value:', providedValue); } - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); + if (providedDependencies !== null) { + parts.push(`deps:`, providedDependencies); } else { parts.push(`(no deps)`); } @@ -1272,23 +1293,26 @@ export function compositeFrom(description) { }); switch (returnedWith) { - case 'raise': + case 'raiseOutput': debug(() => (isBase - ? colors.bright(`end composition - raise (base: explicit)`) - : colors.bright(`end composition - raise`))); + ? colors.bright(`end composition - raiseOutput (base: explicit)`) + : colors.bright(`end composition - raiseOutput`))); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable(...continuationArgs); - case 'raiseAbove': - debug(() => colors.bright(`end composition - raiseAbove`)); + case 'raiseOutputAbove': + debug(() => colors.bright(`end composition - raiseOutputAbove`)); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable.raiseOutput(...continuationArgs); case 'continuation': if (isBase) { - debug(() => colors.bright(`end composition - raise (inferred)`)); + debug(() => colors.bright(`end composition - raiseOutput (inferred)`)); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable(...continuationArgs); } else { - Object.assign(availableDependencies, continuingWithDependencies); + Object.assign(availableDependencies, providedDependencies); break; } } @@ -1302,8 +1326,8 @@ export function compositeFrom(description) { } constructedDescriptor.flags = { - update: anyStepsUpdate, - expose: anyStepsExpose, + update: compositionUpdates, + expose: compositionExposes, compose: compositionNests, }; @@ -1311,7 +1335,7 @@ export function compositeFrom(description) { constructedDescriptor.update = updateDescription; } - if (anyStepsExpose) { + if (compositionExposes) { const expose = constructedDescriptor.expose = {}; expose.dependencies = @@ -1321,25 +1345,25 @@ export function compositeFrom(description) { ]); if (compositionNests) { - if (anyStepsTransform) { + if (compositionUpdates) { expose.transform = (value, continuation, dependencies) => - _computeOrTransform(value, dependencies, continuation); + _computeOrTransform(value, continuation, dependencies); } if (anyStepsCompute) { expose.compute = (continuation, dependencies) => - _computeOrTransform(noTransformSymbol, dependencies, continuation); + _computeOrTransform(noTransformSymbol, continuation, dependencies); } if (base.cacheComposition) { expose.cache = base.cacheComposition; } - } else if (anyStepsUpdate) { + } else if (compositionUpdates) { expose.transform = (value, dependencies) => - _computeOrTransform(value, dependencies, null); + _computeOrTransform(value, null, dependencies); } else { expose.compute = (dependencies) => - _computeOrTransform(noTransformSymbol, dependencies, null); + _computeOrTransform(noTransformSymbol, null, dependencies); } } diff --git a/src/data/things/index.js b/src/data/things/index.js index f908653d..77e5fa76 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -140,8 +140,8 @@ function evaluatePropertyDescriptors() { compose: false, steps: value, }); - - continue; + } else if (value.toResolvedComposition) { + results[key] = compositeFrom(value.toResolvedComposition()); } } -- cgit 1.3.0-6-gf8a5 From cd73f85962f542f9b44feb2a7616bc0d9aac797b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 11:05:00 -0300 Subject: data: miscellaneous utility updates --- src/data/things/composite.js | 142 ++++++++++++++++++++++++++++++------------- src/data/things/thing.js | 2 +- src/data/things/track.js | 53 ++++++++++++---- 3 files changed, 141 insertions(+), 56 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cbbe6f8f..cfa557de 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,6 +5,7 @@ import {TupleMap} from '#wiki-data'; import { isArray, + isString, isWholeNumber, oneOf, validateArrayItems, @@ -1426,17 +1427,24 @@ export function debugComposite(fn) { // compositional step, the property will be exposed as undefined instead // of null. // -export function exposeDependency({dependency}) { - return { - annotation: `exposeDependency`, - flags: {expose: true}, +export const exposeDependency = templateCompositeFrom({ + annotation: `exposeDependency`, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, + compose: false, + + inputs: { + dependency: input.staticDependency(), + }, + + steps: () => [ + { + dependencies: [input('dependency')], + compute: ({ + [input('dependency')]: dependency + }) => dependency, }, - }; -} + ], +}); // Exposes a constant value exactly as it is; like exposeDependency, this // is typically the base of a composition serving as a particular property @@ -1488,7 +1496,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ dependencies: [input('from'), input('mode')], compute: (continuation, { - [input('from')]: dependency, + [input('from')]: value, [input('mode')]: mode, }) => { let availability; @@ -1591,7 +1599,7 @@ export const exitWithoutDependency = templateCompositeFrom({ { dependencies: ['#availability', input('value')], - continuation: (continuation, { + compute: (continuation, { ['#availability']: availability, [input('value')]: value, }) => @@ -1628,9 +1636,13 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), mode: input(availabilityCheckModeInput), - output: input({defaultValue: {}}), + output: input.staticValue({defaultValue: {}}), }, + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), @@ -1657,9 +1669,13 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ inputs: { mode: input(availabilityCheckModeInput), - output: input({defaultValue: {}}), + output: input.staticValue({defaultValue: {}}), }, + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + steps: () => [ withResultOfAvailabilityCheck({ from: input.updateValue(), @@ -1820,41 +1836,81 @@ export function withPropertyFromList({ // Gets the listed properties from each of a list of objects, providing lists // of property values each into a dependency prefixed with the same name as the // list (by default). Like withPropertyFromList, this doesn't alter indices. -export function withPropertiesFromList({ - list, - properties, - prefix = - (list.startsWith('#') - ? list - : `#${list}`), -}) { - return { - annotation: `withPropertiesFromList`, - flags: {expose: true, compose: true}, +export const withPropertiesFromList = templateCompositeFrom({ + annotation: `withPropertiesFromList`, - expose: { - mapDependencies: {list}, - options: {prefix, properties}, + inputs: { + list: input({type: 'array'}), + + properties: input({ + validate: validateArrayItems(isString), + }), - compute(continuation, {list, '#options': {prefix, properties}}) { - const lists = + prefix: input({ + type: 'string', + null: true, + }), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`)) + : '#lists'), + + steps: () => [ + { + dependencies: [input('list'), input('properties')], + compute: (continuation, { + [input('list')]: list, + [input('properties')]: properties, + }) => continuation({ + ['#lists']: Object.fromEntries( - properties.map(property => [`${prefix}.${property}`, []])); + properties.map(property => [ + property, + list.map(item => item[property] ?? null), + ])), + }), + }, - for (const item of list) { - for (const property of properties) { - lists[`${prefix}.${property}`].push( - (item === null || item === undefined - ? null - : item[property] ?? null)); - } - } + { + dependencies: [ + input.staticDependency('list'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#lists', + ], - return continuation(lists); - } - } - } -} + compute: (continuation, { + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#lists']: lists, + }) => + (properties + ? continuation( + Object.fromEntries( + properties.map(property => [ + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`), + lists[property], + ]))) + : continuation({'#lists': lists})), + }, + ], +}); // Replaces items of a list, which are null or undefined, with some fallback // value, either a constant (set {value}) or from a dependency ({dependency}). diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 265cfe18..f63a619d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -363,7 +363,7 @@ export const contribsPresent = templateCompositeFrom({ steps: () => [ withResultOfAvailabilityCheck({ - fromDependency: input('contribs'), + from: input('contribs'), mode: input.value('empty'), }), diff --git a/src/data/things/track.js b/src/data/things/track.js index 05b762b9..f31fe3ae 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -68,10 +68,18 @@ export class Track extends Thing { }), withContainingTrackSection(), - withPropertyFromObject({object: '#trackSection', property: 'color'}), + + withPropertyFromObject({ + object: '#trackSection', + property: input.value('color'), + }), + exposeDependencyOrContinue({dependency: '#trackSection.color'}), - withPropertyFromAlbum({property: 'color'}), + withPropertyFromAlbum({ + property: input.value('color'), + }), + exposeDependency({dependency: '#album.color'}), ], @@ -127,9 +135,15 @@ export class Track extends Thing { validate: input.value(isFileExtension), }), - withPropertyFromAlbum({property: 'trackCoverArtFileExtension'}), + withPropertyFromAlbum({ + property: input.value('trackCoverArtFileExtension'), + }), + exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - exposeConstant({value: 'jpg'}), + + exposeConstant({ + value: input.value('jpg'), + }), ], // Date of cover art release. Like coverArtFileExtension, this represents @@ -148,7 +162,10 @@ export class Track extends Thing { validate: input.value(isDate), }), - withPropertyFromAlbum({property: 'trackArtDate'}), + withPropertyFromAlbum({ + property: input.value('trackArtDate'), + }), + exposeDependency({dependency: '#album.trackArtDate'}), ], @@ -185,7 +202,10 @@ export class Track extends Thing { exposeDependencyOrContinue({dependency: '#artistContribs'}), - withPropertyFromAlbum({property: 'artistContribs'}), + withPropertyFromAlbum({ + property: input.value('artistContribs'), + }), + exposeDependency({dependency: '#album.artistContribs'}), ], @@ -208,7 +228,10 @@ export class Track extends Thing { exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), - withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), + withPropertyFromAlbum({ + property: input.value('trackCoverArtistContribs'), + }), + exposeDependency({dependency: '#album.trackCoverArtistContribs'}), ], @@ -257,7 +280,11 @@ export class Track extends Thing { date: [ exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), - withPropertyFromAlbum({property: 'date'}), + + withPropertyFromAlbum({ + property: input.value('date'), + }), + exposeDependency({dependency: '#album.date'}), ], @@ -608,9 +635,7 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ : continuation()), }, - withResolvedContribs({ - from: 'coverArtistContribs', - }), + withResolvedContribs({from: 'coverArtistContribs'}), { dependencies: ['#resolvedContribs'], @@ -625,7 +650,9 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ })), }, - withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), + withPropertyFromAlbum({ + property: input.value('trackCoverArtistContribs'), + }), { dependencies: ['#album.trackCoverArtistContribs'], @@ -663,6 +690,8 @@ export const exitWithoutUniqueCoverArt = templateCompositeFrom({ export const trackReverseReferenceList = templateCompositeFrom({ annotation: `trackReverseReferenceList`, + compose: false, + inputs: { list: input({type: 'string'}), }, -- cgit 1.3.0-6-gf8a5 From 6ca1e4b3ad691478e94f09cfe94683cb079f6bdf Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 14:20:19 -0300 Subject: data: update withPropertiesFromObject --- src/data/things/composite.js | 103 ++++++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cfa557de..4be01a55 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1764,34 +1764,85 @@ export const withPropertyFromObject = templateCompositeFrom({ // as a dependency prefixed with the same name as the object (by default). // If the object itself is null, all provided dependencies will be null; // if it's missing only select properties, those will be provided as null. -export function withPropertiesFromObject({ - object, - properties, - prefix = - (object.startsWith('#') - ? object - : `#${object}`), -}) { - return { - annotation: `withPropertiesFromObject`, - flags: {expose: true, compose: true}, +export const withPropertiesFromObject = templateCompositeFrom({ + annotation: `withPropertiesFromObject`, - expose: { - mapDependencies: {object}, - options: {prefix, properties}, + inputs: { + object: input({ + type: 'object', + null: true, + }), - compute: (continuation, {object, '#options': {prefix, properties}}) => - continuation( - Object.fromEntries( - properties.map(property => [ - `${prefix}.${property}`, - (object === null || object === undefined - ? null - : object[property] ?? null), - ]))), + properties: input({ + validate: validateArrayItems(isString), + }), + + prefix: input.staticValue({ + type: 'string', + null: true, + }), + }, + + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`)) + : '#object'), + + steps: () => [ + { + dependencies: [input('object'), input('properties')], + compute: (continuation, { + [input('object')]: object, + [input('properties')]: properties, + }) => continuation({ + ['#entries']: + (object === null + ? properties.map(property => [property, null]) + : properties.map(property => [property, object[property]])), + }), }, - }; -} + + { + dependencies: [ + input.staticDependency('object'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#entries', + ], + + compute: ({ + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#entries']: entries, + }) => + (properties + ? continuation( + Object.fromEntries( + entries.map(([property, value]) => [ + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`), + value ?? null, + ]))) + : continuation({ + ['#object']: + Object.fromEntries(entries), + })), + }, + ], +}); // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions @@ -1846,7 +1897,7 @@ export const withPropertiesFromList = templateCompositeFrom({ validate: validateArrayItems(isString), }), - prefix: input({ + prefix: input.staticValue({ type: 'string', null: true, }), -- cgit 1.3.0-6-gf8a5 From ee3b52cfe889eb514f5d6a5f78297875f278e206 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 14:37:20 -0300 Subject: data: update exposeConstant, fillMissingListItems --- src/data/things/composite.js | 103 ++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4be01a55..40f4fc16 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1451,17 +1451,24 @@ export const exposeDependency = templateCompositeFrom({ // descriptor. It generally follows steps which will conditionally early // exit with some other value, with the exposeConstant base serving as the // fallback default value. -export function exposeConstant({value}) { - return { - annotation: `exposeConstant`, - flags: {expose: true}, +export const exposeConstant = templateCompositeFrom({ + annotation: `exposeConstant`, - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, + compose: false, + + inputs: { + value: input.staticValue(), + }, + + steps: () => [ + { + dependencies: [input('value')], + compute: ({ + [input('value')]: value, + }) => value, }, - }; -} + ], +}); // Checks the availability of a dependency and provides the result to later // steps under '#availability' (by default). This is mainly intended for use @@ -1964,55 +1971,43 @@ export const withPropertiesFromList = templateCompositeFrom({ }); // Replaces items of a list, which are null or undefined, with some fallback -// value, either a constant (set {value}) or from a dependency ({dependency}). -// By default, this replaces the passed dependency. -export function fillMissingListItems({ - list, - value, - dependency, - into = list, -}) { - if (value !== undefined && dependency !== undefined) { - throw new TypeError(`Don't provide both value and dependency`); - } +// value. By default, this replaces the passed dependency. +export const fillMissingListItems = templateCompositeFrom({ + annotation: `fillMissingListItems`, - if (value === undefined && dependency === undefined) { - throw new TypeError(`Missing value or dependency`); - } - - if (dependency) { - return { - annotation: `fillMissingListItems.fromDependency`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list, dependency}, - mapContinuation: {into}, + inputs: { + list: input({type: 'array'}), + fill: input(), + }, - compute: (continuation, {list, dependency}) => - continuation({ - into: list.map(item => item ?? dependency), - }), - }, - }; - } else { - return { - annotation: `fillMissingListItems.fromValue`, - flags: {expose: true, compose: true}, + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {value}, + steps: () => [ + { + dependencies: [input('list'), input('fill')], + compute: (continuation, { + [input('list')]: list, + [input('fill')]: fill, + }) => continuation({ + ['#filled']: + list.map(item => item ?? fill), + }), + }, - compute: (continuation, {list, '#options': {value}}) => - continuation({ - into: list.map(item => item ?? value), - }), - }, - }; - } -} + { + dependencies: [input.staticDependency('list'), '#filled'], + compute: (continuation, { + [input.staticDependency('list')]: list, + ['#filled']: filled, + }) => continuation({ + [list ?? '#list']: + filled, + }), + }, + ], +}); // Filters particular values out of a list. Note that this will always // completely skip over null, but can be used to filter out any other -- cgit 1.3.0-6-gf8a5 From e3e8a904c24e71f303a1f29c8f1700478d929901 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 14:37:36 -0300 Subject: data: miscellaneous syntax fixes --- src/data/things/album.js | 17 ++++++++++++++--- src/data/things/composite.js | 6 +++--- src/data/things/thing.js | 4 ++-- src/data/things/track.js | 25 ++++++++++++++++--------- 4 files changed, 35 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index 20a1a5b3..fd8a71d3 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -122,9 +122,20 @@ export class Album extends Thing { ]), }), - fillMissingListItems({list: '#sections.tracks', value: []}), - fillMissingListItems({list: '#sections.isDefaultTrackSection', value: false}), - fillMissingListItems({list: '#sections.color', dependency: 'color'}), + fillMissingListItems({ + list: '#sections.tracks', + fill: input.value([]), + }), + + fillMissingListItems({ + list: '#sections.isDefaultTrackSection', + fill: input.value(false), + }), + + fillMissingListItems({ + list: '#sections.color', + fill: input.dependency('color'), + }), withFlattenedList({ list: '#sections.tracks', diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 40f4fc16..38b7bcc9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -2073,7 +2073,7 @@ export const withFlattenedList = templateCompositeFrom({ const flattenedList = sourceList.flat(); const indices = []; let lastEndIndex = 0; - for (const {length} of sourceArray) { + for (const {length} of sourceList) { indices.push(lastEndIndex); lastEndIndex += length; } @@ -2116,8 +2116,8 @@ export const withUnflattenedList = templateCompositeFrom({ steps: () => [ { - dependencies: [input('list'), input('indices')], - compute({ + dependencies: [input('list'), input('indices'), input('filter')], + compute(continuation, { [input('list')]: list, [input('indices')]: indices, [input('filter')]: filter, diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f63a619d..0dea1fa4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -216,7 +216,7 @@ export function contributionList() { steps: [ withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({value: []}), + exposeConstant({value: input.value([])}), ], }); } @@ -343,7 +343,7 @@ export const singleReference = templateCompositeFrom({ withResolvedReference({ ref: input.updateValue(), data: input('data'), - find: input('findFunction'), + find: input('find'), }), exposeDependency({dependency: '#resolvedReference'}), diff --git a/src/data/things/track.js b/src/data/things/track.js index f31fe3ae..5ccf4f8b 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -210,7 +210,10 @@ export class Track extends Thing { ], contributorContribs: [ - inheritFromOriginalRelease({property: 'contributorContribs'}), + inheritFromOriginalRelease({ + property: input.value('contributorContribs'), + }), + contributionList(), ], @@ -236,7 +239,9 @@ export class Track extends Thing { ], referencedTracks: [ - inheritFromOriginalRelease({property: 'referencedTracks'}), + inheritFromOriginalRelease({ + property: input.value('referencedTracks'), + }), referenceList({ class: input.value(Track), @@ -246,7 +251,9 @@ export class Track extends Thing { ], sampledTracks: [ - inheritFromOriginalRelease({property: 'sampledTracks'}), + inheritFromOriginalRelease({ + property: input.value('sampledTracks'), + }), referenceList({ class: input.value(Track), @@ -313,11 +320,11 @@ export class Track extends Thing { { flags: {expose: true}, expose: { - dependencies: ['this', 'trackData', '#originalRelease'], + dependencies: [input.myself(), '#originalRelease', 'trackData'], compute: ({ - this: thisTrack, + [input.myself()]: thisTrack, + ['#originalRelease']: originalRelease, trackData, - '#originalRelease': originalRelease, }) => (originalRelease === thisTrack ? [] @@ -339,17 +346,17 @@ export class Track extends Thing { // the "Tracks - by Times Referenced" listing page (or other data // processing). referencedByTracks: trackReverseReferenceList({ - list: 'referencedTracks', + list: input.value('referencedTracks'), }), // For the same reasoning, exclude re-releases from sampled tracks too. sampledByTracks: trackReverseReferenceList({ - list: 'sampledTracks', + list: input.value('sampledTracks'), }), featuredInFlashes: reverseReferenceList({ data: 'flashData', - list: 'featuredTracks', + list: input.value('featuredTracks'), }), }); -- cgit 1.3.0-6-gf8a5 From c1018a0163ae28dc122aad7cb292a5e805c3d25a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 15:30:49 -0300 Subject: data: fix update collation from steps --- src/data/things/composite.js | 48 +++++++++++++++++++++++--------------------- src/data/things/thing.js | 35 +++++++++++--------------------- 2 files changed, 37 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 38b7bcc9..f744f604 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -573,13 +573,22 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.close(); + const inputMetadata = getStaticInputMetadata(inputOptions); + const expectedOutputNames = (Array.isArray(description.outputs) ? description.outputs : typeof description.outputs === 'function' - ? description.outputs(getStaticInputMetadata(inputOptions)) + ? description.outputs(inputMetadata) : []); + const ownUpdateDescription = + (typeof description.update === 'object' + ? description.update + : typeof description.update === 'function' + ? description.update(inputMetadata) + : null); + const outputOptions = {}; const instantiatedTemplate = { @@ -644,8 +653,8 @@ export function templateCompositeFrom(description) { finalDescription.compose = description.compose; } - if ('update' in description) { - finalDescription.update = description.update; + if (ownUpdateDescription) { + finalDescription.update = ownUpdateDescription; } if ('inputs' in description) { @@ -820,7 +829,6 @@ export function compositeFrom(description) { const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); - const updateDescription = {}; // Steps default to exposing if using a shorthand syntax where flags aren't // specified at all. @@ -932,7 +940,6 @@ export function compositeFrom(description) { const stepEntries = stitchArrays({ step: steps, expose: stepExposeDescriptions, - update: stepUpdateDescriptions, stepComposes: stepsCompose, stepComputes: stepsCompute, stepTransforms: stepsTransform, @@ -942,7 +949,6 @@ export function compositeFrom(description) { const { step, expose, - update, stepComposes, stepComputes, stepTransforms, @@ -974,12 +980,6 @@ export function compositeFrom(description) { return push(new TypeError( `Steps which only transform can't be used in a composition that doesn't update`)); } - - if (update) { - // TODO: This is a dumb assign statement, and it could probably do more - // interesting things, like combining validation functions. - Object.assign(updateDescription, update); - } }); } @@ -1332,8 +1332,13 @@ export function compositeFrom(description) { compose: compositionNests, }; - if (constructedDescriptor.update) { - constructedDescriptor.update = updateDescription; + if (compositionUpdates) { + // TODO: This is a dumb assign statement, and it could probably do more + // interesting things, like combining validation functions. + constructedDescriptor.update = + Object.assign( + {...description.update ?? {}}, + ...stepUpdateDescriptions.filter(Boolean)); } if (compositionExposes) { @@ -1569,15 +1574,12 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ validate: input({type: 'function', null: true}), }, - update: { - dependencies: [input.staticValue('validate')], - compute: ({ - [input.staticValue('validate')]: validate, - }) => - (validate - ? {validate} - : {}), - }, + update: ({ + [input.staticValue('validate')]: validate, + }) => + (validate + ? {validate} + : {}), steps: () => [ exposeDependencyOrContinue({ diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0dea1fa4..ca1018eb 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -281,24 +281,19 @@ export const referenceList = templateCompositeFrom({ compose: false, inputs: { - class: input(thingClassInput), + class: input.staticValue(thingClassInput), + find: input({type: 'function'}), // todo: validate data: input(), }, - update: { - dependencies: [ - input.staticValue('class'), - ], - - compute({ - [input.staticValue('class')]: thingClass, - }) { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReferenceList(referenceType)}; - }, + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; }, steps: () => [ @@ -326,17 +321,11 @@ export const singleReference = templateCompositeFrom({ data: input(), }, - update: { - dependencies: [ - input.staticValue('class'), - ], - - compute({ - [input.staticValue('class')]: thingClass, - }) { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReference(referenceType)}; - }, + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; }, steps: () => [ -- cgit 1.3.0-6-gf8a5 From cb124756780e41c6791981233da4b56c031d6142 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 15:52:46 -0300 Subject: data: support update description in input.updateValue() --- src/data/things/composite.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f744f604..e6cc267a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -369,7 +369,15 @@ export function input(nameOrDescription) { input.symbol = Symbol.for('hsmusic.composite.input'); -input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.updateValue = (description = null) => + (description + ? { + symbol: input.symbol, + shape: 'input.updateValue', + value: description, + } + : Symbol.for('hsmusic.composite.input.updateValue')); + input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); @@ -814,7 +822,9 @@ export function compositeFrom(description) { .filter(Boolean); const anyInputsUseUpdateValue = - dependenciesFromInputs.includes(input.updateValue()); + dependenciesFromInputs + .filter(dependency => isInputToken(dependency)) + .some(token => getInputTokenShape(token) === 'input.updateValue'); const base = composition.at(-1); const steps = composition.slice(); @@ -871,13 +881,22 @@ export function compositeFrom(description) { : null)); // The update description for a step, if present at all, is always set - // explicitly. + // explicitly. There may be multiple per step - namely that step's own + // {update} description, and any descriptions passed as the value in an + // input.updateValue({...}) token. const stepUpdateDescriptions = steps .map((step, index) => (stepsUpdate[index] - ? step.update ?? null - : null)); + ? [ + step.update ?? null, + ...(stepExposeDescriptions[index]?.dependencies ?? []) + .filter(dependency => isInputToken(dependency)) + .filter(token => getInputTokenShape(token) === 'input.updateValue') + .map(token => getInputTokenValue(token)) + .filter(Boolean), + ] + : [])); // Indicates presence of a {compute} function on the expose description. const stepsCompute = @@ -1338,7 +1357,7 @@ export function compositeFrom(description) { constructedDescriptor.update = Object.assign( {...description.update ?? {}}, - ...stepUpdateDescriptions.filter(Boolean)); + ...stepUpdateDescriptions.flat()); } if (compositionExposes) { -- cgit 1.3.0-6-gf8a5 From 8e3e15be98d43c1aa8a4f13709106f6848a0a9e4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 15:53:35 -0300 Subject: data: use error.cause for nested composite compute errors --- src/data/things/composite.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e6cc267a..4074aef7 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1369,15 +1369,27 @@ export function compositeFrom(description) { ...dependenciesFromSteps, ]); + const _wrapper = (...args) => { + try { + return _computeOrTransform(...args); + } catch (thrownError) { + const error = new Error( + `Error computing composition` + + (annotation ? ` ${annotation}` : '')); + error.cause = thrownError; + throw error; + } + }; + if (compositionNests) { if (compositionUpdates) { expose.transform = (value, continuation, dependencies) => - _computeOrTransform(value, continuation, dependencies); + _wrapper(value, continuation, dependencies); } if (anyStepsCompute) { expose.compute = (continuation, dependencies) => - _computeOrTransform(noTransformSymbol, continuation, dependencies); + _wrapper(noTransformSymbol, continuation, dependencies); } if (base.cacheComposition) { @@ -1385,10 +1397,10 @@ export function compositeFrom(description) { } } else if (compositionUpdates) { expose.transform = (value, dependencies) => - _computeOrTransform(value, null, dependencies); + _wrapper(value, null, dependencies); } else { expose.compute = (dependencies) => - _computeOrTransform(noTransformSymbol, null, dependencies); + _wrapper(noTransformSymbol, null, dependencies); } } -- cgit 1.3.0-6-gf8a5 From 998cc860302e3fb1e7a40c055e8ac66f195b1366 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 15:54:34 -0300 Subject: data: withResultOfAvailabilityCheck: handle undefined in 'empty' --- src/data/things/composite.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4074aef7..700cc922 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1512,8 +1512,8 @@ export const exposeConstant = templateCompositeFrom({ // Customize {mode} to select one of these modes, or default to 'null': // // * 'null': Check that the value isn't null (and not undefined either). -// * 'empty': Check that the value is neither null nor an empty array. -// This will outright error for undefined. +// * 'empty': Check that the value is neither null, undefined, nor an empty +// array. // * 'falsy': Check that the value isn't false when treated as a boolean // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! @@ -1546,11 +1546,11 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ switch (mode) { case 'null': - availability = value !== null && value !== undefined; + availability = value !== undefined && value !== null; break; case 'empty': - availability = !empty(value); + availability = value !== undefined && !empty(value); break; case 'falsy': -- cgit 1.3.0-6-gf8a5 From 4852931ecf2c7ce63851ea6f3a60c9d5b142ae6f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 16:04:06 -0300 Subject: data: minor fixes --- src/data/things/thing.js | 2 +- src/data/things/track.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ca1018eb..77f549fe 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -602,7 +602,7 @@ export const withResolvedReference = templateCompositeFrom({ } return continuation.raiseOutput({ - ['#resolvedReference']: match, + ['#resolvedReference']: match ?? null, }); }, }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 5ccf4f8b..54b2c124 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -638,7 +638,9 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ dependencies: ['disableUniqueCoverArt'], compute: (continuation, {disableUniqueCoverArt}) => (disableUniqueCoverArt - ? continuation.raiseOutput({into: false}) + ? continuation.raiseOutput({ + ['#hasUniqueCoverArt']: false, + }) : continuation()), }, @@ -652,8 +654,7 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ (empty(contribsFromTrack) ? continuation() : continuation.raiseOutput({ - ['#hasUniqueCoverArt']: - true, + ['#hasUniqueCoverArt']: true, })), }, -- cgit 1.3.0-6-gf8a5 From 72c526dfeee2b227400b73c3b220cf36c885b703 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 17:01:21 -0300 Subject: data: auto-prefix '#' in output names --- src/data/things/composite.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 700cc922..26df71ae 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -588,6 +588,10 @@ export function templateCompositeFrom(description) { ? description.outputs : typeof description.outputs === 'function' ? description.outputs(inputMetadata) + .map(name => + (name.startsWith('#') + ? name + : '#' + name)) : []); const ownUpdateDescription = @@ -797,7 +801,9 @@ export function compositeFrom(description) { Object.entries(description.outputs) .map(([continuationName, outputName]) => [ outputName, - providedDependencies[continuationName], + (continuationName in providedDependencies + ? providedDependencies[continuationName] + : providedDependencies[continuationName.replace(/^#/, '')]), ]))); } -- cgit 1.3.0-6-gf8a5 From 981a39a5f3c2a592c84f92692c204b090622aec9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 17:01:51 -0300 Subject: data: fix input.myself() not being spotted in inputs --- src/data/things/composite.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26df71ae..7a9048c2 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -821,6 +821,8 @@ export function compositeFrom(description) { case 'input': case 'input.updateValue': return token; + case 'input.myself': + return 'this'; default: return null; } -- cgit 1.3.0-6-gf8a5 From 64476074cbc7375afe2388ddd6e9e3275c25f3bd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 21 Sep 2023 17:02:18 -0300 Subject: data: minor fixes --- src/data/things/track.js | 4 +++- src/upd8.js | 4 ---- 2 files changed, 3 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 54b2c124..3e0d95bf 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -192,7 +192,9 @@ export class Track extends Thing { }), artistContribs: [ - inheritFromOriginalRelease({property: 'artistContribs'}), + inheritFromOriginalRelease({ + property: input.value('artistContribs'), + }), withResolvedContribs({ from: input.updateValue({validate: isContributionList}), diff --git a/src/upd8.js b/src/upd8.js index 92e89eaf..2cc8f554 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -613,10 +613,6 @@ async function main() { // which are only available after the initial linking. sortWikiDataArrays(wikiData); - console.log( - CacheableObject.getUpdateValue(wikiData.albumData[0], 'trackSections'), - wikiData.albumData[0].trackSections); - if (precacheData) { progressCallAll('Caching all data values', Object.entries(wikiData) .filter(([key]) => -- cgit 1.3.0-6-gf8a5 From e14ed656f5bd1577118d053317037377c1a7a818 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 22 Sep 2023 14:00:16 -0300 Subject: data: miscellaneous improvements/fixes for updating composites --- src/data/things/composite.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7a9048c2..98537c95 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -869,13 +869,18 @@ export function compositeFrom(description) { ? compositionNests : true))); - // Steps don't update unless the corresponding flag is explicitly set. + // Steps update if the corresponding flag is explicitly set, if a transform + // function is provided, or if the dependencies include an input.updateValue + // token. const stepsUpdate = steps .map(step => (step.flags ? step.flags.update ?? false - : false)); + : !!step.transform || + !!step.dependencies?.some(dependency => + isInputToken(dependency) && + getInputTokenShape(dependency) === 'input.updateValue'))); // The expose description for a step is just the entire step object, when // using the shorthand syntax where {flags: {expose: true}} is left implied. @@ -901,9 +906,8 @@ export function compositeFrom(description) { ...(stepExposeDescriptions[index]?.dependencies ?? []) .filter(dependency => isInputToken(dependency)) .filter(token => getInputTokenShape(token) === 'input.updateValue') - .map(token => getInputTokenValue(token)) - .filter(Boolean), - ] + .map(token => getInputTokenValue(token)), + ].filter(Boolean) : [])); // Indicates presence of a {compute} function on the expose description. @@ -960,6 +964,7 @@ export function compositeFrom(description) { anyStepsExpose; const compositionUpdates = + 'update' in description || anyInputsUseUpdateValue || anyStepsUseUpdateValue || anyStepsUpdate; @@ -1010,8 +1015,8 @@ export function compositeFrom(description) { }); } - if (!compositionNests && !anyStepsUpdate && !anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute or update`)); + if (!compositionNests && !anyStepsCompute && !anyStepsTransform) { + aggregate.push(new TypeError(`Expected at least one step to compute or transform`)); } aggregate.close(); @@ -1341,6 +1346,7 @@ export function compositeFrom(description) { return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, providedDependencies); + if (providedValue !== null) valueSoFar = providedValue; break; } } @@ -1395,7 +1401,7 @@ export function compositeFrom(description) { _wrapper(value, continuation, dependencies); } - if (anyStepsCompute) { + if (anyStepsCompute && !anyStepsUseUpdateValue && !anyInputsUseUpdateValue) { expose.compute = (continuation, dependencies) => _wrapper(noTransformSymbol, continuation, dependencies); } -- cgit 1.3.0-6-gf8a5 From 7f7c50e7976bebc937c302638cade5e1fd543ff4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 22 Sep 2023 14:00:43 -0300 Subject: data: improve selecting values for input tokens in dependencies --- src/data/things/composite.js | 45 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 98537c95..da2848f8 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1163,21 +1163,46 @@ export function compositeFrom(description) { let continuationStorage; + const filterableDependencies = { + ...availableDependencies, + ...inputMetadata, + ...inputValues, + ... + (expectingTransform + ? {[input.updateValue()]: valueSoFar} + : {}), + [input.myself()]: initialDependencies?.['this'] ?? null, + }; + + const selectDependencies = + (expose.dependencies ?? []).map(dependency => { + if (!isInputToken(dependency)) return dependency; + const tokenShape = getInputTokenShape(dependency); + const tokenValue = getInputTokenValue(dependency); + switch (tokenShape) { + case 'input': + case 'input.staticDependency': + case 'input.staticValue': + return dependency; + case 'input.myself': + return input.myself(); + case 'input.dependency': + return tokenValue; + case 'input.updateValue': + return input.updateValue(); + default: + throw new Error(`Unexpected token ${tokenShape} as dependency`); + } + }) + const filteredDependencies = - filterProperties({ - ...availableDependencies, - ...inputMetadata, - ...inputValues, - ... - (callingTransformForThisStep - ? {[input.updateValue()]: valueSoFar} - : {}), - [input.myself()]: initialDependencies['this'], - }, expose.dependencies ?? []); + filterProperties(filterableDependencies, selectDependencies); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies, + `selecting:`, selectDependencies, + `from available:`, filterableDependencies, ...callingTransformForThisStep ? [`from value:`, valueSoFar] : []]); let result; -- cgit 1.3.0-6-gf8a5 From 8bcae16b391762f6b533654ec06c3bf0c8770d35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 23 Sep 2023 20:24:08 -0300 Subject: data, test: WIP tests for compositeFrom --- src/data/things/composite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index da2848f8..c0f0ab0b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -752,6 +752,9 @@ export function templateCompositeFrom(description) { templateCompositeFrom.symbol = Symbol(); +export const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); +export const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); + export function compositeFrom(description) { const {annotation} = description; @@ -1070,9 +1073,6 @@ export function compositeFrom(description) { return {continuation, continuationStorage}; } - const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); - const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); - function _computeOrTransform(initialValue, continuationIfApplicable, initialDependencies) { const expectingTransform = initialValue !== noTransformSymbol; -- cgit 1.3.0-6-gf8a5 From f3d98f5ea63db7f7b2155e7efb0812f025c5bcf3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 23 Sep 2023 20:35:57 -0300 Subject: data, test: collate update description from composition inputs --- src/data/things/composite.js | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c0f0ab0b..34e550a1 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -837,6 +837,16 @@ export function compositeFrom(description) { .filter(dependency => isInputToken(dependency)) .some(token => getInputTokenShape(token) === 'input.updateValue'); + // Update descriptions passed as the value in an input.updateValue() token, + // as provided as inputs for this composition. + const inputUpdateDescriptions = + Object.values(description.inputs ?? {}) + .map(token => + (getInputTokenShape(token) === 'input.updateValue' + ? getInputTokenValue(token) + : null)) + .filter(Boolean); + const base = composition.at(-1); const steps = composition.slice(); @@ -1396,6 +1406,7 @@ export function compositeFrom(description) { constructedDescriptor.update = Object.assign( {...description.update ?? {}}, + ...inputUpdateDescriptions, ...stepUpdateDescriptions.flat()); } -- cgit 1.3.0-6-gf8a5 From 84b09a42c7baf248115f596217c07871e374d1af Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 23 Sep 2023 22:10:38 -0300 Subject: data: fix updating valueSoFar on non-transform calls --- src/data/things/composite.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 34e550a1..fdb80cf3 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1381,7 +1381,9 @@ export function compositeFrom(description) { return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, providedDependencies); - if (providedValue !== null) valueSoFar = providedValue; + if (callingTransformForThisStep && providedValue !== null) { + valueSoFar = providedValue; + } break; } } -- cgit 1.3.0-6-gf8a5 From 3b458e5c403054bda58733e238ab666596cc9f70 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 23 Sep 2023 22:12:54 -0300 Subject: data: refactor/tidy input token construction --- src/data/things/composite.js | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fdb80cf3..293952b7 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -355,35 +355,30 @@ import { const globalCompositeCache = {}; -export function input(nameOrDescription) { - if (typeof nameOrDescription === 'string') { - return Symbol.for(`hsmusic.composite.input:${nameOrDescription}`); - } else { - return { - symbol: Symbol.for('hsmusic.composite.input'), - shape: 'input', - value: nameOrDescription, - }; - } -} +const _valueIntoToken = shape => + (value = null) => + (value === null + ? Symbol.for(`hsmusic.composite.${shape}`) + : typeof value === 'string' + ? Symbol.for(`hsmusic.composite.${shape}:${value}`) + : { + symbol: Symbol.for(`hsmusic.composite.input`), + shape, + value, + }); +export const input = _valueIntoToken('input'); input.symbol = Symbol.for('hsmusic.composite.input'); -input.updateValue = (description = null) => - (description - ? { - symbol: input.symbol, - shape: 'input.updateValue', - value: description, - } - : Symbol.for('hsmusic.composite.input.updateValue')); +input.value = _valueIntoToken('input.value'); +input.dependency = _valueIntoToken('input.dependency'); input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); -input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); -input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); -input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); -input.staticValue = name => Symbol.for(`hsmusic.composite.input.staticValue:${name}`); +input.updateValue = _valueIntoToken('input.updateValue'); + +input.staticDependency = _valueIntoToken('input.staticDependency'); +input.staticValue = _valueIntoToken('input.staticValue'); function isInputToken(token) { if (typeof token === 'object') { -- cgit 1.3.0-6-gf8a5 From 8d674012710d673b773e13b0cf0be9abba6ccc00 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 23 Sep 2023 22:13:35 -0300 Subject: util: showAggregate: show error causes --- src/util/sugar.js | 49 +++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/util/sugar.js b/src/util/sugar.js index ebb7d61e..14fb250e 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -544,15 +544,17 @@ export function showAggregate(topError, { print = true, } = {}) { const recursive = (error, {level}) => { - let header = showTraces + let headerPart = showTraces ? `[${error.constructor.name || 'unnamed'}] ${ error.message || '(no message)' }` : error instanceof AggregateError ? `[${error.message || '(no message)'}]` : error.message || '(no message)'; + if (showTraces) { const stackLines = error.stack?.split('\n'); + const stackLine = stackLines?.find( (line) => line.trim().startsWith('at') && @@ -560,30 +562,41 @@ export function showAggregate(topError, { !line.includes('node:') && !line.includes('<anonymous>') ); + const tracePart = stackLine ? '- ' + stackLine .trim() .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match)) : '(no stack trace)'; - header += ` ${colors.dim(tracePart)}`; - } - const bar = level % 2 === 0 ? '\u2502' : colors.dim('\u254e'); - const head = level % 2 === 0 ? '\u257f' : colors.dim('\u257f'); - - if (error instanceof AggregateError) { - return ( - header + - '\n' + - error.errors - .map((error) => recursive(error, {level: level + 1})) - .flatMap((str) => str.split('\n')) - .map((line, i) => i === 0 ? ` ${head} ${line}` : ` ${bar} ${line}`) - .join('\n') - ); - } else { - return header; + + headerPart += ` ${colors.dim(tracePart)}`; } + + const head1 = level % 2 === 0 ? '\u21aa' : colors.dim('\u21aa'); + const bar1 = ' '; + + const causePart = + (error.cause + ? recursive(error.cause, {level: level + 1}) + .split('\n') + .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`) + .join('\n') + : ''); + + const head2 = level % 2 === 0 ? '\u257f' : colors.dim('\u257f'); + const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e'); + + const aggregatePart = + (error instanceof AggregateError + ? error.errors + .map(error => recursive(error, {level: level + 1})) + .flatMap(str => str.split('\n')) + .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`) + .join('\n') + : ''); + + return [headerPart, causePart, aggregatePart].filter(Boolean).join('\n'); }; const message = -- cgit 1.3.0-6-gf8a5 From b4137b02f09761b78c520e5514381cda714dcf6d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 23 Sep 2023 22:14:11 -0300 Subject: data: fix calls to oneOf instead of is --- src/data/things/composite.js | 4 ++-- src/data/things/thing.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 293952b7..791b8360 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -4,10 +4,10 @@ import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; import { + is, isArray, isString, isWholeNumber, - oneOf, validateArrayItems, } from '#validators'; @@ -1567,7 +1567,7 @@ export const exposeConstant = templateCompositeFrom({ // const availabilityCheckModeInput = { - validate: oneOf('null', 'empty', 'falsy'), + validate: is('null', 'empty', 'falsy'), defaultValue: 'null', }; diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 77f549fe..ef547f74 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -7,7 +7,7 @@ import {colors} from '#cli'; import find from '#find'; import {stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; -import {oneOf} from '#validators'; +import {is} from '#validators'; import { compositeFrom, @@ -460,7 +460,7 @@ export const withResolvedContribs = templateCompositeFrom({ from: input(), notFoundMode: input({ - validate: oneOf('exit', 'filter', 'null'), + validate: is('exit', 'filter', 'null'), defaultValue: 'null', }), }, @@ -562,7 +562,7 @@ export const withResolvedReference = templateCompositeFrom({ find: input({type: 'function'}), notFoundMode: input({ - validate: oneOf('null', 'exit'), + validate: is('null', 'exit'), defaultValue: 'null', }), }, @@ -627,7 +627,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ find: input({type: 'function'}), notFoundMode: input({ - validate: oneOf('exit', 'filter', 'null'), + validate: is('exit', 'filter', 'null'), defaultValue: 'filter', }), }, -- cgit 1.3.0-6-gf8a5 From e304bebf19340b825df10a17315b534f5dca0219 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 23 Sep 2023 22:15:06 -0300 Subject: data: WIP input validation Static only, as of this commit. --- src/data/things/composite.js | 55 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 791b8360..27b345cd 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -548,7 +548,12 @@ export function templateCompositeFrom(description) { }); const wrongTypeInputNames = []; - const wrongInputCallInputNames = []; + + const expectedStaticValueInputNames = []; + const expectedStaticDependencyInputNames = []; + + const validateFailedInputNames = []; + const validateFailedErrors = []; for (const [name, value] of Object.entries(inputOptions)) { if (misplacedInputNames.includes(name)) { @@ -559,6 +564,37 @@ export function templateCompositeFrom(description) { wrongTypeInputNames.push(name); continue; } + + const descriptionShape = getInputTokenShape(description.inputs[name]); + const descriptionValue = getInputTokenValue(description.inputs[name]); + + const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); + const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); + + if (descriptionShape === 'input.staticValue') { + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } + } + + if (descriptionShape === 'input.staticDependency') { + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } + } + + if (descriptionValue && 'validate' in descriptionValue) { + if (tokenShape === 'input.value') { + try { + descriptionValue.validate(tokenValue); + } catch (error) { + validateFailedInputNames.push(name); + validateFailedErrors.push(error); + } + } + } } if (!empty(misplacedInputNames)) { @@ -569,6 +605,23 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); } + if (!empty(expectedStaticDependencyInputNames)) { + inputOptionsAggregate.push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + } + + if (!empty(expectedStaticValueInputNames)) { + inputOptionsAggregate.push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + } + + for (const {name, validationError} of stitchArrays({ + name: validateFailedInputNames, + validationError: validateFailedErrors, + })) { + const error = new Error(`${name}: Validation failed for static value`); + error.cause = validationError; + inputOptionsAggregate.push(error); + } + for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); -- cgit 1.3.0-6-gf8a5 From 424cb392d3a4fc2f7b47b68de30378fc5abf1321 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 24 Sep 2023 15:40:20 -0300 Subject: content: generateAlbumStyleRules: provide album & track directory --- .../dependencies/generateAlbumCommentaryPage.js | 2 +- .../dependencies/generateAlbumGalleryPage.js | 2 +- src/content/dependencies/generateAlbumInfoPage.js | 2 +- .../dependencies/generateAlbumStyleRules.js | 72 +++++++++++++--------- src/content/dependencies/generateTrackInfoPage.js | 2 +- 5 files changed, 46 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index de619251..5979ed3f 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -21,7 +21,7 @@ export default { relation('generatePageLayout'); relations.albumStyleRules = - relation('generateAlbumStyleRules', album); + relation('generateAlbumStyleRules', album, null); relations.albumLink = relation('linkAlbum', album); diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index 68b56bd9..b98abc46 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -51,7 +51,7 @@ export default { relation('generatePageLayout'); relations.albumStyleRules = - relation('generateAlbumStyleRules', album); + relation('generateAlbumStyleRules', album, null); relations.albumLink = relation('linkAlbum', album); diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index ce17ab21..5c4344b0 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -37,7 +37,7 @@ export default { relation('generatePageLayout'); relations.albumStyleRules = - relation('generateAlbumStyleRules', album); + relation('generateAlbumStyleRules', album, null); relations.socialEmbed = relation('generateAlbumSocialEmbed', album); diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js index 1acaea17..9269ae83 100644 --- a/src/content/dependencies/generateAlbumStyleRules.js +++ b/src/content/dependencies/generateAlbumStyleRules.js @@ -3,14 +3,13 @@ import {empty} from '#sugar'; export default { extraDependencies: ['to'], - data(album) { + data(album, track) { const data = {}; data.hasWallpaper = !empty(album.wallpaperArtistContribs); data.hasBanner = !empty(album.bannerArtistContribs); if (data.hasWallpaper) { - data.hasWallpaperStyle = !!album.wallpaperStyle; data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension]; data.wallpaperStyle = album.wallpaperStyle; } @@ -20,40 +19,53 @@ export default { data.bannerStyle = album.bannerStyle; } + data.albumDirectory = album.directory; + + if (track) { + data.trackDirectory = track.directory; + } + return data; }, generate(data, {to}) { - const wallpaperPart = - (data.hasWallpaper - ? [ - `body::before {`, - ` background-image: url("${to(...data.wallpaperPath)}");`, - ...(data.hasWallpaperStyle - ? data.wallpaperStyle - .split('\n') - .map(line => ` ${line}`) - : []), - `}`, - ] - : []); + const indent = parts => + (parts ?? []) + .filter(Boolean) + .join('\n') + .split('\n') + .map(line => ' '.repeat(4) + line) + .join('\n'); - const bannerPart = - (data.hasBannerStyle - ? [ - `#banner img {`, - ...data.bannerStyle - .split('\n') - .map(line => ` ${line}`), - `}`, - ] + const rule = (selector, parts) => + (!empty(parts.filter(Boolean)) + ? [`${selector} {`, indent(parts), `}`] : []); - return [ - ...wallpaperPart, - ...bannerPart, - ] - .filter(Boolean) - .join('\n'); + const wallpaperRule = + data.hasWallpaper && + rule(`body::before`, [ + `background-image: url("${to(...data.wallpaperPath)}");`, + data.wallpaperStyle, + ]); + + 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 ( + [...wallpaperRule, ...bannerRule, ...dataRule] + .filter(Boolean) + .join('\n')); }, }; diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 334c5422..c20c0d08 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -44,7 +44,7 @@ export default { relation('generatePageLayout'); relations.albumStyleRules = - relation('generateAlbumStyleRules', track.album); + relation('generateAlbumStyleRules', track.album, track); relations.socialEmbed = relation('generateTrackSocialEmbed', track); -- cgit 1.3.0-6-gf8a5 From 84686df07e1029bcad91b9fda47b6f3bd280ee56 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 6 Jun 2023 20:07:03 -0300 Subject: content: generateCoverArtwork: mode commentary --- src/content/dependencies/generateCoverArtwork.js | 14 +++++++++++++- src/static/site4.css | 11 +++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index 4060c6b0..385385e3 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -32,7 +32,7 @@ export default { }, mode: { - validate: v => v.is('primary', 'thumbnail'), + validate: v => v.is('primary', 'thumbnail', 'commentary'), default: 'primary', }, }, @@ -73,6 +73,18 @@ export default { square: true, }); + case 'commentary': + return relations.image + .slots({ + path: slots.path, + alt: slots.alt, + thumb: 'medium', + class: 'commentary-art', + reveal: true, + link: true, + square: true, + }); + default: return html.blank(); } diff --git a/src/static/site4.css b/src/static/site4.css index ab8976bc..eb8d5520 100644 --- a/src/static/site4.css +++ b/src/static/site4.css @@ -533,6 +533,13 @@ p .current { margin-top: 5px; } +.commentary-art { + float: right; + width: 30%; + max-width: 250px; + margin: 15px 0 10px 20px; +} + .js-hide, .js-show-once-data, .js-hide-once-data { @@ -1250,6 +1257,10 @@ html[data-url-key="localized.home"] .carousel-container { animation-delay: 125ms; } +h3.content-heading { + clear: both; +} + /* This animation's name is referenced in JavaScript */ @keyframes highlight-hash-link { 0% { -- cgit 1.3.0-6-gf8a5 From 1c3d50b059b3848f685ff6655242f5cb6815ddd6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 6 Jun 2023 20:17:22 -0300 Subject: content: generateAlbumSidebarTrackSection: be mildly more versatile --- .../generateAlbumSidebarTrackSection.js | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index d71b0bdb..e8c0377d 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -33,10 +33,19 @@ export default { } } + data.trackDirectories = + trackSection.tracks + .map(track => track.directory); + return data; }, - generate(data, relations, {getColors, html, language}) { + slots: { + anchor: {type: 'boolean'}, + open: {type: 'boolean'}, + }, + + generate(slots, data, relations, {getColors, html, language}) { const sectionName = html.tag('span', {class: 'group-name'}, (data.isDefaultTrackSection @@ -59,7 +68,13 @@ export default { 'current', }, language.$('albumSidebar.trackList.item', { - track: trackLink, + track: + (slots.anchor + ? trackLink.slots({ + anchor: true, + hash: data.trackDirectories[index], + }) + : trackLink), }))); return html.tag('details', @@ -67,6 +82,11 @@ export default { class: data.includesCurrentTrack && 'current', open: ( + // Allow forcing open via a template slot. + // This isn't exactly janky, but the rest of this function + // kind of is when you contextualize it in a template... + slots.open || + // Leave sidebar track sections collapsed on album info page, // since there's already a view of the full track listing // in the main content area. -- cgit 1.3.0-6-gf8a5 From 1b81e54834d5f447b5b6e52eb7dfb875673a44f1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 6 Jun 2023 20:17:35 -0300 Subject: content: generateAlbumCommentaryPage: add sidebar, artworks --- .../dependencies/generateAlbumCommentaryPage.js | 41 +++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 5979ed3f..85a2108c 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -2,10 +2,13 @@ import {stitchArrays} from '#sugar'; export default { contentDependencies: [ + 'generateAlbumCoverArtwork', 'generateAlbumNavAccent', + 'generateAlbumSidebarTrackSection', 'generateAlbumStyleRules', 'generateColorStyleVariables', 'generateContentHeading', + 'generateTrackCoverArtwork', 'generatePageLayout', 'linkAlbum', 'linkTrack', @@ -30,6 +33,9 @@ export default { relation('generateAlbumNavAccent', album, null); if (album.commentary) { + relations.albumCommentaryCover = + relation('generateAlbumCoverArtwork', album); + relations.albumCommentaryContent = relation('transformContent', album.commentary); } @@ -46,6 +52,13 @@ export default { tracksWithCommentary .map(track => relation('linkTrack', track)); + relations.trackCommentaryCovers = + tracksWithCommentary + .map(track => + (track.hasUniqueCoverArt + ? relation('generateTrackCoverArtwork', track) + : null)); + relations.trackCommentaryContent = tracksWithCommentary .map(track => relation('transformContent', track.commentary)); @@ -57,6 +70,13 @@ export default { ? null : relation('generateColorStyleVariables'))); + relations.sidebarAlbumLink = + relation('linkAlbum', album); + + relations.sidebarTrackSections = + album.trackSections.map(trackSection => + relation('generateAlbumSidebarTrackSection', album, null, trackSection)); + return relations; }, @@ -129,6 +149,11 @@ export default { {class: ['content-heading']}, language.$('albumCommentaryPage.entry.title.albumCommentary')), + relations.albumCommentaryCover + ?.slots({ + displayMode: 'commentary', + }), + html.tag('blockquote', relations.albumCommentaryContent), ], @@ -137,15 +162,19 @@ export default { heading: relations.trackCommentaryHeadings, link: relations.trackCommentaryLinks, directory: data.trackCommentaryDirectories, + cover: relations.trackCommentaryCovers, content: relations.trackCommentaryContent, colorVariables: relations.trackCommentaryColorVariables, color: data.trackCommentaryColors, - }).map(({heading, link, directory, content, colorVariables, color}) => [ + }).map(({heading, link, directory, cover, content, colorVariables, color}) => [ heading.slots({ tag: 'h3', id: directory, title: link, }), + + cover?.slots({mode: 'commentary'}), + html.tag('blockquote', (color ? {style: colorVariables.slot('color', color).content} @@ -170,6 +199,16 @@ export default { }), }, ], + + leftSidebarStickyMode: 'column', + leftSidebarContent: [ + html.tag('h1', relations.sidebarAlbumLink), + relations.sidebarTrackSections.map(section => + section.slots({ + anchor: true, + open: true, + })), + ], }); }, }; -- cgit 1.3.0-6-gf8a5 From 528d909f4eeec9d7f6b54031752a94a6c9dca5ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 24 Sep 2023 10:29:09 -0300 Subject: content: generateAlbum{CommentaryPage,SidebarTrackSection}: merge fixes --- src/content/dependencies/generateAlbumCommentaryPage.js | 4 +--- src/content/dependencies/generateAlbumSidebarTrackSection.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 85a2108c..5c057b80 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -150,9 +150,7 @@ export default { language.$('albumCommentaryPage.entry.title.albumCommentary')), relations.albumCommentaryCover - ?.slots({ - displayMode: 'commentary', - }), + ?.slots({mode: 'commentary'}), html.tag('blockquote', relations.albumCommentaryContent), diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index e8c0377d..00e9b621 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -45,7 +45,7 @@ export default { open: {type: 'boolean'}, }, - generate(slots, data, relations, {getColors, html, language}) { + generate(data, relations, slots, {getColors, html, language}) { const sectionName = html.tag('span', {class: 'group-name'}, (data.isDefaultTrackSection -- cgit 1.3.0-6-gf8a5 From b695bf0c8d0c904ef5b26c76361b804a7ebdc335 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 24 Sep 2023 10:29:25 -0300 Subject: content: generateCoverArtwork: lazy commentary covers --- src/content/dependencies/generateCoverArtwork.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index 385385e3..aeba97de 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -83,6 +83,7 @@ export default { reveal: true, link: true, square: true, + lazy: true, }); default: -- cgit 1.3.0-6-gf8a5 From 8b1d5053f71959498c7327493db9f64b94f8de30 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 24 Sep 2023 11:00:57 -0300 Subject: css: misc. adjustments for sticky column sidebar --- src/static/site4.css | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'src') diff --git a/src/static/site4.css b/src/static/site4.css index eb8d5520..0e6166b4 100644 --- a/src/static/site4.css +++ b/src/static/site4.css @@ -1449,6 +1449,30 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r align-self: flex-start; } +.sidebar-column.sidebar.sticky-column { + max-height: calc(100vh - 20px); + overflow-y: scroll; + align-self: start; + padding-bottom: 0; + box-sizing: border-box; + flex-basis: 275px; + padding-top: 0; +} + +.sidebar-column.sidebar.sticky-column > h1 { + position: sticky; + top: 0; + margin: 0 calc(-1 * var(--content-padding)); + margin-bottom: 10px; + + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + padding: 10px 5px; + + background: var(--bg-black-color); + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); +} + /* Image overlay */ #image-overlay-container { -- cgit 1.3.0-6-gf8a5 From fbabffe43bfaacab4fdb1ae508525b6c5c41dbbd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 24 Sep 2023 15:41:20 -0300 Subject: client: rework (most) steps to fail gracefully --- src/static/client2.js | 753 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 502 insertions(+), 251 deletions(-) (limited to 'src') diff --git a/src/static/client2.js b/src/static/client2.js index d9afcb03..96b422a8 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -6,13 +6,28 @@ // ephemeral nature. import {getColors} from '../util/colors.js'; -import {getArtistNumContributions} from '../util/wiki-data.js'; +import {empty, stitchArrays} from '../util/sugar.js'; + +import { + filterMultipleArrays, + getArtistNumContributions, +} from '../util/wiki-data.js'; let albumData, artistData; let officialAlbumData, fandomAlbumData, beyondAlbumData; let ready = false; +const clientInfo = window.hsmusicClientInfo = Object.create(null); + +const clientSteps = { + getPageReferences: [], + addInternalListeners: [], + mutatePageContent: [], + initializeState: [], + addPageListeners: [], +}; + // Localiz8tion nonsense ---------------------------------- const language = document.documentElement.getAttribute('lang'); @@ -86,113 +101,148 @@ function fetchData(type, directory) { // JS-based links ----------------------------------------- -for (const a of document.body.querySelectorAll('[data-random]')) { - a.addEventListener('click', (evt) => { - if (!ready) { - evt.preventDefault(); - return; - } +const scriptedLinkInfo = clientInfo.scriptedLinkInfo = { + randomLinks: null, + revealLinks: null, - const tracks = albumData => - albumData - .map(album => album.tracks) - .reduce((acc, tracks) => acc.concat(tracks), []); + nextLink: null, + previousLink: null, + randomLink: null, +}; - setTimeout(() => { - a.href = rebase('js-disabled'); - }); +function getScriptedLinkReferences() { + scriptedLinkInfo.randomLinks = + document.querySelectorAll('[data-random]'); - switch (a.dataset.random) { - case 'album': - a.href = openAlbum(pick(albumData).directory); - break; - - case 'album-in-official': - a.href = openAlbum(pick(officialAlbumData).directory); - break; - - case 'album-in-fandom': - a.href = openAlbum(pick(fandomAlbumData).directory); - break; - - case 'album-in-beyond': - a.href = openAlbum(pick(beyondAlbumData).directory); - break; - - case 'track': - a.href = openTrack(getRefDirectory(pick(tracks(albumData)))); - break; - - case 'track-in-album': - a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks))); - break; - - case 'track-in-official': - a.href = openTrack(getRefDirectory(pick(tracks(officialAlbumData)))); - break; - - case 'track-in-fandom': - a.href = openTrack(getRefDirectory(pick(tracks(fandomAlbumData)))); - break; - - case 'track-in-beyond': - a.href = openTrack(getRefDirectory(pick(tracks(beyondAlbumData)))); - break; - - case 'artist': - a.href = openArtist(pick(artistData).directory); - break; - - case 'artist-more-than-one-contrib': - a.href = - openArtist( - pick(artistData.filter((artist) => getArtistNumContributions(artist) > 1)) - .directory); - break; - } - }); + scriptedLinkInfo.revealLinks = + document.getElementsByClassName('reveal'); + + scriptedLinkInfo.nextNavLink = + document.getElementById('next-button'); + + scriptedLinkInfo.previousNavLink = + document.getElementById('previous-button'); + + scriptedLinkInfo.randomNavLink = + document.getElementById('random-button'); } -const next = document.getElementById('next-button'); -const previous = document.getElementById('previous-button'); -const random = document.getElementById('random-button'); +function addRandomLinkListeners() { + for (const a of scriptedLinkInfo.randomLinks ?? []) { + a.addEventListener('click', evt => { + if (!ready) { + evt.preventDefault(); + return; + } -const prependTitle = (el, prepend) => { - const existing = el.getAttribute('title'); - if (existing) { - el.setAttribute('title', prepend + ' ' + existing); - } else { - el.setAttribute('title', prepend); - } -}; + const tracks = albumData => + albumData + .map(album => album.tracks) + .reduce((acc, tracks) => acc.concat(tracks), []); -if (next) prependTitle(next, '(Shift+N)'); -if (previous) prependTitle(previous, '(Shift+P)'); -if (random) prependTitle(random, '(Shift+R)'); - -document.addEventListener('keypress', (event) => { - if (event.shiftKey) { - if (event.charCode === 'N'.charCodeAt(0)) { - if (next) next.click(); - } else if (event.charCode === 'P'.charCodeAt(0)) { - if (previous) previous.click(); - } else if (event.charCode === 'R'.charCodeAt(0)) { - if (random && ready) random.click(); - } + setTimeout(() => { + a.href = rebase('js-disabled'); + }); + + switch (a.dataset.random) { + case 'album': + a.href = openAlbum(pick(albumData).directory); + break; + + case 'album-in-official': + a.href = openAlbum(pick(officialAlbumData).directory); + break; + + case 'album-in-fandom': + a.href = openAlbum(pick(fandomAlbumData).directory); + break; + + case 'album-in-beyond': + a.href = openAlbum(pick(beyondAlbumData).directory); + break; + + case 'track': + a.href = openTrack(getRefDirectory(pick(tracks(albumData)))); + break; + + case 'track-in-album': + a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks))); + break; + + case 'track-in-official': + a.href = openTrack(getRefDirectory(pick(tracks(officialAlbumData)))); + break; + + case 'track-in-fandom': + a.href = openTrack(getRefDirectory(pick(tracks(fandomAlbumData)))); + break; + + case 'track-in-beyond': + a.href = openTrack(getRefDirectory(pick(tracks(beyondAlbumData)))); + break; + + case 'artist': + a.href = openArtist(pick(artistData).directory); + break; + + case 'artist-more-than-one-contrib': + a.href = + openArtist( + pick(artistData.filter((artist) => getArtistNumContributions(artist) > 1)) + .directory); + break; + } + }); } -}); +} + +function mutateNavigationLinkContent() { + const prependTitle = (el, prepend) => + el?.setAttribute('title', + (el.hasAttribute('title') + ? prepend + ' ' + el.getAttribute('title') + : prepend)); -for (const reveal of document.querySelectorAll('.reveal')) { - reveal.addEventListener('click', (event) => { - if (!reveal.classList.contains('revealed')) { - reveal.classList.add('revealed'); - event.preventDefault(); - event.stopPropagation(); - reveal.dispatchEvent(new CustomEvent('hsmusic-reveal')); + prependTitle(scriptedLinkInfo.nextNavLink, '(Shift+N)'); + prependTitle(scriptedLinkInfo.previousNavLink, '(Shift+P)'); + prependTitle(scriptedLinkInfo.randomNavLink, '(Shift+R)'); +} + +function addNavigationKeyPressListeners() { + document.addEventListener('keypress', (event) => { + if (event.shiftKey) { + if (event.charCode === 'N'.charCodeAt(0)) { + scriptedLinkInfo.nextNavLink?.click(); + } else if (event.charCode === 'P'.charCodeAt(0)) { + scriptedLinkInfo.previousNavLink?.click(); + } else if (event.charCode === 'R'.charCodeAt(0)) { + if (ready) { + scriptedLinkInfo.randomNavLink?.click(); + } + } } }); } +function addRevealLinkClickListeners() { + for (const reveal of scriptedLinkInfo.revealLinks ?? []) { + reveal.addEventListener('click', (event) => { + if (!reveal.classList.contains('revealed')) { + reveal.classList.add('revealed'); + event.preventDefault(); + event.stopPropagation(); + reveal.dispatchEvent(new CustomEvent('hsmusic-reveal')); + } + }); + } +} + +clientSteps.getPageReferences.push(getScriptedLinkReferences); +clientSteps.addPageListeners.push(addRandomLinkListeners); +clientSteps.addPageListeners.push(addNavigationKeyPressListeners); +clientSteps.addPageListeners.push(addRevealLinkClickListeners); +clientSteps.mutatePageContent.push(mutateNavigationLinkContent); + const elements1 = document.getElementsByClassName('js-hide-once-data'); const elements2 = document.getElementsByClassName('js-show-once-data'); @@ -454,205 +504,393 @@ if (localStorage.tryInfoCards) { // Custom hash links -------------------------------------- -function addHashLinkHandlers() { +const hashLinkInfo = clientInfo.hashLinkInfo = { + links: null, + hrefs: null, + targets: null, + + state: { + highlightedTarget: null, + scrollingAfterClick: false, + concludeScrollingStateInterval: null, + }, + + event: { + whenHashLinkClicked: [], + }, +}; + +function getHashLinkReferences() { + const info = hashLinkInfo; + + info.links = + Array.from(document.querySelectorAll('a[href^="#"]:not([href="#"])')); + + info.hrefs = + info.links + .map(link => link.getAttribute('href')); + + info.targets = + info.hrefs + .map(href => document.getElementById(href.slice(1))); + + filterMultipleArrays( + info.links, + info.hrefs, + info.targets, + (_link, _href, target) => target); +} + +function processScrollingAfterHashLinkClicked() { + const {state} = hashLinkInfo; + + if (state.concludeScrollingStateInterval) return; + + let lastScroll = window.scrollY; + state.scrollingAfterClick = true; + state.concludeScrollingStateInterval = setInterval(() => { + if (Math.abs(window.scrollY - lastScroll) < 10) { + clearInterval(state.concludeScrollingStateInterval); + state.scrollingAfterClick = false; + state.concludeScrollingStateInterval = null; + } else { + lastScroll = window.scrollY; + } + }, 200); +} + +function addHashLinkListeners() { // Instead of defining a scroll offset (to account for the sticky heading) // in JavaScript, we interface with the CSS property 'scroll-margin-top'. // This lets the scroll offset be consolidated where it makes sense, and // sets an appropriate offset when (re)loading a page with hash for free! - let wasHighlighted; + const info = hashLinkInfo; + const {state, event} = info; - for (const a of document.links) { - const href = a.getAttribute('href'); - if (!href || !href.startsWith('#')) { - continue; - } + for (const {hashLink, href, target} of stitchArrays({ + hashLink: info.links, + href: info.hrefs, + target: info.targets, + })) { + hashLink.addEventListener('click', evt => { + if (evt.metaKey || evt.shiftKey || evt.ctrlKey || evt.altKey) { + return; + } - a.addEventListener('click', handleHashLinkClicked); - } + // Hide skipper box right away, so the layout is updated on time for the + // math operations coming up next. + const skipper = document.getElementById('skippers'); + skipper.style.display = 'none'; + setTimeout(() => skipper.style.display = ''); - function handleHashLinkClicked(evt) { - if (evt.metaKey || evt.shiftKey || evt.ctrlKey || evt.altKey) { - return; - } + const box = target.getBoundingClientRect(); + const style = window.getComputedStyle(target); - const href = evt.target.getAttribute('href'); - const id = href.slice(1); - const linked = document.getElementById(id); + const scrollY = + window.scrollY + + box.top + - style['scroll-margin-top'].replace('px', ''); - if (!linked) { - return; - } - - // Hide skipper box right away, so the layout is updated on time for the - // math operations coming up next. - const skipper = document.getElementById('skippers'); - skipper.style.display = 'none'; - setTimeout(() => skipper.style.display = ''); + evt.preventDefault(); + history.pushState({}, '', href); + window.scrollTo({top: scrollY, behavior: 'smooth'}); + target.focus({preventScroll: true}); - const box = linked.getBoundingClientRect(); - const style = window.getComputedStyle(linked); + const maxScroll = + document.body.scrollHeight + - window.innerHeight; - const scrollY = - window.scrollY - + box.top - - style['scroll-margin-top'].replace('px', ''); + if (scrollY > maxScroll && target.classList.contains('content-heading')) { + if (state.highlightedTarget) { + state.highlightedTarget.classList.remove('highlight-hash-link'); + } - evt.preventDefault(); - history.pushState({}, '', href); - window.scrollTo({top: scrollY, behavior: 'smooth'}); - linked.focus({preventScroll: true}); + target.classList.add('highlight-hash-link'); + state.highlightedTarget = target; + } - const maxScroll = - document.body.scrollHeight - - window.innerHeight; + processScrollingAfterHashLinkClicked(); - if (scrollY > maxScroll && linked.classList.contains('content-heading')) { - if (wasHighlighted) { - wasHighlighted.classList.remove('highlight-hash-link'); + for (const handler of event.whenHashLinkClicked) { + handler({ + link: hashLink, + }); } + }); + } - wasHighlighted = linked; - linked.classList.add('highlight-hash-link'); - linked.addEventListener('animationend', function handle(evt) { - if (evt.animationName === 'highlight-hash-link') { - linked.removeEventListener('animationend', handle); - linked.classList.remove('highlight-hash-link'); - wasHighlighted = null; - } - }); - } + for (const target of info.targets) { + target.addEventListener('animationend', evt => { + if (evt.animationName !== 'highlight-hash-link') return; + target.classList.remove('highlight-hash-link'); + if (target !== state.highlightedTarget) return; + state.highlightedTarget = null; + }); } } -addHashLinkHandlers(); +clientSteps.getPageReferences.push(getHashLinkReferences); +clientSteps.addPageListeners.push(addHashLinkListeners); // Sticky content heading --------------------------------- -const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky-heading-container')) - .map(stickyContainer => { - const {parentElement: contentContainer} = stickyContainer; - const stickySubheadingRow = stickyContainer.querySelector('.content-sticky-subheading-row'); - const stickySubheading = stickySubheadingRow.querySelector('h2'); - let stickyCoverContainer = stickyContainer.querySelector('.content-sticky-heading-cover-container'); - let stickyCover = stickyCoverContainer?.querySelector('.content-sticky-heading-cover'); - const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading')); - const contentCover = contentContainer.querySelector('#cover-art-container'); - - if (stickyCover?.querySelector('.image-text-area')) { - stickyCoverContainer.remove(); - stickyCoverContainer = null; - stickyCover = null; - } +const stickyHeadingInfo = clientInfo.stickyHeadingInfo = { + stickyContainers: null, - return { - contentContainer, - contentCover, - contentHeadings, - stickyContainer, - stickyCover, - stickyCoverContainer, - stickySubheading, - stickySubheadingRow, - state: { - displayedHeading: null, - }, - }; - }); + stickySubheadingRows: null, + stickySubheadings: null, -const topOfViewInside = (el, scroll = window.scrollY) => ( - scroll > el.offsetTop && - scroll < el.offsetTop + el.offsetHeight -); - -function prepareStickyHeadings() { - for (const { - contentCover, - stickyCover, - } of stickyHeadingInfo) { - const coverRevealImage = contentCover?.querySelector('.reveal'); - if (coverRevealImage) { - stickyCover.classList.add('content-sticky-heading-cover-needs-reveal'); - coverRevealImage.addEventListener('hsmusic-reveal', () => { - stickyCover.classList.remove('content-sticky-heading-cover-needs-reveal'); - }); + stickyCoverContainers: null, + stickyCoverTextAreas: null, + stickyCovers: null, + + contentContainers: null, + contentHeadings: null, + contentCovers: null, + contentCoversReveal: null, + + state: { + displayedHeading: null, + }, + + event: { + whenDisplayedHeadingChanges: [], + }, +}; + +function getStickyHeadingReferences() { + const info = stickyHeadingInfo; + + info.stickyContainers = + Array.from(document.getElementsByClassName('content-sticky-heading-container')); + + info.stickyCoverContainers = + info.stickyContainers + .map(el => el.querySelector('.content-sticky-heading-cover-container')); + + info.stickyCovers = + info.stickyCoverContainers + .map(el => el?.querySelector('.content-sticky-heading-cover')); + + info.stickyCoverTextAreas = + info.stickyCovers + .map(el => el?.querySelector('.image-text-area')); + + info.stickySubheadingRows = + info.stickyContainers + .map(el => el.querySelector('.content-sticky-subheading-row')); + + info.stickySubheadings = + info.stickySubheadingRows + .map(el => el.querySelector('h2')); + + info.contentContainers = + info.stickyContainers + .map(el => el.parentElement); + + info.contentCovers = + info.contentContainers + .map(el => el.querySelector('#cover-art-container')); + + info.contentCoversReveal = + info.contentCovers + .map(el => el ? !!el.querySelector('.reveal') : null); + + info.contentHeadings = + info.contentContainers + .map(el => Array.from(el.querySelectorAll('.content-heading'))); +} + +function removeTextPlaceholderStickyHeadingCovers() { + const info = stickyHeadingInfo; + + const hasTextArea = + info.stickyCoverTextAreas.map(el => !!el); + + const coverContainersWithTextArea = + info.stickyCoverContainers + .filter((_el, index) => hasTextArea[index]); + + for (const el of coverContainersWithTextArea) { + el.remove(); + } + + info.stickyCoverContainers = + info.stickyCoverContainers + .map((el, index) => hasTextArea[index] ? null : el); + + info.stickyCovers = + info.stickyCovers + .map((el, index) => hasTextArea[index] ? null : el); + + info.stickyCoverTextAreas = + info.stickyCoverTextAreas + .slice() + .fill(null); +} + +function addRevealClassToStickyHeadingCovers() { + const info = stickyHeadingInfo; + + const stickyCoversWhichReveal = + info.stickyCovers + .filter((_el, index) => info.contentCoversReveal[index]); + + for (const el of stickyCoversWhichReveal) { + el.classList.add('content-sticky-heading-cover-needs-reveal'); + } +} + +function addRevealListenersForStickyHeadingCovers() { + const info = stickyHeadingInfo; + + const stickyCovers = info.stickyCovers.slice(); + const contentCovers = info.contentCovers.slice(); + + filterMultipleArrays( + stickyCovers, + contentCovers, + (_stickyCover, _contentCover, index) => info.contentCoversReveal[index]); + + for (const {stickyCover, contentCover} of stitchArrays({ + stickyCover: stickyCovers, + contentCover: contentCovers, + })) { + // TODO: Janky - should use internal event instead of DOM event + contentCover.querySelector('.reveal').addEventListener('hsmusic-reveal', () => { + stickyCover.classList.remove('content-sticky-heading-cover-needs-reveal'); + }); + } +} + +function topOfViewInside(el, scroll = window.scrollY) { + return ( + scroll > el.offsetTop && + scroll < el.offsetTop + el.offsetHeight); +} + +function updateStickyCoverVisibility(index) { + const info = stickyHeadingInfo; + + const stickyCoverContainer = info.stickyCoverContainers[index]; + const contentCover = info.contentCovers[index]; + + if (contentCover && stickyCoverContainer) { + if (contentCover.getBoundingClientRect().bottom < 0) { + stickyCoverContainer.classList.add('visible'); + } else { + stickyCoverContainer.classList.remove('visible'); } } } -function updateStickyHeading() { - for (const { - contentContainer, - contentCover, - contentHeadings, - stickyContainer, - stickyCoverContainer, - stickySubheading, - stickySubheadingRow, - state, - } of stickyHeadingInfo) { - let closestHeading = null; +function getContentHeadingClosestToStickySubheading(index) { + const info = stickyHeadingInfo; - if (contentCover && stickyCoverContainer) { - if (contentCover.getBoundingClientRect().bottom < 0) { - stickyCoverContainer.classList.add('visible'); - } else { - stickyCoverContainer.classList.remove('visible'); - } + const contentContainer = info.contentContainers[index]; + + if (!topOfViewInside(contentContainer)) { + return null; + } + + const stickySubheading = info.stickySubheadings[index]; + + if (stickySubheading.childNodes.length === 0) { + // Supply a non-breaking space to ensure correct basic line height. + stickySubheading.appendChild(document.createTextNode('\xA0')); + } + + const stickyContainer = info.stickyContainers[index]; + const stickyRect = stickyContainer.getBoundingClientRect(); + + // TODO: Should this compute with the subheading row instead of h2? + const subheadingRect = stickySubheading.getBoundingClientRect(); + + const stickyBottom = stickyRect.bottom + subheadingRect.height; + + // Iterate from bottom to top of the content area. + const contentHeadings = info.contentHeadings[index]; + for (const heading of contentHeadings.slice().reverse()) { + const headingRect = heading.getBoundingClientRect(); + if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 20) { + return heading; } + } - if (topOfViewInside(contentContainer)) { - if (stickySubheading.childNodes.length === 0) { - // to ensure correct basic line height - stickySubheading.appendChild(document.createTextNode('\xA0')); - } + return null; +} - const stickyRect = stickyContainer.getBoundingClientRect(); - const subheadingRect = stickySubheading.getBoundingClientRect(); - const stickyBottom = stickyRect.bottom + subheadingRect.height; - - // This array is reversed so that we're starting from the bottom when - // iterating over it. - for (let i = contentHeadings.length - 1; i >= 0; i--) { - const heading = contentHeadings[i]; - const headingRect = heading.getBoundingClientRect(); - if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 20) { - closestHeading = heading; - break; +function updateStickySubheadingContent(index) { + const info = stickyHeadingInfo; + const {event, state} = info; + + const closestHeading = getContentHeadingClosestToStickySubheading(index); + + if (state.displayedHeading === closestHeading) return; + + const stickySubheadingRow = info.stickySubheadingRows[index]; + + if (closestHeading) { + const stickySubheading = info.stickySubheadings[index]; + + // Array.from needed to iterate over a live array with for..of + for (const child of Array.from(stickySubheading.childNodes)) { + child.remove(); + } + + for (const child of closestHeading.childNodes) { + if (child.tagName === 'A') { + for (const grandchild of child.childNodes) { + stickySubheading.appendChild(grandchild.cloneNode(true)); } + } else { + stickySubheading.appendChild(child.cloneNode(true)); } } - if (state.displayedHeading !== closestHeading) { - if (closestHeading) { - // Array.from needed to iterate over a live array with for..of - for (const child of Array.from(stickySubheading.childNodes)) { - child.remove(); - } + stickySubheadingRow.classList.add('visible'); + } else { + stickySubheadingRow.classList.remove('visible'); + } - for (const child of closestHeading.childNodes) { - if (child.tagName === 'A') { - for (const grandchild of child.childNodes) { - stickySubheading.appendChild(grandchild.cloneNode(true)); - } - } else { - stickySubheading.appendChild(child.cloneNode(true)); - } - } + const oldDisplayedHeading = state.displayedHeading; - stickySubheadingRow.classList.add('visible'); - } else { - stickySubheadingRow.classList.remove('visible'); - } + state.displayedHeading = closestHeading; - state.displayedHeading = closestHeading; - } + for (const handler of event.whenDisplayedHeadingChanges) { + handler(index, { + oldHeading: oldDisplayedHeading, + newHeading: closestHeading, + }); } } -document.addEventListener('scroll', updateStickyHeading); -prepareStickyHeadings(); -updateStickyHeading(); +function updateStickyHeadings(index) { + updateStickyCoverVisibility(index); + updateStickySubheadingContent(index); +} + +function initializeStateForStickyHeadings() { + for (let i = 0; i < stickyHeadingInfo.stickyContainers.length; i++) { + updateStickyHeadings(i); + } +} + +function addScrollListenerForStickyHeadings() { + document.addEventListener('scroll', () => { + for (let i = 0; i < stickyHeadingInfo.stickyContainers.length; i++) { + updateStickyHeadings(i); + } + }); +} + +clientSteps.getPageReferences.push(getStickyHeadingReferences); +clientSteps.mutatePageContent.push(removeTextPlaceholderStickyHeadingCovers); +clientSteps.mutatePageContent.push(addRevealClassToStickyHeadingCovers); +clientSteps.initializeState.push(initializeStateForStickyHeadings); +clientSteps.addPageListeners.push(addRevealListenersForStickyHeadingCovers); +clientSteps.addPageListeners.push(addScrollListenerForStickyHeadings); // Image overlay ------------------------------------------ @@ -970,3 +1208,16 @@ for (const info of groupContributionsTableInfo) { sortGroupContributionsTableBy(info, 'count'); }); } + +// Run setup steps ---------------------------------------- + +for (const [key, steps] of Object.entries(clientSteps)) { + for (const step of steps) { + try { + step(); + } catch (error) { + console.warn(`During ${key}, failed to run ${step.name}`); + console.debug(error); + } + } +} -- cgit 1.3.0-6-gf8a5 From 33f622ca94cdac2b7b6b1d3bbd57a96248e57035 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 24 Sep 2023 15:41:53 -0300 Subject: client: implement album commentary sidebar dynamics --- src/static/client2.js | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) (limited to 'src') diff --git a/src/static/client2.js b/src/static/client2.js index 96b422a8..4f4a7153 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -1209,6 +1209,217 @@ for (const info of groupContributionsTableInfo) { }); } +// Sticky commentary sidebar ------------------------------ + +const albumCommentarySidebarInfo = clientInfo.albumCommentarySidebarInfo = { + sidebar: null, + + sidebarTrackLinks: null, + sidebarTrackDirectories: null, + + sidebarTrackSections: null, + sidebarTrackSectionStartIndices: null, + + state: { + currentTrackSection: null, + currentTrackLink: null, + justChangedTrackSection: false, + }, +}; + +function getAlbumCommentarySidebarReferences() { + const info = albumCommentarySidebarInfo; + + info.sidebar = + document.getElementById('sidebar-left'); + + info.sidebarHeading = + info.sidebar.querySelector('h1'); + + info.sidebarTrackLinks = + Array.from(info.sidebar.querySelectorAll('li a')); + + info.sidebarTrackDirectories = + info.sidebarTrackLinks + .map(el => el.getAttribute('href').slice(1)); + + info.sidebarTrackSections = + Array.from(info.sidebar.getElementsByTagName('details')); + + info.sidebarTrackSectionStartIndices = + info.sidebarTrackSections + .map(details => details.querySelector('ol, ul')) + .reduce( + (accumulator, _list, index, array) => + (empty(accumulator) + ? [0] + : [ + ...accumulator, + (accumulator[accumulator.length - 1] + + array[index - 1].querySelectorAll('li a').length), + ]), + []); +} + +function scrollAlbumCommentarySidebar() { + const info = albumCommentarySidebarInfo; + const {state} = info; + const {currentTrackLink, currentTrackSection} = state; + + if (!currentTrackLink) { + return; + } + + const {sidebar, sidebarHeading} = info; + + const scrollTop = sidebar.scrollTop; + + const headingRect = sidebarHeading.getBoundingClientRect(); + const sidebarRect = sidebar.getBoundingClientRect(); + + const stickyPadding = headingRect.height; + const sidebarViewportHeight = sidebarRect.height - stickyPadding; + + const linkRect = currentTrackLink.getBoundingClientRect(); + const sectionRect = currentTrackSection.getBoundingClientRect(); + + const sectionTopEdge = + sectionRect.top - (sidebarRect.top - scrollTop); + + const sectionHeight = + sectionRect.height; + + const sectionScrollTop = + sectionTopEdge - stickyPadding - 10; + + const linkTopEdge = + linkRect.top - (sidebarRect.top - scrollTop); + + const linkBottomEdge = + linkRect.bottom - (sidebarRect.top - scrollTop); + + const linkScrollTop = + linkTopEdge - stickyPadding - 5; + + const linkDistanceFromSection = + linkScrollTop - sectionTopEdge; + + const linkVisibleFromTopOfSection = + linkBottomEdge - sectionTopEdge > sidebarViewportHeight; + + const linkScrollBottom = + linkScrollTop - sidebarViewportHeight + linkRect.height + 20; + + const maxScrollInViewport = + scrollTop + stickyPadding + sidebarViewportHeight; + + const minScrollInViewport = + scrollTop + stickyPadding; + + if (linkBottomEdge > maxScrollInViewport) { + if (linkVisibleFromTopOfSection) { + sidebar.scrollTo({top: linkScrollBottom, behavior: 'smooth'}); + } else { + sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'}); + } + } else if (linkTopEdge < minScrollInViewport) { + if (linkVisibleFromTopOfSection) { + sidebar.scrollTo({top: linkScrollTop, behavior: 'smooth'}); + } else { + sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'}); + } + } else if (state.justChangedTrackSection) { + if (sectionHeight < sidebarViewportHeight) { + sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'}); + } + } +} + +function markDirectoryAsCurrentForAlbumCommentary(trackDirectory) { + const info = albumCommentarySidebarInfo; + const {state} = info; + + const trackIndex = + (trackDirectory + ? info.sidebarTrackDirectories + .indexOf(trackDirectory) + : -1); + + const sectionIndex = + (trackIndex >= 0 + ? info.sidebarTrackSectionStartIndices + .findIndex((start, index, array) => + (index === array.length - 1 + ? true + : trackIndex < array[index + 1])) + : -1); + + const sidebarTrackLink = + (trackIndex >= 0 + ? info.sidebarTrackLinks[trackIndex] + : null); + + const sidebarTrackSection = + (sectionIndex >= 0 + ? info.sidebarTrackSections[sectionIndex] + : null); + + state.currentTrackLink?.classList?.remove('current'); + state.currentTrackLink = sidebarTrackLink; + state.currentTrackLink?.classList?.add('current'); + + if (sidebarTrackSection !== state.currentTrackSection) { + if (sidebarTrackSection && !sidebarTrackSection.open) { + if (state.currentTrackSection) { + state.currentTrackSection.open = false; + } + + sidebarTrackSection.open = true; + } + + state.currentTrackSection?.classList?.remove('current'); + state.currentTrackSection = sidebarTrackSection; + state.currentTrackSection?.classList?.add('current'); + state.justChangedTrackSection = true; + } else { + state.justChangedTrackSection = false; + } +} + +function addAlbumCommentaryInternalListeners() { + const info = albumCommentarySidebarInfo; + + const mainContentIndex = + (stickyHeadingInfo.contentContainers ?? []) + .findIndex(({id}) => id === 'content'); + + if (mainContentIndex === -1) return; + + stickyHeadingInfo.event.whenDisplayedHeadingChanges.push((index, {newHeading}) => { + if (index !== mainContentIndex) return; + if (hashLinkInfo.state.scrollingAfterClick) return; + + const trackDirectory = + (newHeading + ? newHeading.id + : null); + + markDirectoryAsCurrentForAlbumCommentary(trackDirectory); + scrollAlbumCommentarySidebar(); + }); + + hashLinkInfo.event.whenHashLinkClicked.push(({link}) => { + const hash = link.getAttribute('href').slice(1); + if (!info.sidebarTrackDirectories.includes(hash)) return; + markDirectoryAsCurrentForAlbumCommentary(hash); + }); +} + +if (document.documentElement.dataset.urlKey === 'localized.albumCommentary') { + clientSteps.getPageReferences.push(getAlbumCommentarySidebarReferences); + clientSteps.addInternalListeners.push(addAlbumCommentaryInternalListeners); +} + // Run setup steps ---------------------------------------- for (const [key, steps] of Object.entries(clientSteps)) { -- cgit 1.3.0-6-gf8a5 From 219596b6d52443d1090c94e50244cf79d548a167 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 25 Sep 2023 08:48:19 -0300 Subject: data, test: exposeConstant, withResultOfAvailabilityCheck --- src/data/things/composite.js | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 27b345cd..1148687c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -721,7 +721,6 @@ export function templateCompositeFrom(description) { const finalInputs = {}; for (const [name, description_] of Object.entries(description.inputs)) { - // TODO: Validate inputOptions[name] against staticValue, staticDependency shapes const description = getInputTokenValue(description_); const tokenShape = getInputTokenShape(description_); -- cgit 1.3.0-6-gf8a5 From b5cfc2a793f22da60606a4dd7387fcf3d3163843 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 25 Sep 2023 14:23:23 -0300 Subject: data: misc. improvements for input validation & infrastructure --- src/data/things/composite.js | 254 ++++++++++++++++++++++++++++-------------- src/data/things/thing.js | 72 +++++++----- src/data/things/track.js | 17 +-- src/data/things/validators.js | 58 +++++++++- src/util/sugar.js | 7 +- 5 files changed, 279 insertions(+), 129 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1148687c..0f943ec3 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,7 +5,6 @@ import {TupleMap} from '#wiki-data'; import { is, - isArray, isString, isWholeNumber, validateArrayItems, @@ -18,6 +17,7 @@ import { openAggregate, stitchArrays, unique, + withAggregate, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -443,13 +443,53 @@ function getStaticInputMetadata(inputOptions) { return metadata; } -export function templateCompositeFrom(description) { - const compositeName = +function getCompositionName(description) { + return ( (description.annotation ? description.annotation - : `unnamed composite`); + : `unnamed composite`)); +} + +function validateInputValue(value, description) { + const tokenValue = getInputTokenValue(description); + + const {acceptsNull, defaultValue, type, validate} = tokenValue || {}; - const descriptionAggregate = openAggregate({message: `Errors in description for ${compositeName}`}); + if (value === null || value === undefined) { + if (acceptsNull || defaultValue === null) { + return true; + } else { + throw new TypeError( + (type + ? `Expected ${type}, got ${value}` + : `Expected value, got ${value}`)); + } + } + + if (type) { + // Note: null is already handled earlier in this function, so it won't + // cause any trouble here. + const typeofValue = + (typeof value === 'object' + ? Array.isArray(value) ? 'array' : 'object' + : typeof value); + + if (typeofValue !== type) { + throw new TypeError(`Expected ${type}, got ${typeofValue}`); + } + } + + if (validate) { + validate(value); + } + + return true; +} + +export function templateCompositeFrom(description) { + const compositionName = getCompositionName(description); + + const descriptionAggregate = openAggregate({message: `Errors in description for ${compositionName}`}); if ('steps' in description) { if (Array.isArray(description.steps)) { @@ -469,7 +509,7 @@ export function templateCompositeFrom(description) { break validateInputs; } - descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + descriptionAggregate.nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { const missingCallsToInput = []; const wrongCallsToInput = []; @@ -515,7 +555,7 @@ export function templateCompositeFrom(description) { throw new Error(`${value}: Expected "#" at start`); } }), - {message: `Errors in output descriptions for ${compositeName}`}); + {message: `Errors in output descriptions for ${compositionName}`}); } } @@ -527,7 +567,7 @@ export function templateCompositeFrom(description) { : []); const instantiate = (inputOptions = {}) => { - const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); + const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositionName}`}); const providedInputNames = Object.keys(inputOptions); @@ -543,7 +583,6 @@ export function templateCompositeFrom(description) { if (!inputDescription) return true; if ('defaultValue' in inputDescription) return false; if ('defaultDependency' in inputDescription) return false; - if (inputDescription.null === true) return false; return true; }); @@ -655,7 +694,7 @@ export function templateCompositeFrom(description) { symbol: templateCompositeFrom.symbol, outputs(providedOptions) { - const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositeName}`}); + const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositionName}`}); const misplacedOutputNames = []; const wrongTypeOutputNames = []; @@ -718,28 +757,27 @@ export function templateCompositeFrom(description) { } if ('inputs' in description) { - const finalInputs = {}; - - for (const [name, description_] of Object.entries(description.inputs)) { - const description = getInputTokenValue(description_); - const tokenShape = getInputTokenShape(description_); + const inputMapping = {}; + for (const [name, token] of Object.entries(description.inputs)) { + const tokenValue = getInputTokenValue(token); if (name in inputOptions) { if (typeof inputOptions[name] === 'string') { - finalInputs[name] = input.dependency(inputOptions[name]); + inputMapping[name] = input.dependency(inputOptions[name]); } else { - finalInputs[name] = inputOptions[name]; + inputMapping[name] = inputOptions[name]; } - } else if (description.defaultValue) { - finalInputs[name] = input.value(description.defaultValue); - } else if (description.defaultDependency) { - finalInputs[name] = input.dependency(description.defaultDependency); + } else if (tokenValue.defaultValue) { + inputMapping[name] = input.value(tokenValue.defaultValue); + } else if (tokenValue.defaultDependency) { + inputMapping[name] = input.dependency(tokenValue.defaultDependency); } else { - finalInputs[name] = input.value(null); + inputMapping[name] = input.value(null); } } - finalDescription.inputs = finalInputs; + finalDescription.inputMapping = inputMapping; + finalDescription.inputDescriptions = description.inputs; } if ('outputs' in description) { @@ -768,7 +806,7 @@ export function templateCompositeFrom(description) { const finalDescription = {...ownDescription}; - const aggregate = openAggregate({message: `Errors resolving ${compositeName}`}); + const aggregate = openAggregate({message: `Errors resolving ${compositionName}`}); const steps = ownDescription.steps(); @@ -804,6 +842,7 @@ export const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol' export function compositeFrom(description) { const {annotation} = description; + const compositionName = getCompositionName(description); const debug = fn => { if (compositeFrom.debug === true) { @@ -835,7 +874,7 @@ export function compositeFrom(description) { ? compositeFrom(step.toResolvedComposition()) : step)); - const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + const inputMetadata = getStaticInputMetadata(description.inputMapping ?? {}); function _mapDependenciesToOutputs(providedDependencies) { if (!description.outputs) { @@ -861,7 +900,7 @@ export function compositeFrom(description) { // nested inside, so input('name')-shaped tokens are going to be evaluated // in the context of the containing composition. const dependenciesFromInputs = - Object.values(description.inputs ?? {}) + Object.values(description.inputMapping ?? {}) .map(token => { const tokenShape = getInputTokenShape(token); const tokenValue = getInputTokenValue(token); @@ -884,10 +923,41 @@ export function compositeFrom(description) { .filter(dependency => isInputToken(dependency)) .some(token => getInputTokenShape(token) === 'input.updateValue'); + const inputNames = + Object.keys(description.inputMapping ?? {}); + + const inputSymbols = + inputNames.map(name => input(name)); + + const inputsMayBeDynamicValue = + stitchArrays({ + mappingToken: Object.values(description.inputMapping ?? {}), + descriptionToken: Object.values(description.inputDescriptions ?? {}), + }).map(({mappingToken, descriptionToken}) => { + if (getInputTokenShape(descriptionToken) === 'input.staticValue') return false; + if (getInputTokenShape(mappingToken) === 'input.value') return false; + return true; + }); + + const inputDescriptions = + Object.values(description.inputDescriptions ?? {}); + + /* + const inputsAcceptNull = + Object.values(description.inputDescriptions ?? {}) + .map(token => { + const tokenValue = getInputTokenValue(token); + if (!tokenValue) return false; + if ('acceptsNull' in tokenValue) return tokenValue.acceptsNull; + if ('defaultValue' in tokenValue) return tokenValue.defaultValue === null; + return false; + }); + */ + // Update descriptions passed as the value in an input.updateValue() token, // as provided as inputs for this composition. const inputUpdateDescriptions = - Object.values(description.inputs ?? {}) + Object.values(description.inputMapping ?? {}) .map(token => (getInputTokenShape(token) === 'input.updateValue' ? getInputTokenValue(token) @@ -903,7 +973,6 @@ export function compositeFrom(description) { (annotation ? ` (${annotation})` : ''), }); - // TODO: Check description.compose ?? true instead. const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); @@ -1141,30 +1210,44 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; const inputValues = - ('inputs' in description - ? Object.fromEntries(Object.entries(description.inputs) - .map(([name, token]) => { - const tokenShape = getInputTokenShape(token); - const tokenValue = getInputTokenValue(token); - switch (tokenShape) { - case 'input.dependency': - return [input(name), initialDependencies[tokenValue]]; - case 'input.value': - return [input(name), tokenValue]; - case 'input.updateValue': - if (!expectingTransform) { - throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); - } - return [input(name), valueSoFar]; - case 'input.myself': - return [input(name), initialDependencies['this']]; - case 'input': - return [input(name), initialDependencies[token]]; - default: - throw new TypeError(`Unexpected input shape ${tokenShape}`); - } - })) - : {}); + Object.values(description.inputMapping ?? {}) + .map(token => { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { + case 'input.dependency': + return initialDependencies[tokenValue]; + case 'input.value': + return tokenValue; + case 'input.updateValue': + if (!expectingTransform) + throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); + return valueSoFar; + case 'input.myself': + return initialDependencies['this']; + case 'input': + return initialDependencies[token]; + default: + throw new TypeError(`Unexpected input shape ${tokenShape}`); + } + }); + + withAggregate({message: `Errors in dynamic input values provided to ${compositionName}`}, ({push}) => { + for (const {dynamic, name, value, description} of stitchArrays({ + dynamic: inputsMayBeDynamicValue, + name: inputNames, + value: inputValues, + description: inputDescriptions, + })) { + if (!dynamic) continue; + try { + validateInputValue(value, description); + } catch (error) { + error.message = `${name}: ${error.message}`; + throw error; + } + } + }); if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); @@ -1220,10 +1303,15 @@ export function compositeFrom(description) { let continuationStorage; + const inputDictionary = + Object.fromEntries( + stitchArrays({symbol: inputSymbols, value: inputValues}) + .map(({symbol, value}) => [symbol, value])); + const filterableDependencies = { ...availableDependencies, ...inputMetadata, - ...inputValues, + ...inputDictionary, ... (expectingTransform ? {[input.updateValue()]: valueSoFar} @@ -1568,7 +1656,7 @@ export const exposeDependency = templateCompositeFrom({ compose: false, inputs: { - dependency: input.staticDependency(), + dependency: input.staticDependency({acceptsNull: true}), }, steps: () => [ @@ -1618,17 +1706,17 @@ export const exposeConstant = templateCompositeFrom({ // for values like zero and the empty string! // -const availabilityCheckModeInput = { +const inputAvailabilityCheckMode = () => input({ validate: is('null', 'empty', 'falsy'), defaultValue: 'null', -}; +}); export const withResultOfAvailabilityCheck = templateCompositeFrom({ annotation: `withResultOfAvailabilityCheck`, inputs: { - from: input(), - mode: input(availabilityCheckModeInput), + from: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), }, outputs: ['#availability'], @@ -1669,8 +1757,8 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ annotation: `exposeDependencyOrContinue`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), }, steps: () => [ @@ -1700,8 +1788,12 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { - mode: input(availabilityCheckModeInput), - validate: input({type: 'function', null: true}), + mode: inputAvailabilityCheckMode(), + + validate: input({ + type: 'function', + defaultValue: null, + }), }, update: ({ @@ -1725,9 +1817,9 @@ export const exitWithoutDependency = templateCompositeFrom({ annotation: `exitWithoutDependency`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), - value: input({null: true}), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + value: input({defaultValue: null}), }, steps: () => [ @@ -1755,7 +1847,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ annotation: `exitWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckModeInput), + mode: inputAvailabilityCheckMode(), value: input({defaultValue: null}), }, @@ -1763,6 +1855,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ exitWithoutDependency({ dependency: input.updateValue(), mode: input('mode'), + value: input('value'), }), ], }); @@ -1773,8 +1866,8 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ annotation: `raiseOutputWithoutDependency`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), output: input.staticValue({defaultValue: {}}), }, @@ -1807,7 +1900,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ annotation: `raiseOutputWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckModeInput), + mode: inputAvailabilityCheckMode(), output: input.staticValue({defaultValue: {}}), }, @@ -1841,7 +1934,7 @@ export const withPropertyFromObject = templateCompositeFrom({ annotation: `withPropertyFromObject`, inputs: { - object: input({type: 'object', null: true}), + object: input({type: 'object', acceptsNull: true}), property: input({type: 'string'}), }, @@ -1907,19 +2000,13 @@ export const withPropertiesFromObject = templateCompositeFrom({ annotation: `withPropertiesFromObject`, inputs: { - object: input({ - type: 'object', - null: true, - }), + object: input({type: 'object', acceptsNull: true}), properties: input({ validate: validateArrayItems(isString), }), - prefix: input.staticValue({ - type: 'string', - null: true, - }), + prefix: input.staticValue({type: 'string', defaultValue: null}), }, outputs: ({ @@ -2036,10 +2123,7 @@ export const withPropertiesFromList = templateCompositeFrom({ validate: validateArrayItems(isString), }), - prefix: input.staticValue({ - type: 'string', - null: true, - }), + prefix: input.staticValue({type: 'string', defaultValue: null}), }, outputs: ({ @@ -2109,7 +2193,7 @@ export const fillMissingListItems = templateCompositeFrom({ inputs: { list: input({type: 'array'}), - fill: input(), + fill: input({acceptsNull: true}), }, outputs: ({ @@ -2150,8 +2234,8 @@ export const excludeFromList = templateCompositeFrom({ inputs: { list: input(), - item: input({null: true}), - items: input({validate: isArray, null: true}), + item: input({defaultValue: null}), + items: input({type: 'array', defaultValue: null}), }, outputs: ({ diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ef547f74..290be59b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -25,8 +25,8 @@ import { import { isAdditionalFileList, isBoolean, - isCommentary, isColor, + isCommentary, isContributionList, isDate, isDimensions, @@ -41,12 +41,13 @@ import { validateInstanceOf, validateReference, validateReferenceList, + validateWikiData, } from '#validators'; import CacheableObject from './cacheable-object.js'; export default class Thing extends CacheableObject { - static referenceType = Symbol('Thing.referenceType'); + static referenceType = Symbol.for('Thing.referenceType'); static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); @@ -283,10 +284,8 @@ export const referenceList = templateCompositeFrom({ inputs: { class: input.staticValue(thingClassInput), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), - - // todo: validate - data: input(), }, update: ({ @@ -316,9 +315,7 @@ export const singleReference = templateCompositeFrom({ inputs: { class: input(thingClassInput), find: input({type: 'function'}), - - // todo: validate - data: input(), + data: inputWikiData({allowMixedTypes: false}), }, update: ({ @@ -347,7 +344,10 @@ export const contribsPresent = templateCompositeFrom({ compose: false, inputs: { - contribs: input({type: 'string'}), + contribs: input.staticDependency({ + validate: isContributionList, + acceptsNull: true, + }), }, steps: () => [ @@ -371,9 +371,7 @@ export const reverseReferenceList = templateCompositeFrom({ compose: false, inputs: { - // todo: validate - data: input(), - + data: inputWikiData({allowMixedTypes: false}), list: input({type: 'string'}), }, @@ -448,6 +446,21 @@ export const commentatorArtists = templateCompositeFrom({ // Compositional utilities +// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] +// value because classes aren't initialized by when templateCompositeFrom gets +// called (see: circular imports). So the reference types have to be hard-coded, +// which somewhat defeats the point of storing them on the class in the first +// place... +export function inputWikiData({ + referenceType = '', + allowMixedTypes = false, +} = {}) { + return input({ + validate: validateWikiData(referenceType), + acceptsNull: true, + }); +} + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist @@ -456,8 +469,10 @@ export const withResolvedContribs = templateCompositeFrom({ annotation: `withResolvedContribs`, inputs: { - // todo: validate - from: input(), + from: input({ + validate: isContributionList, + acceptsNull: true, + }), notFoundMode: input({ validate: is('exit', 'filter', 'null'), @@ -514,10 +529,12 @@ export const exitWithoutContribs = templateCompositeFrom({ annotation: `exitWithoutContribs`, inputs: { - // todo: validate - contribs: input(), + contribs: input({ + validate: isContributionList, + acceptsNull: true, + }), - value: input({null: true}), + value: input({defaultValue: null}), }, steps: () => [ @@ -553,12 +570,9 @@ export const withResolvedReference = templateCompositeFrom({ annotation: `withResolvedReference`, inputs: { - // todo: validate - ref: input(), - - // todo: validate - data: input(), + ref: input({type: 'string', acceptsNull: true}), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), notFoundMode: input({ @@ -618,12 +632,12 @@ export const withResolvedReferenceList = templateCompositeFrom({ annotation: `withResolvedReferenceList`, inputs: { - // todo: validate - list: input(), - - // todo: validate - data: input(), + list: input({ + validate: validateArrayItems(isString), + acceptsNull: true, + }), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), notFoundMode: input({ @@ -706,9 +720,7 @@ export const withReverseReferenceList = templateCompositeFrom({ annotation: `withReverseReferenceList`, inputs: { - // todo: validate - data: input(), - + data: inputWikiData({allowMixedTypes: false}), list: input({type: 'string'}), }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 3e0d95bf..c77bf889 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -18,12 +18,13 @@ import { } from '#composite'; import { + is, isBoolean, isColor, isContributionList, isDate, isFileExtension, - oneOf, + validateWikiData, } from '#validators'; import CacheableObject from './cacheable-object.js'; @@ -434,7 +435,7 @@ export const withAlbum = templateCompositeFrom({ inputs: { notFoundMode: input({ - validate: oneOf('exit', 'null'), + validate: is('exit', 'null'), defaultValue: 'null', }), }, @@ -488,7 +489,7 @@ export const withPropertyFromAlbum = templateCompositeFrom({ property: input.staticValue({type: 'string'}), notFoundMode: input({ - validate: oneOf('exit', 'null'), + validate: is('exit', 'null'), defaultValue: 'null', }), }, @@ -527,7 +528,7 @@ export const withContainingTrackSection = templateCompositeFrom({ inputs: { notFoundMode: input({ - validate: oneOf('exit', 'null'), + validate: is('exit', 'null'), defaultValue: 'null', }), }, @@ -589,8 +590,10 @@ export const withOriginalRelease = templateCompositeFrom({ inputs: { selfIfOriginal: input({type: 'boolean', defaultValue: false}), - // todo: validate - data: input({defaultDependency: 'trackData'}), + data: input({ + validate: validateWikiData({referenceType: 'track'}), + defaultDependency: 'trackData', + }), }, outputs: ['#originalRelease'], @@ -683,7 +686,7 @@ export const exitWithoutUniqueCoverArt = templateCompositeFrom({ annotation: `exitWithoutUniqueCoverArt`, inputs: { - value: input({null: true}), + value: input({defaultValue: null}), }, steps: () => [ diff --git a/src/data/things/validators.js b/src/data/things/validators.js index cd4c2b46..048f7ebb 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -1,7 +1,7 @@ import {inspect as nodeInspect} from 'node:util'; import {colors, ENABLE_COLOR} from '#cli'; -import {withAggregate} from '#sugar'; +import {empty, withAggregate} from '#sugar'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -404,6 +404,62 @@ export function validateReferenceList(type = '') { return validateArrayItems(validateReference(type)); } +export function validateWikiData({ + referenceType = '', + allowMixedTypes = false, +}) { + if (referenceType && allowMixedTypes) { + throw new TypeError(`Don't specify both referenceType and allowMixedTypes`); + } + + const isArrayOfObjects = validateArrayItems(isObject); + + return (array) => { + isArrayOfObjects(array); + + if (empty(array)) { + return true; + } + + const allRefTypes = + new Set(array.map(object => + object.constructor[Symbol.for('Thing.referenceType')])); + + if (allRefTypes.has(undefined)) { + if (allRefTypes.size === 1) { + throw new TypeError(`Expected array of wiki data objects, got array of other objects`); + } else { + throw new TypeError(`Expected array of wiki data objects, got mixed items`); + } + } + + if (allRefTypes.size > 1) { + if (allowMixedTypes) { + return true; + } + + const types = () => Array.from(allRefTypes).join(', '); + + if (referenceType) { + if (allRefTypes.has(referenceType)) { + allRefTypes.remove(referenceType); + throw new TypeError(`Expected array of only ${referenceType}, also got other types: ${types()}`) + } else { + throw new TypeError(`Expected array of only ${referenceType}, got other types: ${types()}`); + } + } + + throw new TypeError(`Expected array of unmixed reference types, got multiple: ${types()}`); + } + + if (referenceType && !allRefTypes.has(referenceType)) { + throw new TypeError(`Expected array of ${referenceType}, got array of ${allRefTypes[0]}`) + } + + return true; + }; +} + // Compositional utilities export function oneOf(...checks) { diff --git a/src/util/sugar.js b/src/util/sugar.js index 14fb250e..24d409fb 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -599,12 +599,7 @@ export function showAggregate(topError, { return [headerPart, causePart, aggregatePart].filter(Boolean).join('\n'); }; - const message = - (topError instanceof AggregateError - ? recursive(topError, {level: 0}) - : (showTraces - ? topError.stack - : topError.toString())); + const message = recursive(topError, {level: 0}); if (print) { console.error(message); -- cgit 1.3.0-6-gf8a5 From 8ed9240b56d61be97a5cc1da7c9fcb179bb58a93 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 26 Sep 2023 11:32:12 -0300 Subject: content: generateAlbumStyleRules: oops --- src/content/dependencies/generateAlbumStyleRules.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js index 9269ae83..c5acf374 100644 --- a/src/content/dependencies/generateAlbumStyleRules.js +++ b/src/content/dependencies/generateAlbumStyleRules.js @@ -64,8 +64,9 @@ export default { ]); return ( - [...wallpaperRule, ...bannerRule, ...dataRule] + [wallpaperRule, bannerRule, dataRule] .filter(Boolean) + .flat() .join('\n')); }, }; -- cgit 1.3.0-6-gf8a5 From 747df818115b4aefd2433990f2997fe4c80bc501 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 28 Sep 2023 11:35:11 -0300 Subject: data: refactor most openAggregate calls -> withAggregate --- src/data/things/composite.js | 327 ++++++++++++++++++++----------------------- 1 file changed, 153 insertions(+), 174 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 0f943ec3..26be4a67 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -489,77 +489,75 @@ function validateInputValue(value, description) { export function templateCompositeFrom(description) { const compositionName = getCompositionName(description); - const descriptionAggregate = openAggregate({message: `Errors in description for ${compositionName}`}); - - if ('steps' in description) { - if (Array.isArray(description.steps)) { - descriptionAggregate.push(new TypeError(`Wrap steps array in a function`)); - } else if (typeof description.steps !== 'function') { - descriptionAggregate.push(new TypeError(`Expected steps to be a function (returning an array)`)); + withAggregate({message: `Errors in description for ${compositionName}`}, ({map, nest, push}) => { + if ('steps' in description) { + if (Array.isArray(description.steps)) { + push(new TypeError(`Wrap steps array in a function`)); + } else if (typeof description.steps !== 'function') { + push(new TypeError(`Expected steps to be a function (returning an array)`)); + } } - } - validateInputs: - if ('inputs' in description) { - if (Array.isArray(description.inputs)) { - descriptionAggregate.push(new Error(`Expected inputs to be object, got array`)); - break validateInputs; - } else if (typeof description.inputs !== 'object') { - descriptionAggregate.push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); - break validateInputs; - } + validateInputs: + if ('inputs' in description) { + if (Array.isArray(description.inputs)) { + push(new Error(`Expected inputs to be object, got array`)); + break validateInputs; + } else if (typeof description.inputs !== 'object') { + push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + break validateInputs; + } - descriptionAggregate.nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { - const missingCallsToInput = []; - const wrongCallsToInput = []; + nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.inputs)) { - if (!isInputToken(value)) { - missingCallsToInput.push(name); - continue; + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); + continue; + } + + if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { + wrongCallsToInput.push(name); + } } - if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { - wrongCallsToInput.push(name); + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); } - } - for (const name of missingCallsToInput) { - push(new Error(`${name}: Missing call to input()`)); - } + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); + } + }); + } - for (const name of wrongCallsToInput) { - const shape = getInputTokenShape(description.inputs[name]); - push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); + validateOutputs: + if ('outputs' in description) { + if ( + !Array.isArray(description.outputs) && + typeof description.outputs !== 'function' + ) { + push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + break validateOutputs; } - }); - } - validateOutputs: - if ('outputs' in description) { - if ( - !Array.isArray(description.outputs) && - typeof description.outputs !== 'function' - ) { - descriptionAggregate.push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); - break validateOutputs; - } - - if (Array.isArray(description.outputs)) { - descriptionAggregate.map( - description.outputs, - decorateErrorWithIndex(value => { - if (typeof value !== 'string') { - throw new Error(`${value}: Expected string, got ${typeof value}`) - } else if (!value.startsWith('#')) { - throw new Error(`${value}: Expected "#" at start`); - } - }), - {message: `Errors in output descriptions for ${compositionName}`}); + if (Array.isArray(description.outputs)) { + map( + description.outputs, + decorateErrorWithIndex(value => { + if (typeof value !== 'string') { + throw new Error(`${value}: Expected string, got ${typeof value}`) + } else if (!value.startsWith('#')) { + throw new Error(`${value}: Expected "#" at start`); + } + }), + {message: `Errors in output descriptions for ${compositionName}`}); + } } - } - - descriptionAggregate.close(); + }); const expectedInputNames = (description.inputs @@ -567,106 +565,104 @@ export function templateCompositeFrom(description) { : []); const instantiate = (inputOptions = {}) => { - const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositionName}`}); - - const providedInputNames = Object.keys(inputOptions); - - const misplacedInputNames = - providedInputNames - .filter(name => !expectedInputNames.includes(name)); - - const missingInputNames = - expectedInputNames - .filter(name => !providedInputNames.includes(name)) - .filter(name => { - const inputDescription = description.inputs[name].value; - if (!inputDescription) return true; - if ('defaultValue' in inputDescription) return false; - if ('defaultDependency' in inputDescription) return false; - return true; - }); + withAggregate({message: `Errors in input options passed to ${compositionName}`}, ({push}) => { + const providedInputNames = Object.keys(inputOptions); + + const misplacedInputNames = + providedInputNames + .filter(name => !expectedInputNames.includes(name)); + + const missingInputNames = + expectedInputNames + .filter(name => !providedInputNames.includes(name)) + .filter(name => { + const inputDescription = description.inputs[name].value; + if (!inputDescription) return true; + if ('defaultValue' in inputDescription) return false; + if ('defaultDependency' in inputDescription) return false; + return true; + }); - const wrongTypeInputNames = []; + const wrongTypeInputNames = []; - const expectedStaticValueInputNames = []; - const expectedStaticDependencyInputNames = []; + const expectedStaticValueInputNames = []; + const expectedStaticDependencyInputNames = []; - const validateFailedInputNames = []; - const validateFailedErrors = []; + const validateFailedInputNames = []; + const validateFailedErrors = []; - for (const [name, value] of Object.entries(inputOptions)) { - if (misplacedInputNames.includes(name)) { - continue; - } + for (const [name, value] of Object.entries(inputOptions)) { + if (misplacedInputNames.includes(name)) { + continue; + } - if (typeof value !== 'string' && !isInputToken(value)) { - wrongTypeInputNames.push(name); - continue; - } + if (typeof value !== 'string' && !isInputToken(value)) { + wrongTypeInputNames.push(name); + continue; + } - const descriptionShape = getInputTokenShape(description.inputs[name]); - const descriptionValue = getInputTokenValue(description.inputs[name]); + const descriptionShape = getInputTokenShape(description.inputs[name]); + const descriptionValue = getInputTokenValue(description.inputs[name]); - const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); - const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); + const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); + const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); - if (descriptionShape === 'input.staticValue') { - if (tokenShape !== 'input.value') { - expectedStaticValueInputNames.push(name); - continue; + if (descriptionShape === 'input.staticValue') { + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } } - } - if (descriptionShape === 'input.staticDependency') { - if (typeof value !== 'string' && tokenShape !== 'input.dependency') { - expectedStaticDependencyInputNames.push(name); - continue; + if (descriptionShape === 'input.staticDependency') { + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } } - } - if (descriptionValue && 'validate' in descriptionValue) { - if (tokenShape === 'input.value') { - try { - descriptionValue.validate(tokenValue); - } catch (error) { - validateFailedInputNames.push(name); - validateFailedErrors.push(error); + if (descriptionValue && 'validate' in descriptionValue) { + if (tokenShape === 'input.value') { + try { + descriptionValue.validate(tokenValue); + } catch (error) { + validateFailedInputNames.push(name); + validateFailedErrors.push(error); + } } } } - } - - if (!empty(misplacedInputNames)) { - inputOptionsAggregate.push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); - } - if (!empty(missingInputNames)) { - inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); - } + if (!empty(misplacedInputNames)) { + push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); + } - if (!empty(expectedStaticDependencyInputNames)) { - inputOptionsAggregate.push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); - } + if (!empty(missingInputNames)) { + push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); + } - if (!empty(expectedStaticValueInputNames)) { - inputOptionsAggregate.push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); - } + if (!empty(expectedStaticDependencyInputNames)) { + push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + } - for (const {name, validationError} of stitchArrays({ - name: validateFailedInputNames, - validationError: validateFailedErrors, - })) { - const error = new Error(`${name}: Validation failed for static value`); - error.cause = validationError; - inputOptionsAggregate.push(error); - } + if (!empty(expectedStaticValueInputNames)) { + push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + } - for (const name of wrongTypeInputNames) { - const type = typeof inputOptions[name]; - inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); - } + for (const {name, validationError} of stitchArrays({ + name: validateFailedInputNames, + validationError: validateFailedErrors, + })) { + const error = new Error(`${name}: Validation failed for static value`); + error.cause = validationError; + push(error); + } - inputOptionsAggregate.close(); + for (const name of wrongTypeInputNames) { + const type = typeof inputOptions[name]; + push(new Error(`${name}: Expected string or input() call, got ${type}`)); + } + }); const inputMetadata = getStaticInputMetadata(inputOptions); @@ -694,48 +690,31 @@ export function templateCompositeFrom(description) { symbol: templateCompositeFrom.symbol, outputs(providedOptions) { - const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositionName}`}); - - const misplacedOutputNames = []; - const wrongTypeOutputNames = []; - // const notPrivateOutputNames = []; + withAggregate({message: `Errors in output options passed to ${compositionName}`}, ({push}) => { + const misplacedOutputNames = []; + const wrongTypeOutputNames = []; + + for (const [name, value] of Object.entries(providedOptions)) { + if (!expectedOutputNames.includes(name)) { + misplacedOutputNames.push(name); + continue; + } - for (const [name, value] of Object.entries(providedOptions)) { - if (!expectedOutputNames.includes(name)) { - misplacedOutputNames.push(name); - continue; + if (typeof value !== 'string') { + wrongTypeOutputNames.push(name); + continue; + } } - if (typeof value !== 'string') { - wrongTypeOutputNames.push(name); - continue; + if (!empty(misplacedOutputNames)) { + push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); } - /* - if (!value.startsWith('#')) { - notPrivateOutputNames.push(name); - continue; + for (const name of wrongTypeOutputNames) { + const type = typeof providedOptions[name]; + push(new Error(`${name}: Expected string, got ${type}`)); } - */ - } - - if (!empty(misplacedOutputNames)) { - outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); - } - - for (const name of wrongTypeOutputNames) { - const type = typeof providedOptions[name]; - outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); - } - - /* - for (const name of notPrivateOutputNames) { - const into = providedOptions[name]; - outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); - } - */ - - outputOptionsAggregate.close(); + }); Object.assign(outputOptions, providedOptions); return instantiatedTemplate; -- cgit 1.3.0-6-gf8a5 From 1e09cfe3fcaa3f6e020e50ce49ea77c254b04dfd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 28 Sep 2023 11:51:16 -0300 Subject: data: reuse validateInputValue for static inputs --- src/data/things/composite.js | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26be4a67..e58b6524 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -588,7 +588,6 @@ export function templateCompositeFrom(description) { const expectedStaticValueInputNames = []; const expectedStaticDependencyInputNames = []; - const validateFailedInputNames = []; const validateFailedErrors = []; for (const [name, value] of Object.entries(inputOptions)) { @@ -602,7 +601,6 @@ export function templateCompositeFrom(description) { } const descriptionShape = getInputTokenShape(description.inputs[name]); - const descriptionValue = getInputTokenValue(description.inputs[name]); const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); @@ -621,14 +619,12 @@ export function templateCompositeFrom(description) { } } - if (descriptionValue && 'validate' in descriptionValue) { - if (tokenShape === 'input.value') { - try { - descriptionValue.validate(tokenValue); - } catch (error) { - validateFailedInputNames.push(name); - validateFailedErrors.push(error); - } + if (tokenShape === 'input.value') { + try { + validateInputValue(tokenValue, description.inputs[name]); + } catch (error) { + error.message = `${name}: ${error.message}`; + validateFailedErrors.push(error); } } } @@ -649,19 +645,14 @@ export function templateCompositeFrom(description) { push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); } - for (const {name, validationError} of stitchArrays({ - name: validateFailedInputNames, - validationError: validateFailedErrors, - })) { - const error = new Error(`${name}: Validation failed for static value`); - error.cause = validationError; - push(error); - } - for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; push(new Error(`${name}: Expected string or input() call, got ${type}`)); } + + for (const error of validateFailedErrors) { + push(error); + } }); const inputMetadata = getStaticInputMetadata(inputOptions); @@ -1211,7 +1202,7 @@ export function compositeFrom(description) { } }); - withAggregate({message: `Errors in dynamic input values provided to ${compositionName}`}, ({push}) => { + withAggregate({message: `Errors in input values provided to ${compositionName}`}, ({push}) => { for (const {dynamic, name, value, description} of stitchArrays({ dynamic: inputsMayBeDynamicValue, name: inputNames, @@ -1223,7 +1214,7 @@ export function compositeFrom(description) { validateInputValue(value, description); } catch (error) { error.message = `${name}: ${error.message}`; - throw error; + push(error); } } }); -- cgit 1.3.0-6-gf8a5 From d719eff73be9b18a3c83b984e68469c3be91457c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 28 Sep 2023 13:19:52 -0300 Subject: data: compositeFrom: validate static token shapes for normal input --- src/data/things/composite.js | 49 +++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e58b6524..de6827c6 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -576,7 +576,7 @@ export function templateCompositeFrom(description) { expectedInputNames .filter(name => !providedInputNames.includes(name)) .filter(name => { - const inputDescription = description.inputs[name].value; + const inputDescription = getInputTokenValue(description.inputs[name]); if (!inputDescription) return true; if ('defaultValue' in inputDescription) return false; if ('defaultDependency' in inputDescription) return false; @@ -587,6 +587,7 @@ export function templateCompositeFrom(description) { const expectedStaticValueInputNames = []; const expectedStaticDependencyInputNames = []; + const expectedValueProvidingTokenInputNames = []; const validateFailedErrors = []; @@ -605,18 +606,33 @@ export function templateCompositeFrom(description) { const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); - if (descriptionShape === 'input.staticValue') { - if (tokenShape !== 'input.value') { - expectedStaticValueInputNames.push(name); - continue; - } - } + switch (descriptionShape) { + case'input.staticValue': + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } + break; - if (descriptionShape === 'input.staticDependency') { - if (typeof value !== 'string' && tokenShape !== 'input.dependency') { - expectedStaticDependencyInputNames.push(name); - continue; - } + case 'input.staticDependency': + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } + break; + + case 'input': + if (typeof value !== 'string' && ![ + 'input', + 'input.value', + 'input.dependency', + 'input.myself', + 'input.updateValue', + ].includes(tokenShape)) { + expectedValueProvidingTokenInputNames.push(name); + continue; + } + break; } if (tokenShape === 'input.value') { @@ -645,6 +661,15 @@ export function templateCompositeFrom(description) { push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); } + for (const name of expectedValueProvidingTokenInputNames) { + const shapeOrType = + (isInputToken(inputOptions[name]) + ? getInputTokenShape(inputOptions[name]) + : typeof inputOptions[name]); + + push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${shapeOrType}`)); + } + for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; push(new Error(`${name}: Expected string or input() call, got ${type}`)); -- cgit 1.3.0-6-gf8a5 From 518647f8b80ffda6d502b1a75656da7f2ae4b9d3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 28 Sep 2023 14:00:18 -0300 Subject: data: templateCompositeFrom: improve error message consistency --- src/data/things/composite.js | 32 +++++++++++++++++++------------- src/util/sugar.js | 10 ++++++++++ 2 files changed, 29 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index de6827c6..33f49e9b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -16,6 +16,7 @@ import { filterProperties, openAggregate, stitchArrays, + typeAppearance, unique, withAggregate, } from '#sugar'; @@ -381,7 +382,9 @@ input.staticDependency = _valueIntoToken('input.staticDependency'); input.staticValue = _valueIntoToken('input.staticValue'); function isInputToken(token) { - if (typeof token === 'object') { + if (token === null) { + return false; + } else if (typeof token === 'object') { return token.symbol === Symbol.for('hsmusic.composite.input'); } else if (typeof token === 'symbol') { return token.description.startsWith('hsmusic.composite.input'); @@ -653,26 +656,29 @@ export function templateCompositeFrom(description) { push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); } - if (!empty(expectedStaticDependencyInputNames)) { - push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + const inputAppearance = name => + (isInputToken(inputOptions[name]) + ? `${getInputTokenShape(inputOptions[name])}() call` + : `dependency name`); + + for (const name of expectedStaticDependencyInputNames) { + const appearance = inputAppearance(name); + push(new Error(`${name}: Expected dependency name, got ${appearance}`)); } - if (!empty(expectedStaticValueInputNames)) { - push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + for (const name of expectedStaticValueInputNames) { + const appearance = inputAppearance(name) + push(new Error(`${name}: Expected input.value() call, got ${appearance}`)); } for (const name of expectedValueProvidingTokenInputNames) { - const shapeOrType = - (isInputToken(inputOptions[name]) - ? getInputTokenShape(inputOptions[name]) - : typeof inputOptions[name]); - - push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${shapeOrType}`)); + const appearance = getInputTokenShape(inputOptions[name]); + push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${appearance}`)); } for (const name of wrongTypeInputNames) { - const type = typeof inputOptions[name]; - push(new Error(`${name}: Expected string or input() call, got ${type}`)); + const type = typeAppearance(inputOptions[name]); + push(new Error(`${name}: Expected dependency name or input() call, got ${type}`)); } for (const error of validateFailedErrors) { diff --git a/src/util/sugar.js b/src/util/sugar.js index 24d409fb..ef6ab18c 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -230,6 +230,16 @@ export function escapeRegex(string) { return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); } +// Gets the "look" of some arbitrary value. It's like typeof, but smarter. +// Don't use this for actually validating types - it's only suitable for +// inclusion in error messages. +export function typeAppearance(value) { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (Array.isArray(value)) return 'array'; + return typeof value; +} + // Binds default values for arguments in a {key: value} type function argument // (typically the second argument, but may be overridden by providing a // [bindOpts.bindIndex] argument). Typically useful for preparing a function for -- cgit 1.3.0-6-gf8a5 From 411c053dc4b314b2bc0d58d3899fd796ad0054c2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 28 Sep 2023 14:11:02 -0300 Subject: data, util: use typeAppearance in more places --- src/data/things/composite.js | 30 +++++++++++++++--------------- src/data/things/validators.js | 6 +++--- src/util/html.js | 4 ++-- src/util/replacer.js | 4 ++-- src/util/sugar.js | 6 +++--- 5 files changed, 25 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 33f49e9b..b6009525 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -395,7 +395,7 @@ function isInputToken(token) { function getInputTokenShape(token) { if (!isInputToken(token)) { - throw new TypeError(`Expected an input token, got ${token}`); + throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`); } if (typeof token === 'object') { @@ -407,7 +407,7 @@ function getInputTokenShape(token) { function getInputTokenValue(token) { if (!isInputToken(token)) { - throw new TypeError(`Expected an input token, got ${token}`); + throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`); } if (typeof token === 'object') { @@ -464,8 +464,8 @@ function validateInputValue(value, description) { } else { throw new TypeError( (type - ? `Expected ${type}, got ${value}` - : `Expected value, got ${value}`)); + ? `Expected ${type}, got ${typeAppearance(value)}` + : `Expected value, got ${typeAppearance(value)}`)); } } @@ -478,7 +478,7 @@ function validateInputValue(value, description) { : typeof value); if (typeofValue !== type) { - throw new TypeError(`Expected ${type}, got ${typeofValue}`); + throw new TypeError(`Expected ${type}, got ${typeAppearance(value)}`); } } @@ -503,11 +503,11 @@ export function templateCompositeFrom(description) { validateInputs: if ('inputs' in description) { - if (Array.isArray(description.inputs)) { - push(new Error(`Expected inputs to be object, got array`)); - break validateInputs; - } else if (typeof description.inputs !== 'object') { - push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + if ( + Array.isArray(description.inputs) || + typeof description.inputs !== 'object' + ) { + push(new Error(`Expected inputs to be object, got ${typeAppearance(description.inputs)}`)); break validateInputs; } @@ -543,7 +543,7 @@ export function templateCompositeFrom(description) { !Array.isArray(description.outputs) && typeof description.outputs !== 'function' ) { - push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + push(new Error(`Expected outputs to be array or function, got ${typeAppearance(description.outputs)}`)); break validateOutputs; } @@ -552,7 +552,7 @@ export function templateCompositeFrom(description) { description.outputs, decorateErrorWithIndex(value => { if (typeof value !== 'string') { - throw new Error(`${value}: Expected string, got ${typeof value}`) + throw new Error(`${value}: Expected string, got ${typeAppearance(value)}`) } else if (!value.startsWith('#')) { throw new Error(`${value}: Expected "#" at start`); } @@ -733,8 +733,8 @@ export function templateCompositeFrom(description) { } for (const name of wrongTypeOutputNames) { - const type = typeof providedOptions[name]; - push(new Error(`${name}: Expected string, got ${type}`)); + const appearance = typeAppearance(providedOptions[name]); + push(new Error(`${name}: Expected string, got ${appearance}`)); } }); @@ -865,7 +865,7 @@ export function compositeFrom(description) { if (!Array.isArray(description.steps)) { throw new TypeError( - `Expected steps to be array, got ${typeof description.steps}` + + `Expected steps to be array, got ${typeAppearance(description.steps)}` + (annotation ? ` (${annotation})` : '')); } diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 048f7ebb..ba62fb84 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -1,7 +1,7 @@ import {inspect as nodeInspect} from 'node:util'; import {colors, ENABLE_COLOR} from '#cli'; -import {empty, withAggregate} from '#sugar'; +import {empty, typeAppearance, withAggregate} from '#sugar'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -15,7 +15,7 @@ export function a(noun) { export function isType(value, type) { if (typeof value !== type) - throw new TypeError(`Expected ${a(type)}, got ${typeof value}`); + throw new TypeError(`Expected ${a(type)}, got ${typeAppearance(value)}`); return true; } @@ -132,7 +132,7 @@ export function isObject(value) { export function isArray(value) { if (typeof value !== 'object' || value === null || !Array.isArray(value)) - throw new TypeError(`Expected an array, got ${value}`); + throw new TypeError(`Expected an array, got ${typeAppearance(value)}`); return true; } diff --git a/src/util/html.js b/src/util/html.js index c7395fbf..282a52da 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -2,7 +2,7 @@ import {inspect} from 'node:util'; -import {empty} from '#sugar'; +import {empty, typeAppearance} from '#sugar'; import * as commonValidators from '#validators'; // COMPREHENSIVE! @@ -633,7 +633,7 @@ export class Template { static validateDescription(description) { if (typeof description !== 'object') { - throw new TypeError(`Expected object, got ${typeof description}`); + throw new TypeError(`Expected object, got ${typeAppearance(description)}`); } if (description === null) { diff --git a/src/util/replacer.js b/src/util/replacer.js index 647d1f0e..095ee060 100644 --- a/src/util/replacer.js +++ b/src/util/replacer.js @@ -6,7 +6,7 @@ // for embedding in a wiki webpage. import * as html from '#html'; -import {escapeRegex} from '#sugar'; +import {escapeRegex, typeAppearance} from '#sugar'; // Syntax literals. const tagBeginning = '[['; @@ -407,7 +407,7 @@ export function postprocessHeadings(inputNodes) { export function parseInput(input) { if (typeof input !== 'string') { - throw new TypeError(`Expected input to be string, got ${input}`); + throw new TypeError(`Expected input to be string, got ${typeAppearance(input)}`); } try { diff --git a/src/util/sugar.js b/src/util/sugar.js index ef6ab18c..29fcf848 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -82,7 +82,7 @@ export function stitchArrays(keyToArray) { for (const [key, value] of Object.entries(keyToArray)) { if (value === null) continue; if (Array.isArray(value)) continue; - errors.push(new TypeError(`(${key}) Expected array or null, got ${value}`)); + errors.push(new TypeError(`(${key}) Expected array or null, got ${typeAppearance(value)}`)); } if (!empty(errors)) { @@ -170,11 +170,11 @@ export function setIntersection(set1, set2) { export function filterProperties(object, properties) { if (typeof object !== 'object' || object === null) { - throw new TypeError(`Expected object to be an object, got ${object}`); + throw new TypeError(`Expected object to be an object, got ${typeAppearance(object)}`); } if (!Array.isArray(properties)) { - throw new TypeError(`Expected properties to be an array, got ${properties}`); + throw new TypeError(`Expected properties to be an array, got ${typeAppearance(properties)}`); } const filteredObject = {}; -- cgit 1.3.0-6-gf8a5 From f7376bb5eb2671de7242872ec0c4615b5e244aba Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 28 Sep 2023 14:12:56 -0300 Subject: data: misc minor fixes --- src/data/things/composite.js | 6 +----- src/data/things/homepage-layout.js | 1 - src/data/things/thing.js | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b6009525..eb93bd7c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -976,8 +976,6 @@ export function compositeFrom(description) { const compositionNests = description.compose ?? true; - const exposeDependencies = new Set(); - // Steps default to exposing if using a shorthand syntax where flags aren't // specified at all. const stepsExpose = @@ -1101,7 +1099,6 @@ export function compositeFrom(description) { const stepEntries = stitchArrays({ step: steps, - expose: stepExposeDescriptions, stepComposes: stepsCompose, stepComputes: stepsCompute, stepTransforms: stepsTransform, @@ -1110,7 +1107,6 @@ export function compositeFrom(description) { for (let i = 0; i < stepEntries.length; i++) { const { step, - expose, stepComposes, stepComputes, stepTransforms, @@ -2046,7 +2042,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ '#entries', ], - compute: ({ + compute: (continuation, { [input.staticDependency('object')]: object, [input.staticValue('properties')]: properties, [input.staticValue('prefix')]: prefix, diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index bade280c..bcf99e80 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,7 +1,6 @@ import find from '#find'; import { - compositeFrom, exposeDependency, input, } from '#composite'; diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 290be59b..f1302e17 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -456,7 +456,7 @@ export function inputWikiData({ allowMixedTypes = false, } = {}) { return input({ - validate: validateWikiData(referenceType), + validate: validateWikiData({referenceType, allowMixedTypes}), acceptsNull: true, }); } -- cgit 1.3.0-6-gf8a5 From ea02f6453f697d1e9fc6cfef2cdcf454c3f4286e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 29 Sep 2023 10:09:20 -0300 Subject: data: fix & tidy dynamic outputs in utilities --- src/data/things/composite.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index eb93bd7c..7a3a8319 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1938,15 +1938,12 @@ export const withPropertyFromObject = templateCompositeFrom({ outputs: ({ [input.staticDependency('object')]: object, [input.staticValue('property')]: property, - }) => { - return [ - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - ]; - }, + }) => + (object && property + ? (object.startsWith('#') + ? [`${object}.${property}`] + : [`#${object}.${property}`]) + : ['#value']), steps: () => [ { @@ -2018,7 +2015,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ : object ? `${object}.${property}` : `#object.${property}`)) - : '#object'), + : ['#object']), steps: () => [ { @@ -2135,7 +2132,7 @@ export const withPropertiesFromList = templateCompositeFrom({ : list ? `${list}.${property}` : `#list.${property}`)) - : '#lists'), + : ['#lists']), steps: () => [ { -- cgit 1.3.0-6-gf8a5 From e4dc2be4c12a5578bfb5d5945a592907aed1cb4f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 29 Sep 2023 10:36:59 -0300 Subject: data, test: type validation message adjustments --- src/data/things/composite.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7a3a8319..c03f8833 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -4,6 +4,7 @@ import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; import { + a, is, isString, isWholeNumber, @@ -464,8 +465,8 @@ function validateInputValue(value, description) { } else { throw new TypeError( (type - ? `Expected ${type}, got ${typeAppearance(value)}` - : `Expected value, got ${typeAppearance(value)}`)); + ? `Expected ${a(type)}, got ${typeAppearance(value)}` + : `Expected a value, got ${typeAppearance(value)}`)); } } @@ -478,7 +479,7 @@ function validateInputValue(value, description) { : typeof value); if (typeofValue !== type) { - throw new TypeError(`Expected ${type}, got ${typeAppearance(value)}`); + throw new TypeError(`Expected ${a(type)}, got ${typeAppearance(value)}`); } } @@ -1997,6 +1998,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ object: input({type: 'object', acceptsNull: true}), properties: input({ + type: 'array', validate: validateArrayItems(isString), }), -- cgit 1.3.0-6-gf8a5 From 13b25a8d48d142b60d5c351aad4ad1bf80104320 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 30 Sep 2023 08:29:13 -0300 Subject: util, test: WIP decorate error with index symbol --- src/util/sugar.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/util/sugar.js b/src/util/sugar.js index 29fcf848..0522b59e 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -624,6 +624,7 @@ export function decorateErrorWithIndex(fn) { return fn(x, index, array); } catch (error) { error.message = `(${colors.yellow(`#${index + 1}`)}) ${error.message}`; + error[Symbol.for('hsmusic.sugar.index')] = 1; throw error; } }; -- cgit 1.3.0-6-gf8a5 From 6eaa070e5c036ba8cd45f79c16dc2732b40ea480 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 30 Sep 2023 09:14:29 -0300 Subject: data, util: hsmusic.sugar.index -> hsmusic.decorate.indexInSourceArray --- src/data/things/validators.js | 3 ++- src/util/sugar.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index ba62fb84..bdb22058 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -174,7 +174,8 @@ function validateArrayItemsHelper(itemValidator) { throw new Error(`Expected validator to return true`); } } catch (error) { - error.message = `(index: ${colors.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; + error.message = `(index: ${colors.yellow(`${index}`)}, item: ${inspect(item)}) ${error.message}`; + error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index; throw error; } }; diff --git a/src/util/sugar.js b/src/util/sugar.js index 0522b59e..2e724bae 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -624,7 +624,7 @@ export function decorateErrorWithIndex(fn) { return fn(x, index, array); } catch (error) { error.message = `(${colors.yellow(`#${index + 1}`)}) ${error.message}`; - error[Symbol.for('hsmusic.sugar.index')] = 1; + error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index; throw error; } }; -- cgit 1.3.0-6-gf8a5 From ab7591e45e7e31b4e2c0e2f81e224672145993fa Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 1 Oct 2023 17:01:21 -0300 Subject: data, test: refactor utilities into own file Primarily for more precies test coverage mapping, but also to make navigation a bit easier and consolidate complex functions with lots of imports out of the same space as other, more simple or otherwise specialized files. --- .../control-flow/exitWithoutDependency.js | 35 + .../control-flow/exitWithoutUpdateValue.js | 24 + src/data/composite/control-flow/exposeConstant.js | 26 + .../composite/control-flow/exposeDependency.js | 28 + .../control-flow/exposeDependencyOrContinue.js | 34 + .../control-flow/exposeUpdateValueOrContinue.js | 40 ++ src/data/composite/control-flow/index.js | 9 + .../control-flow/inputAvailabilityCheckMode.js | 9 + .../control-flow/raiseOutputWithoutDependency.js | 39 ++ .../control-flow/raiseOutputWithoutUpdateValue.js | 47 ++ .../control-flow/withResultOfAvailabilityCheck.js | 66 ++ src/data/composite/data/excludeFromList.js | 56 ++ src/data/composite/data/fillMissingListItems.js | 51 ++ src/data/composite/data/index.js | 8 + src/data/composite/data/withFlattenedList.js | 47 ++ src/data/composite/data/withPropertiesFromList.js | 92 +++ .../composite/data/withPropertiesFromObject.js | 87 +++ src/data/composite/data/withPropertyFromList.js | 56 ++ src/data/composite/data/withPropertyFromObject.js | 69 ++ src/data/composite/data/withUnflattenedList.js | 62 ++ src/data/composite/things/album/index.js | 2 + .../composite/things/album/withTrackSections.js | 119 ++++ src/data/composite/things/album/withTracks.js | 51 ++ .../things/track/exitWithoutUniqueCoverArt.js | 26 + src/data/composite/things/track/index.js | 9 + .../things/track/inheritFromOriginalRelease.js | 43 ++ .../things/track/trackReverseReferenceList.js | 38 ++ src/data/composite/things/track/withAlbum.js | 57 ++ .../things/track/withAlwaysReferenceByDirectory.js | 52 ++ .../things/track/withContainingTrackSection.js | 63 ++ .../things/track/withHasUniqueCoverArt.js | 61 ++ .../composite/things/track/withOriginalRelease.js | 59 ++ .../composite/things/track/withOtherReleases.js | 40 ++ .../things/track/withPropertyFromAlbum.js | 49 ++ .../composite/wiki-data/exitWithoutContribs.js | 47 ++ src/data/composite/wiki-data/index.js | 7 + src/data/composite/wiki-data/inputThingClass.js | 23 + src/data/composite/wiki-data/inputWikiData.js | 17 + .../composite/wiki-data/withResolvedContribs.js | 77 +++ .../composite/wiki-data/withResolvedReference.js | 73 +++ .../wiki-data/withResolvedReferenceList.js | 101 +++ .../wiki-data/withReverseReferenceList.js | 40 ++ .../composite/wiki-properties/additionalFiles.js | 30 + src/data/composite/wiki-properties/color.js | 12 + src/data/composite/wiki-properties/commentary.js | 12 + .../wiki-properties/commentatorArtists.js | 55 ++ .../composite/wiki-properties/contribsPresent.js | 30 + .../composite/wiki-properties/contributionList.js | 35 + src/data/composite/wiki-properties/dimensions.js | 13 + src/data/composite/wiki-properties/directory.js | 23 + src/data/composite/wiki-properties/duration.js | 13 + .../composite/wiki-properties/externalFunction.js | 11 + .../composite/wiki-properties/fileExtension.js | 13 + src/data/composite/wiki-properties/flag.js | 19 + src/data/composite/wiki-properties/index.js | 20 + src/data/composite/wiki-properties/name.js | 11 + .../composite/wiki-properties/referenceList.js | 47 ++ .../wiki-properties/reverseReferenceList.js | 30 + src/data/composite/wiki-properties/simpleDate.js | 14 + src/data/composite/wiki-properties/simpleString.js | 14 + .../composite/wiki-properties/singleReference.js | 47 ++ src/data/composite/wiki-properties/urls.js | 14 + src/data/composite/wiki-properties/wikiData.js | 17 + src/data/things/album.js | 157 +---- src/data/things/art-tag.js | 10 +- src/data/things/artist.js | 6 +- src/data/things/composite.js | 727 +-------------------- src/data/things/flash.js | 6 +- src/data/things/group.js | 6 +- src/data/things/homepage-layout.js | 16 +- src/data/things/language.js | 9 +- src/data/things/news-entry.js | 6 +- src/data/things/static-page.js | 6 +- src/data/things/thing.js | 713 +------------------- src/data/things/track.js | 471 +------------ src/data/things/wiki-info.js | 6 +- 76 files changed, 2516 insertions(+), 2042 deletions(-) create mode 100644 src/data/composite/control-flow/exitWithoutDependency.js create mode 100644 src/data/composite/control-flow/exitWithoutUpdateValue.js create mode 100644 src/data/composite/control-flow/exposeConstant.js create mode 100644 src/data/composite/control-flow/exposeDependency.js create mode 100644 src/data/composite/control-flow/exposeDependencyOrContinue.js create mode 100644 src/data/composite/control-flow/exposeUpdateValueOrContinue.js create mode 100644 src/data/composite/control-flow/index.js create mode 100644 src/data/composite/control-flow/inputAvailabilityCheckMode.js create mode 100644 src/data/composite/control-flow/raiseOutputWithoutDependency.js create mode 100644 src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js create mode 100644 src/data/composite/control-flow/withResultOfAvailabilityCheck.js create mode 100644 src/data/composite/data/excludeFromList.js create mode 100644 src/data/composite/data/fillMissingListItems.js create mode 100644 src/data/composite/data/index.js create mode 100644 src/data/composite/data/withFlattenedList.js create mode 100644 src/data/composite/data/withPropertiesFromList.js create mode 100644 src/data/composite/data/withPropertiesFromObject.js create mode 100644 src/data/composite/data/withPropertyFromList.js create mode 100644 src/data/composite/data/withPropertyFromObject.js create mode 100644 src/data/composite/data/withUnflattenedList.js create mode 100644 src/data/composite/things/album/index.js create mode 100644 src/data/composite/things/album/withTrackSections.js create mode 100644 src/data/composite/things/album/withTracks.js create mode 100644 src/data/composite/things/track/exitWithoutUniqueCoverArt.js create mode 100644 src/data/composite/things/track/index.js create mode 100644 src/data/composite/things/track/inheritFromOriginalRelease.js create mode 100644 src/data/composite/things/track/trackReverseReferenceList.js create mode 100644 src/data/composite/things/track/withAlbum.js create mode 100644 src/data/composite/things/track/withAlwaysReferenceByDirectory.js create mode 100644 src/data/composite/things/track/withContainingTrackSection.js create mode 100644 src/data/composite/things/track/withHasUniqueCoverArt.js create mode 100644 src/data/composite/things/track/withOriginalRelease.js create mode 100644 src/data/composite/things/track/withOtherReleases.js create mode 100644 src/data/composite/things/track/withPropertyFromAlbum.js create mode 100644 src/data/composite/wiki-data/exitWithoutContribs.js create mode 100644 src/data/composite/wiki-data/index.js create mode 100644 src/data/composite/wiki-data/inputThingClass.js create mode 100644 src/data/composite/wiki-data/inputWikiData.js create mode 100644 src/data/composite/wiki-data/withResolvedContribs.js create mode 100644 src/data/composite/wiki-data/withResolvedReference.js create mode 100644 src/data/composite/wiki-data/withResolvedReferenceList.js create mode 100644 src/data/composite/wiki-data/withReverseReferenceList.js create mode 100644 src/data/composite/wiki-properties/additionalFiles.js create mode 100644 src/data/composite/wiki-properties/color.js create mode 100644 src/data/composite/wiki-properties/commentary.js create mode 100644 src/data/composite/wiki-properties/commentatorArtists.js create mode 100644 src/data/composite/wiki-properties/contribsPresent.js create mode 100644 src/data/composite/wiki-properties/contributionList.js create mode 100644 src/data/composite/wiki-properties/dimensions.js create mode 100644 src/data/composite/wiki-properties/directory.js create mode 100644 src/data/composite/wiki-properties/duration.js create mode 100644 src/data/composite/wiki-properties/externalFunction.js create mode 100644 src/data/composite/wiki-properties/fileExtension.js create mode 100644 src/data/composite/wiki-properties/flag.js create mode 100644 src/data/composite/wiki-properties/index.js create mode 100644 src/data/composite/wiki-properties/name.js create mode 100644 src/data/composite/wiki-properties/referenceList.js create mode 100644 src/data/composite/wiki-properties/reverseReferenceList.js create mode 100644 src/data/composite/wiki-properties/simpleDate.js create mode 100644 src/data/composite/wiki-properties/simpleString.js create mode 100644 src/data/composite/wiki-properties/singleReference.js create mode 100644 src/data/composite/wiki-properties/urls.js create mode 100644 src/data/composite/wiki-properties/wikiData.js (limited to 'src') diff --git a/src/data/composite/control-flow/exitWithoutDependency.js b/src/data/composite/control-flow/exitWithoutDependency.js new file mode 100644 index 00000000..c660a7ef --- /dev/null +++ b/src/data/composite/control-flow/exitWithoutDependency.js @@ -0,0 +1,35 @@ +// Early exits if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutDependency`, + + inputs: { + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + value: input({defaultValue: null}), + }, + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], +}); diff --git a/src/data/composite/control-flow/exitWithoutUpdateValue.js b/src/data/composite/control-flow/exitWithoutUpdateValue.js new file mode 100644 index 00000000..244b3233 --- /dev/null +++ b/src/data/composite/control-flow/exitWithoutUpdateValue.js @@ -0,0 +1,24 @@ +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import exitWithoutDependency from './exitWithoutDependency.js'; +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutUpdateValue`, + + inputs: { + mode: inputAvailabilityCheckMode(), + value: input({defaultValue: null}), + }, + + steps: () => [ + exitWithoutDependency({ + dependency: input.updateValue(), + mode: input('mode'), + value: input('value'), + }), + ], +}); diff --git a/src/data/composite/control-flow/exposeConstant.js b/src/data/composite/control-flow/exposeConstant.js new file mode 100644 index 00000000..e0435478 --- /dev/null +++ b/src/data/composite/control-flow/exposeConstant.js @@ -0,0 +1,26 @@ +// Exposes a constant value exactly as it is; like exposeDependency, this +// is typically the base of a composition serving as a particular property +// descriptor. It generally follows steps which will conditionally early +// exit with some other value, with the exposeConstant base serving as the +// fallback default value. + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `exposeConstant`, + + compose: false, + + inputs: { + value: input.staticValue(), + }, + + steps: () => [ + { + dependencies: [input('value')], + compute: ({ + [input('value')]: value, + }) => value, + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeDependency.js b/src/data/composite/control-flow/exposeDependency.js new file mode 100644 index 00000000..3aa3d03a --- /dev/null +++ b/src/data/composite/control-flow/exposeDependency.js @@ -0,0 +1,28 @@ +// Exposes a dependency exactly as it is; this is typically the base of a +// composition which was created to serve as one property's descriptor. +// +// Please note that this *doesn't* verify that the dependency exists, so +// if you provide the wrong name or it hasn't been set by a previous +// compositional step, the property will be exposed as undefined instead +// of null. + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `exposeDependency`, + + compose: false, + + inputs: { + dependency: input.staticDependency({acceptsNull: true}), + }, + + steps: () => [ + { + dependencies: [input('dependency')], + compute: ({ + [input('dependency')]: dependency + }) => dependency, + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeDependencyOrContinue.js b/src/data/composite/control-flow/exposeDependencyOrContinue.js new file mode 100644 index 00000000..0f7f223e --- /dev/null +++ b/src/data/composite/control-flow/exposeDependencyOrContinue.js @@ -0,0 +1,34 @@ +// Exposes a dependency as it is, or continues if it's unavailable. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `exposeDependencyOrContinue`, + + inputs: { + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + }, + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('dependency')], + compute: (continuation, { + ['#availability']: availability, + [input('dependency')]: dependency, + }) => + (availability + ? continuation.exit(dependency) + : continuation()), + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeUpdateValueOrContinue.js b/src/data/composite/control-flow/exposeUpdateValueOrContinue.js new file mode 100644 index 00000000..1f94b332 --- /dev/null +++ b/src/data/composite/control-flow/exposeUpdateValueOrContinue.js @@ -0,0 +1,40 @@ +// Exposes the update value of an {update: true} property as it is, +// or continues if it's unavailable. +// +// See withResultOfAvailabilityCheck for {mode} options. +// +// Provide {validate} here to conveniently set a custom validation check +// for this property's update value. +// + +import {input, templateCompositeFrom} from '#composite'; + +import exposeDependencyOrContinue from './exposeDependencyOrContinue.js'; +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default templateCompositeFrom({ + annotation: `exposeUpdateValueOrContinue`, + + inputs: { + mode: inputAvailabilityCheckMode(), + + validate: input({ + type: 'function', + defaultValue: null, + }), + }, + + update: ({ + [input.staticValue('validate')]: validate, + }) => + (validate + ? {validate} + : {}), + + steps: () => [ + exposeDependencyOrContinue({ + dependency: input.updateValue(), + mode: input('mode'), + }), + ], +}); diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js new file mode 100644 index 00000000..dfc53db7 --- /dev/null +++ b/src/data/composite/control-flow/index.js @@ -0,0 +1,9 @@ +export {default as exitWithoutDependency} from './exitWithoutDependency.js'; +export {default as exitWithoutUpdateValue} from './exitWithoutUpdateValue.js'; +export {default as exposeConstant} from './exposeConstant.js'; +export {default as exposeDependency} from './exposeDependency.js'; +export {default as exposeDependencyOrContinue} from './exposeDependencyOrContinue.js'; +export {default as exposeUpdateValueOrContinue} from './exposeUpdateValueOrContinue.js'; +export {default as raiseOutputWithoutDependency} from './raiseOutputWithoutDependency.js'; +export {default as raiseOutputWithoutUpdateValue} from './raiseOutputWithoutUpdateValue.js'; +export {default as withResultOfAvailabilityCheck} from './withResultOfAvailabilityCheck.js'; diff --git a/src/data/composite/control-flow/inputAvailabilityCheckMode.js b/src/data/composite/control-flow/inputAvailabilityCheckMode.js new file mode 100644 index 00000000..d74a1149 --- /dev/null +++ b/src/data/composite/control-flow/inputAvailabilityCheckMode.js @@ -0,0 +1,9 @@ +import {input} from '#composite'; +import {is} from '#validators'; + +export default function inputAvailabilityCheckMode() { + return input({ + validate: is('null', 'empty', 'falsy'), + defaultValue: 'null', + }); +} diff --git a/src/data/composite/control-flow/raiseOutputWithoutDependency.js b/src/data/composite/control-flow/raiseOutputWithoutDependency.js new file mode 100644 index 00000000..03d8036a --- /dev/null +++ b/src/data/composite/control-flow/raiseOutputWithoutDependency.js @@ -0,0 +1,39 @@ +// Raises if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `raiseOutputWithoutDependency`, + + inputs: { + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + output: input.staticValue({defaultValue: {}}), + }, + + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => + (availability + ? continuation() + : continuation.raiseOutputAbove(output)), + }, + ], +}); diff --git a/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js new file mode 100644 index 00000000..3c39f5ba --- /dev/null +++ b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js @@ -0,0 +1,47 @@ +// Raises if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `raiseOutputWithoutUpdateValue`, + + inputs: { + mode: inputAvailabilityCheckMode(), + output: input.staticValue({defaultValue: {}}), + }, + + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input.updateValue(), + mode: input('mode'), + }), + + // TODO: A bit of a kludge, below. Other "do something with the update + // value" type functions can get by pretty much just passing that value + // as an input (input.updateValue()) into the corresponding "do something + // with a dependency/arbitrary value" function. But we can't do that here, + // because the special behavior, raiseOutputAbove(), only works to raise + // output above the composition it's *directly* nested in. Other languages + // have a throw/catch system that might serve as inspiration for something + // better here. + + { + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => + (availability + ? continuation() + : continuation.raiseOutputAbove(output)), + }, + ], +}); diff --git a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js new file mode 100644 index 00000000..bcbd0b37 --- /dev/null +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -0,0 +1,66 @@ +// Checks the availability of a dependency and provides the result to later +// steps under '#availability' (by default). This is mainly intended for use +// by the more specific utilities, which you should consider using instead. +// +// Customize {mode} to select one of these modes, or default to 'null': +// +// * 'null': Check that the value isn't null (and not undefined either). +// * 'empty': Check that the value is neither null, undefined, nor an empty +// array. +// * 'falsy': Check that the value isn't false when treated as a boolean +// (nor an empty array). Keep in mind this will also be false +// for values like zero and the empty string! +// +// See also: +// - exitWithoutDependency +// - exitWithoutUpdateValue +// - exposeDependencyOrContinue +// - exposeUpdateValueOrContinue +// - raiseOutputWithoutDependency +// - raiseOutputWithoutUpdateValue +// + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default templateCompositeFrom({ + annotation: `withResultOfAvailabilityCheck`, + + inputs: { + from: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + }, + + outputs: ['#availability'], + + steps: () => [ + { + dependencies: [input('from'), input('mode')], + + compute: (continuation, { + [input('from')]: value, + [input('mode')]: mode, + }) => { + let availability; + + switch (mode) { + case 'null': + availability = value !== undefined && value !== null; + break; + + case 'empty': + availability = value !== undefined && !empty(value); + break; + + case 'falsy': + availability = !!value && (!Array.isArray(value) || !empty(value)); + break; + } + + return continuation({'#availability': availability}); + }, + }, + ], +}); diff --git a/src/data/composite/data/excludeFromList.js b/src/data/composite/data/excludeFromList.js new file mode 100644 index 00000000..718f2294 --- /dev/null +++ b/src/data/composite/data/excludeFromList.js @@ -0,0 +1,56 @@ +// Filters particular values out of a list. Note that this will always +// completely skip over null, but can be used to filter out any other +// primitive or object value. +// +// See also: +// - fillMissingListItems +// +// More list utilities: +// - withFlattenedList +// - withPropertyFromList +// - withPropertiesFromList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +export default templateCompositeFrom({ + annotation: `excludeFromList`, + + inputs: { + list: input(), + + item: input({defaultValue: null}), + items: input({type: 'array', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], + + steps: () => [ + { + dependencies: [ + input.staticDependency('list'), + input('list'), + input('item'), + input('items'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: listName, + [input('list')]: listContents, + [input('item')]: excludeItem, + [input('items')]: excludeItems, + }) => continuation({ + [listName ?? '#list']: + listContents.filter(item => { + if (excludeItem !== null && item === excludeItem) return false; + if (!empty(excludeItems) && excludeItems.includes(item)) return false; + return true; + }), + }), + }, + ], +}); diff --git a/src/data/composite/data/fillMissingListItems.js b/src/data/composite/data/fillMissingListItems.js new file mode 100644 index 00000000..c06eceda --- /dev/null +++ b/src/data/composite/data/fillMissingListItems.js @@ -0,0 +1,51 @@ +// Replaces items of a list, which are null or undefined, with some fallback +// value. By default, this replaces the passed dependency. +// +// See also: +// - excludeFromList +// +// More list utilities: +// - withFlattenedList +// - withPropertyFromList +// - withPropertiesFromList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `fillMissingListItems`, + + inputs: { + list: input({type: 'array'}), + fill: input({acceptsNull: true}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], + + steps: () => [ + { + dependencies: [input('list'), input('fill')], + compute: (continuation, { + [input('list')]: list, + [input('fill')]: fill, + }) => continuation({ + ['#filled']: + list.map(item => item ?? fill), + }), + }, + + { + dependencies: [input.staticDependency('list'), '#filled'], + compute: (continuation, { + [input.staticDependency('list')]: list, + ['#filled']: filled, + }) => continuation({ + [list ?? '#list']: + filled, + }), + }, + ], +}); diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js new file mode 100644 index 00000000..ecd05129 --- /dev/null +++ b/src/data/composite/data/index.js @@ -0,0 +1,8 @@ +export {default as excludeFromList} from './excludeFromList.js'; +export {default as fillMissingListItems} from './fillMissingListItems.js'; +export {default as withFlattenedList} from './withFlattenedList.js'; +export {default as withPropertiesFromList} from './withPropertiesFromList.js'; +export {default as withPropertiesFromObject} from './withPropertiesFromObject.js'; +export {default as withPropertyFromList} from './withPropertyFromList.js'; +export {default as withPropertyFromObject} from './withPropertyFromObject.js'; +export {default as withUnflattenedList} from './withUnflattenedList.js'; diff --git a/src/data/composite/data/withFlattenedList.js b/src/data/composite/data/withFlattenedList.js new file mode 100644 index 00000000..b08edb4e --- /dev/null +++ b/src/data/composite/data/withFlattenedList.js @@ -0,0 +1,47 @@ +// Flattens an array with one level of nested arrays, providing as dependencies +// both the flattened array as well as the original starting indices of each +// successive source array. +// +// See also: +// - withFlattenedList +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withPropertyFromList +// - withPropertiesFromList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `withFlattenedList`, + + inputs: { + list: input({type: 'array'}), + }, + + outputs: ['#flattenedList', '#flattenedIndices'], + + steps: () => [ + { + dependencies: [input('list')], + compute(continuation, { + [input('list')]: sourceList, + }) { + const flattenedList = sourceList.flat(); + const indices = []; + let lastEndIndex = 0; + for (const {length} of sourceList) { + indices.push(lastEndIndex); + lastEndIndex += length; + } + + return continuation({ + ['#flattenedList']: flattenedList, + ['#flattenedIndices']: indices, + }); + }, + }, + ], +}); diff --git a/src/data/composite/data/withPropertiesFromList.js b/src/data/composite/data/withPropertiesFromList.js new file mode 100644 index 00000000..76ba696c --- /dev/null +++ b/src/data/composite/data/withPropertiesFromList.js @@ -0,0 +1,92 @@ +// Gets the listed properties from each of a list of objects, providing lists +// of property values each into a dependency prefixed with the same name as the +// list (by default). +// +// Like withPropertyFromList, this doesn't alter indices. +// +// See also: +// - withPropertiesFromObject +// - withPropertyFromList +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withFlattenedList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; +import {isString, validateArrayItems} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withPropertiesFromList`, + + inputs: { + list: input({type: 'array'}), + + properties: input({ + validate: validateArrayItems(isString), + }), + + prefix: input.staticValue({type: 'string', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`)) + : ['#lists']), + + steps: () => [ + { + dependencies: [input('list'), input('properties')], + compute: (continuation, { + [input('list')]: list, + [input('properties')]: properties, + }) => continuation({ + ['#lists']: + Object.fromEntries( + properties.map(property => [ + property, + list.map(item => item[property] ?? null), + ])), + }), + }, + + { + dependencies: [ + input.staticDependency('list'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#lists', + ], + + compute: (continuation, { + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#lists']: lists, + }) => + (properties + ? continuation( + Object.fromEntries( + properties.map(property => [ + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`), + lists[property], + ]))) + : continuation({'#lists': lists})), + }, + ], +}); diff --git a/src/data/composite/data/withPropertiesFromObject.js b/src/data/composite/data/withPropertiesFromObject.js new file mode 100644 index 00000000..21726b58 --- /dev/null +++ b/src/data/composite/data/withPropertiesFromObject.js @@ -0,0 +1,87 @@ +// Gets the listed properties from some object, providing each property's value +// as a dependency prefixed with the same name as the object (by default). +// If the object itself is null, all provided dependencies will be null; +// if it's missing only select properties, those will be provided as null. +// +// See also: +// - withPropertiesFromList +// - withPropertyFromObject +// + +import {input, templateCompositeFrom} from '#composite'; +import {isString, validateArrayItems} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withPropertiesFromObject`, + + inputs: { + object: input({type: 'object', acceptsNull: true}), + + properties: input({ + type: 'array', + validate: validateArrayItems(isString), + }), + + prefix: input.staticValue({type: 'string', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`)) + : ['#object']), + + steps: () => [ + { + dependencies: [input('object'), input('properties')], + compute: (continuation, { + [input('object')]: object, + [input('properties')]: properties, + }) => continuation({ + ['#entries']: + (object === null + ? properties.map(property => [property, null]) + : properties.map(property => [property, object[property]])), + }), + }, + + { + dependencies: [ + input.staticDependency('object'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#entries', + ], + + compute: (continuation, { + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#entries']: entries, + }) => + (properties + ? continuation( + Object.fromEntries( + entries.map(([property, value]) => [ + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`), + value ?? null, + ]))) + : continuation({ + ['#object']: + Object.fromEntries(entries), + })), + }, + ], +}); diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js new file mode 100644 index 00000000..3ce05fdf --- /dev/null +++ b/src/data/composite/data/withPropertyFromList.js @@ -0,0 +1,56 @@ +// Gets a property from each of a list of objects (in a dependency) and +// provides the results. +// +// This doesn't alter any list indices, so positions which were null in the +// original list are kept null here. Objects which don't have the specified +// property are retained in-place as null. +// +// See also: +// - withPropertiesFromList +// - withPropertyFromObject +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withFlattenedList +// - withUnflattenedList +// + +import {empty} from '#sugar'; + +// todo: OUHHH THIS ONE'S NOT UPDATED YET LOL +export default function({ + list, + property, + into = null, +}) { + into ??= + (list.startsWith('#') + ? `${list}.${property}` + : `#${list}.${property}`); + + return { + annotation: `withPropertyFromList`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + mapContinuation: {into}, + options: {property}, + + compute(continuation, {list, '#options': {property}}) { + if (list === undefined || empty(list)) { + return continuation({into: []}); + } + + return continuation({ + into: + list.map(item => + (item === null || item === undefined + ? null + : item[property] ?? null)), + }); + }, + }, + }; +} diff --git a/src/data/composite/data/withPropertyFromObject.js b/src/data/composite/data/withPropertyFromObject.js new file mode 100644 index 00000000..b31bab15 --- /dev/null +++ b/src/data/composite/data/withPropertyFromObject.js @@ -0,0 +1,69 @@ +// Gets a property of some object (in a dependency) and provides that value. +// If the object itself is null, or the object doesn't have the listed property, +// the provided dependency will also be null. +// +// See also: +// - withPropertiesFromObject +// - withPropertyFromList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `withPropertyFromObject`, + + inputs: { + object: input({type: 'object', acceptsNull: true}), + property: input({type: 'string'}), + }, + + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => + (object && property + ? (object.startsWith('#') + ? [`${object}.${property}`] + : [`#${object}.${property}`]) + : ['#value']), + + steps: () => [ + { + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + compute: (continuation, { + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => continuation({ + '#output': + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value'), + }), + }, + + { + dependencies: [ + '#output', + input('object'), + input('property'), + ], + + compute: (continuation, { + ['#output']: output, + [input('object')]: object, + [input('property')]: property, + }) => continuation({ + [output]: + (object === null + ? null + : object[property] ?? null), + }), + }, + ], +}); diff --git a/src/data/composite/data/withUnflattenedList.js b/src/data/composite/data/withUnflattenedList.js new file mode 100644 index 00000000..3cfc247b --- /dev/null +++ b/src/data/composite/data/withUnflattenedList.js @@ -0,0 +1,62 @@ +// After mapping the contents of a flattened array in-place (being careful to +// retain the original indices by replacing unmatched results with null instead +// of filtering them out), this function allows for recombining them. It will +// filter out null and undefined items by default (pass {filter: false} to +// disable this). + +import {input, templateCompositeFrom} from '#composite'; +import {isWholeNumber, validateArrayItems} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withUnflattenedList`, + + inputs: { + list: input({ + type: 'array', + defaultDependency: '#flattenedList', + }), + + indices: input({ + validate: validateArrayItems(isWholeNumber), + defaultDependency: '#flattenedIndices', + }), + + filter: input({ + type: 'boolean', + defaultValue: true, + }), + }, + + outputs: ['#unflattenedList'], + + steps: () => [ + { + dependencies: [input('list'), input('indices'), input('filter')], + compute(continuation, { + [input('list')]: list, + [input('indices')]: indices, + [input('filter')]: filter, + }) { + const unflattenedList = []; + + for (let i = 0; i < indices.length; i++) { + const startIndex = indices[i]; + const endIndex = + (i === indices.length - 1 + ? list.length + : indices[i + 1]); + + const values = list.slice(startIndex, endIndex); + unflattenedList.push( + (filter + ? values.filter(value => value !== null && value !== undefined) + : values)); + } + + return continuation({ + ['#unflattenedList']: unflattenedList, + }); + }, + }, + ], +}); diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js new file mode 100644 index 00000000..8139f10e --- /dev/null +++ b/src/data/composite/things/album/index.js @@ -0,0 +1,2 @@ +export {default as withTracks} from './withTracks.js'; +export {default as withTrackSections} from './withTrackSections.js'; diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js new file mode 100644 index 00000000..c99b94d2 --- /dev/null +++ b/src/data/composite/things/album/withTrackSections.js @@ -0,0 +1,119 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {empty, stitchArrays} from '#sugar'; +import {isTrackSectionList} from '#validators'; +import {filterMultipleArrays} from '#wiki-data'; + +import {exitWithoutDependency, exitWithoutUpdateValue} + from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +import { + fillMissingListItems, + withFlattenedList, + withPropertiesFromList, + withUnflattenedList, +} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withTrackSections`, + + outputs: ['#trackSections'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + exitWithoutUpdateValue({ + mode: input.value('empty'), + value: input.value([]), + }), + + // TODO: input.updateValue description down here is a kludge. + withPropertiesFromList({ + list: input.updateValue({ + validate: isTrackSectionList, + }), + prefix: input.value('#sections'), + properties: input.value([ + 'tracks', + 'dateOriginallyReleased', + 'isDefaultTrackSection', + 'color', + ]), + }), + + fillMissingListItems({ + list: '#sections.tracks', + fill: input.value([]), + }), + + fillMissingListItems({ + list: '#sections.isDefaultTrackSection', + fill: input.value(false), + }), + + fillMissingListItems({ + list: '#sections.color', + fill: input.dependency('color'), + }), + + withFlattenedList({ + list: '#sections.tracks', + }).outputs({ + ['#flattenedList']: '#trackRefs', + ['#flattenedIndices']: '#sections.startIndex', + }), + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + notFoundMode: input.value('null'), + find: input.value(find.track), + }).outputs({ + ['#resolvedReferenceList']: '#tracks', + }), + + withUnflattenedList({ + list: '#tracks', + indices: '#sections.startIndex', + }).outputs({ + ['#unflattenedList']: '#sections.tracks', + }), + + { + dependencies: [ + '#sections.tracks', + '#sections.color', + '#sections.dateOriginallyReleased', + '#sections.isDefaultTrackSection', + '#sections.startIndex', + ], + + compute: (continuation, { + '#sections.tracks': tracks, + '#sections.color': color, + '#sections.dateOriginallyReleased': dateOriginallyReleased, + '#sections.isDefaultTrackSection': isDefaultTrackSection, + '#sections.startIndex': startIndex, + }) => { + filterMultipleArrays( + tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, + tracks => !empty(tracks)); + + return continuation({ + ['#trackSections']: + stitchArrays({ + tracks, + color, + dateOriginallyReleased, + isDefaultTrackSection, + startIndex, + }), + }); + }, + }, + ], +}); diff --git a/src/data/composite/things/album/withTracks.js b/src/data/composite/things/album/withTracks.js new file mode 100644 index 00000000..dcea6593 --- /dev/null +++ b/src/data/composite/things/album/withTracks.js @@ -0,0 +1,51 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; + +import {exitWithoutDependency, raiseOutputWithoutDependency} + from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withTracks`, + + outputs: ['#tracks'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: 'trackSections', + mode: input.value('empty'), + output: input.value({ + ['#tracks']: [], + }), + }), + + { + dependencies: ['trackSections'], + compute: (continuation, {trackSections}) => + continuation({ + '#trackRefs': trackSections + .flatMap(section => section.tracks ?? []), + }), + }, + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + find: input.value(find.track), + }), + + { + dependencies: ['#resolvedReferenceList'], + compute: (continuation, { + ['#resolvedReferenceList']: resolvedReferenceList, + }) => continuation({ + ['#tracks']: resolvedReferenceList, + }) + }, + ], +}); diff --git a/src/data/composite/things/track/exitWithoutUniqueCoverArt.js b/src/data/composite/things/track/exitWithoutUniqueCoverArt.js new file mode 100644 index 00000000..f47086d9 --- /dev/null +++ b/src/data/composite/things/track/exitWithoutUniqueCoverArt.js @@ -0,0 +1,26 @@ +// Shorthand for checking if the track has unique cover art and exposing a +// fallback value if it isn't. + +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import withHasUniqueCoverArt from './withHasUniqueCoverArt.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutUniqueCoverArt`, + + inputs: { + value: input({defaultValue: null}), + }, + + steps: () => [ + withHasUniqueCoverArt(), + + exitWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: input.value('falsy'), + value: input('value'), + }), + ], +}); diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js new file mode 100644 index 00000000..3354b1c4 --- /dev/null +++ b/src/data/composite/things/track/index.js @@ -0,0 +1,9 @@ +export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js'; +export {default as inheritFromOriginalRelease} from './inheritFromOriginalRelease.js'; +export {default as trackReverseReferenceList} from './trackReverseReferenceList.js'; +export {default as withAlbum} from './withAlbum.js'; +export {default as withAlwaysReferenceByDirectory} from './withAlwaysReferenceByDirectory.js'; +export {default as withContainingTrackSection} from './withContainingTrackSection.js'; +export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js'; +export {default as withOtherReleases} from './withOtherReleases.js'; +export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js'; diff --git a/src/data/composite/things/track/inheritFromOriginalRelease.js b/src/data/composite/things/track/inheritFromOriginalRelease.js new file mode 100644 index 00000000..a9d57f86 --- /dev/null +++ b/src/data/composite/things/track/inheritFromOriginalRelease.js @@ -0,0 +1,43 @@ +// Early exits with a value inherited from the original release, if +// this track is a rerelease, and otherwise continues with no further +// dependencies provided. If allowOverride is true, then the continuation +// will also be called if the original release exposed the requested +// property as null. + +import {input, templateCompositeFrom} from '#composite'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `inheritFromOriginalRelease`, + + inputs: { + property: input({type: 'string'}), + allowOverride: input({type: 'boolean', defaultValue: false}), + }, + + steps: () => [ + withOriginalRelease(), + + { + dependencies: [ + '#originalRelease', + input('property'), + input('allowOverride'), + ], + + compute: (continuation, { + ['#originalRelease']: originalRelease, + [input('property')]: originalProperty, + [input('allowOverride')]: allowOverride, + }) => { + if (!originalRelease) return continuation(); + + const value = originalRelease[originalProperty]; + if (allowOverride && value === null) return continuation(); + + return continuation.exit(value); + }, + }, + ], +}); diff --git a/src/data/composite/things/track/trackReverseReferenceList.js b/src/data/composite/things/track/trackReverseReferenceList.js new file mode 100644 index 00000000..e7bfedf3 --- /dev/null +++ b/src/data/composite/things/track/trackReverseReferenceList.js @@ -0,0 +1,38 @@ +// Like a normal reverse reference list ("objects which reference this object +// under a specified property"), only excluding re-releases from the possible +// outputs. While it's useful to travel from a re-release to the tracks it +// references, re-releases aren't generally relevant from the perspective of +// the tracks *being* referenced. Apart from hiding re-releases from lists on +// the site, it also excludes keeps them from relational data processing, such +// as on the "Tracks - by Times Referenced" listing page. + +import {input, templateCompositeFrom} from '#composite'; +import {withReverseReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `trackReverseReferenceList`, + + compose: false, + + inputs: { + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseReferenceList({ + data: 'trackData', + list: input('list'), + }), + + { + flags: {expose: true}, + expose: { + dependencies: ['#reverseReferenceList'], + compute: ({ + ['#reverseReferenceList']: reverseReferenceList, + }) => + reverseReferenceList.filter(track => !track.originalReleaseTrack), + }, + }, + ], +}); diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js new file mode 100644 index 00000000..34845ab0 --- /dev/null +++ b/src/data/composite/things/track/withAlbum.js @@ -0,0 +1,57 @@ +// Gets the track's album. This will early exit if albumData is missing. +// By default, if there's no album whose list of tracks includes this track, +// the output dependency will be null; set {notFoundMode: 'exit'} to early +// exit instead. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `withAlbum`, + + inputs: { + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#album'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'albumData', + mode: input.value('empty'), + output: input.value({ + ['#album']: null, + }), + }), + + { + dependencies: [input.myself(), 'albumData'], + compute: (continuation, { + [input.myself()]: track, + ['albumData']: albumData, + }) => + continuation({ + ['#album']: + albumData.find(album => album.tracks.includes(track)), + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#album', + output: input.value({ + ['#album']: null, + }), + }), + + { + dependencies: ['#album'], + compute: (continuation, {'#album': album}) => + continuation.raiseOutput({'#album': album}), + }, + ], +}); diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js new file mode 100644 index 00000000..0aeac788 --- /dev/null +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -0,0 +1,52 @@ +// 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 {exitWithoutDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {excludeFromList, withPropertyFromObject} from '#composite/data'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `withAlwaysReferenceByDirectory`, + + steps: () => [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + excludeFromList({ + list: 'trackData', + item: input.myself(), + }), + + withOriginalRelease({ + data: '#trackData', + }), + + exitWithoutDependency({ + dependency: '#originalRelease', + value: input.value(false), + }), + + withPropertyFromObject({ + object: '#originalRelease', + property: input.value('name'), + }), + + { + dependencies: ['name', '#originalRelease.name'], + compute: (continuation, { + name, + ['#originalRelease.name']: originalName, + }) => continuation({ + ['#alwaysReferenceByDirectory']: name === originalName, + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js new file mode 100644 index 00000000..b2e5f2b3 --- /dev/null +++ b/src/data/composite/things/track/withContainingTrackSection.js @@ -0,0 +1,63 @@ +// Gets the track section containing this track from its album's track list. +// If notFoundMode is set to 'exit', this will early exit if the album can't be +// found or if none of its trackSections includes the track for some reason. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withContainingTrackSection`, + + inputs: { + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#trackSection'], + + steps: () => [ + withPropertyFromAlbum({ + property: input.value('trackSections'), + notFoundMode: input('notFoundMode'), + }), + + { + dependencies: [ + input.myself(), + input('notFoundMode'), + '#album.trackSections', + ], + + compute(continuation, { + [input.myself()]: track, + [input('notFoundMode')]: notFoundMode, + ['#album.trackSections']: trackSections, + }) { + if (!trackSections) { + return continuation.raiseOutput({ + ['#trackSection']: null, + }); + } + + const trackSection = + trackSections.find(({tracks}) => tracks.includes(track)); + + if (trackSection) { + return continuation.raiseOutput({ + ['#trackSection']: trackSection, + }); + } else if (notFoundMode === 'exit') { + return continuation.exit(null); + } else { + return continuation.raiseOutput({ + ['#trackSection']: null, + }); + } + }, + }, + ], +}); diff --git a/src/data/composite/things/track/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js new file mode 100644 index 00000000..96078d5f --- /dev/null +++ b/src/data/composite/things/track/withHasUniqueCoverArt.js @@ -0,0 +1,61 @@ +// Whether or not the track has "unique" cover artwork - a cover which is +// specifically associated with this track in particular, rather than with +// the track's album as a whole. This is typically used to select between +// displaying the track artwork and a fallback, such as the album artwork +// or a placeholder. (This property is named hasUniqueCoverArt instead of +// the usual hasCoverArt to emphasize that it does not inherit from the +// album.) + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +import {withResolvedContribs} from '#composite/wiki-data'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: 'withHasUniqueCoverArt', + + outputs: ['#hasUniqueCoverArt'], + + steps: () => [ + { + dependencies: ['disableUniqueCoverArt'], + compute: (continuation, {disableUniqueCoverArt}) => + (disableUniqueCoverArt + ? continuation.raiseOutput({ + ['#hasUniqueCoverArt']: false, + }) + : continuation()), + }, + + withResolvedContribs({from: 'coverArtistContribs'}), + + { + dependencies: ['#resolvedContribs'], + compute: (continuation, { + ['#resolvedContribs']: contribsFromTrack, + }) => + (empty(contribsFromTrack) + ? continuation() + : continuation.raiseOutput({ + ['#hasUniqueCoverArt']: true, + })), + }, + + withPropertyFromAlbum({ + property: input.value('trackCoverArtistContribs'), + }), + + { + dependencies: ['#album.trackCoverArtistContribs'], + compute: (continuation, { + ['#album.trackCoverArtistContribs']: contribsFromAlbum, + }) => + continuation.raiseOutput({ + ['#hasUniqueCoverArt']: + !empty(contribsFromAlbum), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withOriginalRelease.js b/src/data/composite/things/track/withOriginalRelease.js new file mode 100644 index 00000000..d2ee39df --- /dev/null +++ b/src/data/composite/things/track/withOriginalRelease.js @@ -0,0 +1,59 @@ +// Just includes the original release of this track as a dependency. +// If this track isn't a rerelease, then it'll provide null, unless the +// {selfIfOriginal} option is set, in which case it'll provide this track +// itself. Note that this will early exit if the original release is +// specified by reference and that reference doesn't resolve to anything. +// Outputs to '#originalRelease' by default. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {validateWikiData} from '#validators'; + +import {withResolvedReference} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withOriginalRelease`, + + inputs: { + selfIfOriginal: input({type: 'boolean', defaultValue: false}), + + data: input({ + validate: validateWikiData({referenceType: 'track'}), + defaultDependency: 'trackData', + }), + }, + + outputs: ['#originalRelease'], + + steps: () => [ + withResolvedReference({ + ref: 'originalReleaseTrack', + data: input('data'), + find: input.value(find.track), + notFoundMode: input.value('exit'), + }).outputs({ + ['#resolvedReference']: '#originalRelease', + }), + + { + dependencies: [ + input.myself(), + input('selfIfOriginal'), + '#originalRelease', + ], + + compute: (continuation, { + [input.myself()]: track, + [input('selfIfOriginal')]: selfIfOriginal, + ['#originalRelease']: originalRelease, + }) => + continuation({ + ['#originalRelease']: + (originalRelease ?? + (selfIfOriginal + ? track + : null)), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js new file mode 100644 index 00000000..84420cf8 --- /dev/null +++ b/src/data/composite/things/track/withOtherReleases.js @@ -0,0 +1,40 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `withOtherReleases`, + + outputs: ['#otherReleases'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + mode: input.value('empty'), + }), + + withOriginalRelease({ + selfIfOriginal: input.value(true), + }), + + { + dependencies: [input.myself(), '#originalRelease', 'trackData'], + compute: (continuation, { + [input.myself()]: thisTrack, + ['#originalRelease']: originalRelease, + trackData, + }) => continuation({ + ['#otherReleases']: + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withPropertyFromAlbum.js b/src/data/composite/things/track/withPropertyFromAlbum.js new file mode 100644 index 00000000..b236a6e8 --- /dev/null +++ b/src/data/composite/things/track/withPropertyFromAlbum.js @@ -0,0 +1,49 @@ +// Gets a single property from this track's album, providing it as the same +// property name prefixed with '#album.' (by default). If the track's album +// isn't available, then by default, the property will be provided as null; +// set {notFoundMode: 'exit'} to early exit instead. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {withPropertyFromObject} from '#composite/data'; + +import withAlbum from './withAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withPropertyFromAlbum`, + + inputs: { + property: input.staticValue({type: 'string'}), + + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ({ + [input.staticValue('property')]: property, + }) => ['#album.' + property], + + steps: () => [ + withAlbum({ + notFoundMode: input('notFoundMode'), + }), + + withPropertyFromObject({ + object: '#album', + property: input('property'), + }), + + { + dependencies: ['#value', input.staticValue('property')], + compute: (continuation, { + ['#value']: value, + [input.staticValue('property')]: property, + }) => continuation({ + ['#album.' + property]: value, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/exitWithoutContribs.js b/src/data/composite/wiki-data/exitWithoutContribs.js new file mode 100644 index 00000000..2c8219fc --- /dev/null +++ b/src/data/composite/wiki-data/exitWithoutContribs.js @@ -0,0 +1,47 @@ +// Shorthand for exiting if the contribution list (usually a property's update +// value) resolves to empty - ensuring that the later computed results are only +// returned if these contributions are present. + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; + +import withResolvedContribs from './withResolvedContribs.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutContribs`, + + inputs: { + contribs: input({ + validate: isContributionList, + acceptsNull: true, + }), + + value: input({defaultValue: null}), + }, + + steps: () => [ + withResolvedContribs({ + from: input('contribs'), + }), + + // TODO: Fairly certain exitWithoutDependency would be sufficient here. + + withResultOfAvailabilityCheck({ + from: '#resolvedContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js new file mode 100644 index 00000000..1d0400fc --- /dev/null +++ b/src/data/composite/wiki-data/index.js @@ -0,0 +1,7 @@ +export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as inputThingClass} from './inputThingClass.js'; +export {default as inputWikiData} from './inputWikiData.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 withReverseReferenceList} from './withReverseReferenceList.js'; diff --git a/src/data/composite/wiki-data/inputThingClass.js b/src/data/composite/wiki-data/inputThingClass.js new file mode 100644 index 00000000..d70480e6 --- /dev/null +++ b/src/data/composite/wiki-data/inputThingClass.js @@ -0,0 +1,23 @@ +// Please note that this input, used in a variety of #composite/wiki-data +// utilities, is basically always a kludge. Any usage of it depends on +// referencing Thing class values defined outside of the #composite folder. + +import {input} from '#composite'; +import {isType} from '#validators'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default function inputThingClass() { + return input.staticValue({ + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, + }); +} diff --git a/src/data/composite/wiki-data/inputWikiData.js b/src/data/composite/wiki-data/inputWikiData.js new file mode 100644 index 00000000..cf7a7c2c --- /dev/null +++ b/src/data/composite/wiki-data/inputWikiData.js @@ -0,0 +1,17 @@ +import {input} from '#composite'; +import {validateWikiData} from '#validators'; + +// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] +// value because classes aren't initialized by when templateCompositeFrom gets +// called (see: circular imports). So the reference types have to be hard-coded, +// which somewhat defeats the point of storing them on the class in the first +// place... +export default function inputWikiData({ + referenceType = '', + allowMixedTypes = false, +} = {}) { + return input({ + validate: validateWikiData({referenceType, allowMixedTypes}), + acceptsNull: true, + }); +} diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js new file mode 100644 index 00000000..eda24160 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -0,0 +1,77 @@ +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {stitchArrays} from '#sugar'; +import {is, isContributionList} from '#validators'; +import {filterMultipleArrays} from '#wiki-data'; + +import { + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import { + withPropertiesFromList, +} from '#composite/data'; + +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedContribs`, + + inputs: { + from: input({ + validate: isContributionList, + acceptsNull: true, + }), + + notFoundMode: input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#resolvedContribs'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('from'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedContribs']: [], + }), + }), + + withPropertiesFromList({ + list: input('from'), + properties: input.value(['who', 'what']), + prefix: input.value('#contribs'), + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + find: input.value(find.artist), + notFoundMode: input('notFoundMode'), + }).outputs({ + ['#resolvedReferenceList']: '#contribs.who', + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + + compute(continuation, { + ['#contribs.who']: who, + ['#contribs.what']: what, + }) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + ['#resolvedContribs']: stitchArrays({who, what}), + }); + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js new file mode 100644 index 00000000..0fa5c554 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedReference.js @@ -0,0 +1,73 @@ +// Resolves a reference by using the provided find function to match it +// within the provided thingData dependency. This will early exit if the +// data dependency is null, or, if notFoundMode is set to 'exit', if the find +// function doesn't match anything for the reference. Otherwise, the data +// object is provided on the output dependency; or null, if the reference +// doesn't match anything or itself was null to begin with. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedReference`, + + inputs: { + ref: input({type: 'string', acceptsNull: true}), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + + notFoundMode: input({ + validate: is('null', 'exit'), + defaultValue: 'null', + }), + }, + + outputs: ['#resolvedReference'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('ref'), + output: input.value({ + ['#resolvedReference']: null, + }), + }), + + exitWithoutDependency({ + dependency: input('data'), + }), + + { + dependencies: [ + input('ref'), + input('data'), + input('find'), + input('notFoundMode'), + ], + + compute(continuation, { + [input('ref')]: ref, + [input('data')]: data, + [input('find')]: findFunction, + [input('notFoundMode')]: notFoundMode, + }) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raiseOutput({ + ['#resolvedReference']: match ?? null, + }); + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js new file mode 100644 index 00000000..1d39e5b2 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedReferenceList.js @@ -0,0 +1,101 @@ +// Resolves a list of references, with each reference matched with provided +// data in the same way as withResolvedReference. This will early exit if the +// data dependency is null (even if the reference list is empty). By default +// it will filter out references which don't match, but this can be changed +// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). + +import {input, templateCompositeFrom} from '#composite'; +import {is, isString, validateArrayItems} from '#validators'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedReferenceList`, + + inputs: { + list: input({ + validate: validateArrayItems(isString), + acceptsNull: true, + }), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + + notFoundMode: input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'filter', + }), + }, + + outputs: ['#resolvedReferenceList'], + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedReferenceList']: [], + }), + }), + + { + dependencies: [input('list'), input('data'), input('find')], + compute: (continuation, { + [input('list')]: list, + [input('data')]: data, + [input('find')]: findFunction, + }) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, + + { + dependencies: ['#matches'], + compute: (continuation, {'#matches': matches}) => + (matches.every(match => match) + ? continuation.raiseOutput({ + ['#resolvedReferenceList']: matches, + }) + : continuation()), + }, + + { + dependencies: ['#matches', input('notFoundMode')], + compute(continuation, { + ['#matches']: matches, + [input('notFoundMode')]: notFoundMode, + }) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.filter(match => match), + }); + + case 'null': + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.map(match => match ?? null), + }); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js new file mode 100644 index 00000000..113a6c40 --- /dev/null +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -0,0 +1,40 @@ +// Check out the info on reverseReferenceList! +// This is its composable form. + +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withReverseReferenceList`, + + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, + + outputs: ['#reverseReferenceList'], + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + { + dependencies: [input.myself(), input('data'), input('list')], + + compute: (continuation, { + [input.myself()]: thisThing, + [input('data')]: data, + [input('list')]: refListProperty, + }) => + continuation({ + ['#reverseReferenceList']: + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-properties/additionalFiles.js b/src/data/composite/wiki-properties/additionalFiles.js new file mode 100644 index 00000000..6760527a --- /dev/null +++ b/src/data/composite/wiki-properties/additionalFiles.js @@ -0,0 +1,30 @@ +// 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/color.js b/src/data/composite/wiki-properties/color.js new file mode 100644 index 00000000..1bc9888b --- /dev/null +++ b/src/data/composite/wiki-properties/color.js @@ -0,0 +1,12 @@ +// A color! This'll be some CSS-ready value. + +import {isColor} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isColor}, + }; +} diff --git a/src/data/composite/wiki-properties/commentary.js b/src/data/composite/wiki-properties/commentary.js new file mode 100644 index 00000000..fbea9d5c --- /dev/null +++ b/src/data/composite/wiki-properties/commentary.js @@ -0,0 +1,12 @@ +// Artist commentary! Generally present on tracks and albums. + +import {isCommentary} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isCommentary}, + }; +} diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js new file mode 100644 index 00000000..52aeb868 --- /dev/null +++ b/src/data/composite/wiki-properties/commentatorArtists.js @@ -0,0 +1,55 @@ +// This one's kinda tricky: it parses artist "references" from the +// commentary content, and finds the matching artist for each reference. +// This is mostly useful for credits and listings on artist pages. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {unique} from '#sugar'; + +import {exitWithoutDependency} from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `commentatorArtists`, + + compose: false, + + steps: () => [ + exitWithoutDependency({ + dependency: 'commentary', + mode: input.value('falsy'), + value: input.value([]), + }), + + { + dependencies: ['commentary'], + compute: (continuation, {commentary}) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/<i>(?<who>.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), + }, + + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + find: input.value(find.artist), + }).outputs({ + '#resolvedReferenceList': '#artists', + }), + + { + flags: {expose: true}, + + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), + }, + }, + ], +}); diff --git a/src/data/composite/wiki-properties/contribsPresent.js b/src/data/composite/wiki-properties/contribsPresent.js new file mode 100644 index 00000000..24f302a5 --- /dev/null +++ b/src/data/composite/wiki-properties/contribsPresent.js @@ -0,0 +1,30 @@ +// Nice 'n simple shorthand for an exposed-only flag which is true when any +// contributions are present in the specified property. + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {exposeDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `contribsPresent`, + + compose: false, + + inputs: { + contribs: input.staticDependency({ + validate: isContributionList, + acceptsNull: true, + }), + }, + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('contribs'), + mode: input.value('empty'), + }), + + exposeDependency({dependency: '#availability'}), + ], +}); diff --git a/src/data/composite/wiki-properties/contributionList.js b/src/data/composite/wiki-properties/contributionList.js new file mode 100644 index 00000000..8fde2caa --- /dev/null +++ b/src/data/composite/wiki-properties/contributionList.js @@ -0,0 +1,35 @@ +// Strong 'n sturdy contribution list, rolling a list of references (provided +// as this property's update value) and the resolved results (as get exposed) +// into one property. Update value will look something like this: +// +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] +// +// ...typically as processed from YAML, spreadsheet, or elsewhere. +// Exposes as the same, but with the "who" replaced with matches found in +// artistData - which means this always depends on an `artistData` property +// also existing on this object! +// + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {exposeConstant, exposeDependencyOrContinue} from '#composite/control-flow'; +import {withResolvedContribs} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `contributionList`, + + compose: false, + + update: {validate: isContributionList}, + + steps: () => [ + withResolvedContribs({from: input.updateValue()}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({value: input.value([])}), + ], +}); diff --git a/src/data/composite/wiki-properties/dimensions.js b/src/data/composite/wiki-properties/dimensions.js new file mode 100644 index 00000000..57a01279 --- /dev/null +++ b/src/data/composite/wiki-properties/dimensions.js @@ -0,0 +1,13 @@ +// Plain ol' image dimensions. This is a two-item array of positive integers, +// corresponding to width and height respectively. + +import {isDimensions} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDimensions}, + }; +} diff --git a/src/data/composite/wiki-properties/directory.js b/src/data/composite/wiki-properties/directory.js new file mode 100644 index 00000000..0b2181c9 --- /dev/null +++ b/src/data/composite/wiki-properties/directory.js @@ -0,0 +1,23 @@ +// The all-encompassing "directory" property, used as the unique identifier for +// almost any data object. Also corresponds to a part of the URL which pages of +// such objects are visited at. + +import {isDirectory} from '#validators'; +import {getKebabCase} from '#wiki-data'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + expose: { + dependencies: ['name'], + transform(directory, {name}) { + if (directory === null && name === null) return null; + else if (directory === null) return getKebabCase(name); + else return directory; + }, + }, + }; +} diff --git a/src/data/composite/wiki-properties/duration.js b/src/data/composite/wiki-properties/duration.js new file mode 100644 index 00000000..827f282d --- /dev/null +++ b/src/data/composite/wiki-properties/duration.js @@ -0,0 +1,13 @@ +// Duration! This is a number of seconds, possibly floating point, always +// at minimum zero. + +import {isDuration} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDuration}, + }; +} diff --git a/src/data/composite/wiki-properties/externalFunction.js b/src/data/composite/wiki-properties/externalFunction.js new file mode 100644 index 00000000..c388da6c --- /dev/null +++ b/src/data/composite/wiki-properties/externalFunction.js @@ -0,0 +1,11 @@ +// External function. These should only be used as dependencies for other +// properties, so they're left unexposed. + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true}, + update: {validate: (t) => typeof t === 'function'}, + }; +} diff --git a/src/data/composite/wiki-properties/fileExtension.js b/src/data/composite/wiki-properties/fileExtension.js new file mode 100644 index 00000000..c926fa8b --- /dev/null +++ b/src/data/composite/wiki-properties/fileExtension.js @@ -0,0 +1,13 @@ +// A file extension! Or the default, if provided when calling this. + +import {isFileExtension} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function(defaultFileExtension = null) { + return { + flags: {update: true, expose: true}, + update: {validate: isFileExtension}, + expose: {transform: (value) => value ?? defaultFileExtension}, + }; +} diff --git a/src/data/composite/wiki-properties/flag.js b/src/data/composite/wiki-properties/flag.js new file mode 100644 index 00000000..076e663f --- /dev/null +++ b/src/data/composite/wiki-properties/flag.js @@ -0,0 +1,19 @@ +// Straightforward flag descriptor for a variety of property purposes. +// Provide a default value, true or false! + +import {isBoolean} from '#validators'; + +// TODO: Not templateCompositeFrom. + +// TODO: The description is a lie. This defaults to false. Bad. + +export default function(defaultValue = false) { + if (typeof defaultValue !== 'boolean') { + throw new TypeError(`Always set explicit defaults for flags!`); + } + + return { + flags: {update: true, expose: true}, + update: {validate: isBoolean, default: defaultValue}, + }; +} diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js new file mode 100644 index 00000000..2462b047 --- /dev/null +++ b/src/data/composite/wiki-properties/index.js @@ -0,0 +1,20 @@ +export {default as additionalFiles} from './additionalFiles.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 contribsPresent} from './contribsPresent.js'; +export {default as contributionList} from './contributionList.js'; +export {default as dimensions} from './dimensions.js'; +export {default as directory} from './directory.js'; +export {default as duration} from './duration.js'; +export {default as externalFunction} from './externalFunction.js'; +export {default as fileExtension} from './fileExtension.js'; +export {default as flag} from './flag.js'; +export {default as name} from './name.js'; +export {default as referenceList} from './referenceList.js'; +export {default as reverseReferenceList} from './reverseReferenceList.js'; +export {default as simpleDate} from './simpleDate.js'; +export {default as simpleString} from './simpleString.js'; +export {default as singleReference} from './singleReference.js'; +export {default as urls} from './urls.js'; +export {default as wikiData} from './wikiData.js'; diff --git a/src/data/composite/wiki-properties/name.js b/src/data/composite/wiki-properties/name.js new file mode 100644 index 00000000..5146488b --- /dev/null +++ b/src/data/composite/wiki-properties/name.js @@ -0,0 +1,11 @@ +// A wiki data object's name! Its directory (i.e. unique identifier) will be +// computed based on this value if not otherwise specified. + +import {isName} from '#validators'; + +export default function(defaultName) { + return { + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName}, + }; +} diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js new file mode 100644 index 00000000..f5b6c58e --- /dev/null +++ b/src/data/composite/wiki-properties/referenceList.js @@ -0,0 +1,47 @@ +// Stores and exposes a list of references to other data objects; all items +// must be references to the same type, which is specified on the class input. +// +// See also: +// - singleReference +// - withResolvedReferenceList +// + +import {input, templateCompositeFrom} from '#composite'; +import {validateReferenceList} from '#validators'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputThingClass, inputWikiData, withResolvedReferenceList} + from '#composite/wiki-data'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default templateCompositeFrom({ + annotation: `referenceList`, + + compose: false, + + inputs: { + class: inputThingClass(), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; + }, + + steps: () => [ + withResolvedReferenceList({ + list: input.updateValue(), + data: input('data'), + find: input('find'), + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ], +}); diff --git a/src/data/composite/wiki-properties/reverseReferenceList.js b/src/data/composite/wiki-properties/reverseReferenceList.js new file mode 100644 index 00000000..84ba67df --- /dev/null +++ b/src/data/composite/wiki-properties/reverseReferenceList.js @@ -0,0 +1,30 @@ +// Neat little shortcut for "reversing" the reference lists stored on other +// things - for example, tracks specify a "referenced tracks" property, and +// you would use this to compute a corresponding "referenced *by* tracks" +// property. Naturally, the passed ref list property is of the things in the +// wiki data provided, not the requesting Thing itself. + +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputWikiData, withReverseReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `reverseReferenceList`, + + compose: false, + + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseReferenceList({ + data: input('data'), + list: input('list'), + }), + + exposeDependency({dependency: '#reverseReferenceList'}), + ], +}); diff --git a/src/data/composite/wiki-properties/simpleDate.js b/src/data/composite/wiki-properties/simpleDate.js new file mode 100644 index 00000000..f08d8323 --- /dev/null +++ b/src/data/composite/wiki-properties/simpleDate.js @@ -0,0 +1,14 @@ +// General date type, used as the descriptor for a bunch of properties. +// This isn't dynamic though - it won't inherit from a date stored on +// another object, for example. + +import {isDate} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDate}, + }; +} diff --git a/src/data/composite/wiki-properties/simpleString.js b/src/data/composite/wiki-properties/simpleString.js new file mode 100644 index 00000000..18d65146 --- /dev/null +++ b/src/data/composite/wiki-properties/simpleString.js @@ -0,0 +1,14 @@ +// General string type. This should probably generally be avoided in favor +// of more specific validation, but using it makes it easy to find where we +// might want to improve later, and it's a useful shorthand meanwhile. + +import {isString} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isString}, + }; +} diff --git a/src/data/composite/wiki-properties/singleReference.js b/src/data/composite/wiki-properties/singleReference.js new file mode 100644 index 00000000..34bd2e6d --- /dev/null +++ b/src/data/composite/wiki-properties/singleReference.js @@ -0,0 +1,47 @@ +// Stores and exposes one connection, or reference, to another data object. +// The reference must be to a specific type, which is specified on the class +// input. +// +// See also: +// - referenceList +// - withResolvedReference +// + +import {input, templateCompositeFrom} from '#composite'; +import {validateReference} from '#validators'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputThingClass, inputWikiData, withResolvedReference} + from '#composite/wiki-data'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default templateCompositeFrom({ + annotation: `singleReference`, + + compose: false, + + inputs: { + class: inputThingClass(), + find: input({type: 'function'}), + data: inputWikiData({allowMixedTypes: false}), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; + }, + + steps: () => [ + withResolvedReference({ + ref: input.updateValue(), + data: input('data'), + find: input('find'), + }), + + exposeDependency({dependency: '#resolvedReference'}), + ], +}); diff --git a/src/data/composite/wiki-properties/urls.js b/src/data/composite/wiki-properties/urls.js new file mode 100644 index 00000000..3160a0bf --- /dev/null +++ b/src/data/composite/wiki-properties/urls.js @@ -0,0 +1,14 @@ +// A list of URLs! This will always be present on the data object, even if set +// to an empty array or null. + +import {isURL, validateArrayItems} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)}, + expose: {transform: value => value ?? []}, + }; +} diff --git a/src/data/composite/wiki-properties/wikiData.js b/src/data/composite/wiki-properties/wikiData.js new file mode 100644 index 00000000..4ea47785 --- /dev/null +++ b/src/data/composite/wiki-properties/wikiData.js @@ -0,0 +1,17 @@ +// General purpose wiki data constructor, for properties like artistData, +// trackData, etc. + +import {validateArrayItems, validateInstanceOf} from '#validators'; + +// TODO: Not templateCompositeFrom. + +// TODO: This should validate with validateWikiData. + +export default function(thingClass) { + return { + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)), + }, + }; +} diff --git a/src/data/things/album.js b/src/data/things/album.js index fd8a71d3..e3ac1651 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,21 +1,12 @@ +import {input} from '#composite'; import find from '#find'; -import {empty, stitchArrays} from '#sugar'; -import {isDate, isTrackSectionList} from '#validators'; -import {filterMultipleArrays} from '#wiki-data'; +import {isDate} from '#validators'; + +import {exposeDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {exitWithoutContribs} from '#composite/wiki-data'; import { - exitWithoutDependency, - exitWithoutUpdateValue, - exposeDependency, - exposeUpdateValueOrContinue, - input, - fillMissingListItems, - withFlattenedList, - withPropertiesFromList, - withUnflattenedList, -} from '#composite'; - -import Thing, { additionalFiles, commentary, color, @@ -24,7 +15,6 @@ import Thing, { contributionList, dimensions, directory, - exitWithoutContribs, fileExtension, flag, name, @@ -33,8 +23,14 @@ import Thing, { simpleString, urls, wikiData, - withResolvedReferenceList, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import { + withTracks, + withTrackSections, +} from '#composite/things/album'; + +import Thing from './thing.js'; export class Album extends Thing { static [Thing.referenceType] = 'album'; @@ -101,100 +97,8 @@ export class Album extends Thing { additionalFiles: additionalFiles(), trackSections: [ - exitWithoutDependency({ - dependency: 'trackData', - value: input.value([]), - }), - - exitWithoutUpdateValue({ - mode: input.value('empty'), - value: input.value([]), - }), - - withPropertiesFromList({ - list: input.updateValue(), - prefix: input.value('#sections'), - properties: input.value([ - 'tracks', - 'dateOriginallyReleased', - 'isDefaultTrackSection', - 'color', - ]), - }), - - fillMissingListItems({ - list: '#sections.tracks', - fill: input.value([]), - }), - - fillMissingListItems({ - list: '#sections.isDefaultTrackSection', - fill: input.value(false), - }), - - fillMissingListItems({ - list: '#sections.color', - fill: input.dependency('color'), - }), - - withFlattenedList({ - list: '#sections.tracks', - }).outputs({ - ['#flattenedList']: '#trackRefs', - ['#flattenedIndices']: '#sections.startIndex', - }), - - withResolvedReferenceList({ - list: '#trackRefs', - data: 'trackData', - notFoundMode: input.value('null'), - find: input.value(find.track), - }).outputs({ - ['#resolvedReferenceList']: '#tracks', - }), - - withUnflattenedList({ - list: '#tracks', - indices: '#sections.startIndex', - }).outputs({ - ['#unflattenedList']: '#sections.tracks', - }), - - { - flags: {update: true, expose: true}, - - update: {validate: isTrackSectionList}, - - expose: { - dependencies: [ - '#sections.tracks', - '#sections.color', - '#sections.dateOriginallyReleased', - '#sections.isDefaultTrackSection', - '#sections.startIndex', - ], - - transform(trackSections, { - '#sections.tracks': tracks, - '#sections.color': color, - '#sections.dateOriginallyReleased': dateOriginallyReleased, - '#sections.isDefaultTrackSection': isDefaultTrackSection, - '#sections.startIndex': startIndex, - }) { - filterMultipleArrays( - tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, - tracks => !empty(tracks)); - - return stitchArrays({ - tracks, - color, - dateOriginallyReleased, - isDefaultTrackSection, - startIndex, - }); - } - }, - }, + withTrackSections(), + exposeDependency({dependency: '#trackSections'}), ], artistContribs: contributionList(), @@ -231,33 +135,8 @@ export class Album extends Thing { hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), tracks: [ - exitWithoutDependency({ - dependency: 'trackData', - value: input.value([]), - }), - - exitWithoutDependency({ - dependency: 'trackSections', - mode: input.value('empty'), - value: input.value([]), - }), - - { - dependencies: ['trackSections'], - compute: (continuation, {trackSections}) => - continuation({ - '#trackRefs': trackSections - .flatMap(section => section.tracks ?? []), - }), - }, - - withResolvedReferenceList({ - list: '#trackRefs', - data: 'trackData', - find: input.value(find.track), - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), + withTracks(), + exposeDependency({dependency: '#tracks'}), ], }); diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index ba3cbd0d..1266a4e0 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,14 +1,18 @@ -import {exposeUpdateValueOrContinue, input} from '#composite'; +import {input} from '#composite'; import {sortAlbumsTracksChronologically} from '#wiki-data'; import {isName} from '#validators'; -import Thing, { +import {exposeUpdateValueOrContinue} from '#composite/control-flow'; + +import { color, directory, flag, name, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 085e5663..ff9f8aee 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -2,7 +2,7 @@ import {input} from '#composite'; import find from '#find'; import {isName, validateArrayItems} from '#validators'; -import Thing, { +import { directory, fileExtension, flag, @@ -11,7 +11,9 @@ import Thing, { singleReference, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Artist extends Thing { static [Thing.referenceType] = 'artist'; diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c03f8833..7e068dce 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -2,14 +2,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; - -import { - a, - is, - isString, - isWholeNumber, - validateArrayItems, -} from '#validators'; +import {a} from '#validators'; import { decorateErrorWithIndex, @@ -1639,721 +1632,3 @@ export function debugComposite(fn) { compositeFrom.debug = false; return value; } - -// Exposes a dependency exactly as it is; this is typically the base of a -// composition which was created to serve as one property's descriptor. -// -// Please note that this *doesn't* verify that the dependency exists, so -// if you provide the wrong name or it hasn't been set by a previous -// compositional step, the property will be exposed as undefined instead -// of null. -// -export const exposeDependency = templateCompositeFrom({ - annotation: `exposeDependency`, - - compose: false, - - inputs: { - dependency: input.staticDependency({acceptsNull: true}), - }, - - steps: () => [ - { - dependencies: [input('dependency')], - compute: ({ - [input('dependency')]: dependency - }) => dependency, - }, - ], -}); - -// Exposes a constant value exactly as it is; like exposeDependency, this -// is typically the base of a composition serving as a particular property -// descriptor. It generally follows steps which will conditionally early -// exit with some other value, with the exposeConstant base serving as the -// fallback default value. -export const exposeConstant = templateCompositeFrom({ - annotation: `exposeConstant`, - - compose: false, - - inputs: { - value: input.staticValue(), - }, - - steps: () => [ - { - dependencies: [input('value')], - compute: ({ - [input('value')]: value, - }) => value, - }, - ], -}); - -// Checks the availability of a dependency and provides the result to later -// steps under '#availability' (by default). This is mainly intended for use -// by the more specific utilities, which you should consider using instead. -// Customize {mode} to select one of these modes, or default to 'null': -// -// * 'null': Check that the value isn't null (and not undefined either). -// * 'empty': Check that the value is neither null, undefined, nor an empty -// array. -// * 'falsy': Check that the value isn't false when treated as a boolean -// (nor an empty array). Keep in mind this will also be false -// for values like zero and the empty string! -// - -const inputAvailabilityCheckMode = () => input({ - validate: is('null', 'empty', 'falsy'), - defaultValue: 'null', -}); - -export const withResultOfAvailabilityCheck = templateCompositeFrom({ - annotation: `withResultOfAvailabilityCheck`, - - inputs: { - from: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - }, - - outputs: ['#availability'], - - steps: () => [ - { - dependencies: [input('from'), input('mode')], - - compute: (continuation, { - [input('from')]: value, - [input('mode')]: mode, - }) => { - let availability; - - switch (mode) { - case 'null': - availability = value !== undefined && value !== null; - break; - - case 'empty': - availability = value !== undefined && !empty(value); - break; - - case 'falsy': - availability = !!value && (!Array.isArray(value) || !empty(value)); - break; - } - - return continuation({'#availability': availability}); - }, - }, - ], -}); - -// Exposes a dependency as it is, or continues if it's unavailable. -// See withResultOfAvailabilityCheck for {mode} options! -export const exposeDependencyOrContinue = templateCompositeFrom({ - annotation: `exposeDependencyOrContinue`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('dependency')], - compute: (continuation, { - ['#availability']: availability, - [input('dependency')]: dependency, - }) => - (availability - ? continuation.exit(dependency) - : continuation()), - }, - ], -}); - -// Exposes the update value of an {update: true} property as it is, -// or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! Also provide {validate} here to conveniently -// set a custom validation check for this property's update value. -export const exposeUpdateValueOrContinue = templateCompositeFrom({ - annotation: `exposeUpdateValueOrContinue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - - validate: input({ - type: 'function', - defaultValue: null, - }), - }, - - update: ({ - [input.staticValue('validate')]: validate, - }) => - (validate - ? {validate} - : {}), - - steps: () => [ - exposeDependencyOrContinue({ - dependency: input.updateValue(), - mode: input('mode'), - }), - ], -}); - -// Early exits if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const exitWithoutDependency = templateCompositeFrom({ - annotation: `exitWithoutDependency`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - value: input({defaultValue: null}), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('value')], - compute: (continuation, { - ['#availability']: availability, - [input('value')]: value, - }) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], -}); - -// Early exits if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const exitWithoutUpdateValue = templateCompositeFrom({ - annotation: `exitWithoutUpdateValue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - value: input({defaultValue: null}), - }, - - steps: () => [ - exitWithoutDependency({ - dependency: input.updateValue(), - mode: input('mode'), - value: input('value'), - }), - ], -}); - -// Raises if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const raiseOutputWithoutDependency = templateCompositeFrom({ - annotation: `raiseOutputWithoutDependency`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - output: input.staticValue({defaultValue: {}}), - }, - - outputs: ({ - [input.staticValue('output')]: output, - }) => Object.keys(output), - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('output')], - compute: (continuation, { - ['#availability']: availability, - [input('output')]: output, - }) => - (availability - ? continuation() - : continuation.raiseOutputAbove(output)), - }, - ], -}); - -// Raises if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ - annotation: `raiseOutputWithoutUpdateValue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - output: input.staticValue({defaultValue: {}}), - }, - - outputs: ({ - [input.staticValue('output')]: output, - }) => Object.keys(output), - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input.updateValue(), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('output')], - compute: (continuation, { - ['#availability']: availability, - [input('output')]: output, - }) => - (availability - ? continuation() - : continuation.raiseOutputAbove(output)), - }, - ], -}); - -// Gets a property of some object (in a dependency) and provides that value. -// If the object itself is null, or the object doesn't have the listed property, -// the provided dependency will also be null. -export const withPropertyFromObject = templateCompositeFrom({ - annotation: `withPropertyFromObject`, - - inputs: { - object: input({type: 'object', acceptsNull: true}), - property: input({type: 'string'}), - }, - - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object && property - ? (object.startsWith('#') - ? [`${object}.${property}`] - : [`#${object}.${property}`]) - : ['#value']), - - steps: () => [ - { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => continuation({ - '#output': - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - }), - }, - - { - dependencies: [ - '#output', - input('object'), - input('property'), - ], - - compute: (continuation, { - ['#output']: output, - [input('object')]: object, - [input('property')]: property, - }) => continuation({ - [output]: - (object === null - ? null - : object[property] ?? null), - }), - }, - ], -}); - -// Gets the listed properties from some object, providing each property's value -// as a dependency prefixed with the same name as the object (by default). -// If the object itself is null, all provided dependencies will be null; -// if it's missing only select properties, those will be provided as null. -export const withPropertiesFromObject = templateCompositeFrom({ - annotation: `withPropertiesFromObject`, - - inputs: { - object: input({type: 'object', acceptsNull: true}), - - properties: input({ - type: 'array', - validate: validateArrayItems(isString), - }), - - prefix: input.staticValue({type: 'string', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - }) => - (properties - ? properties.map(property => - (prefix - ? `${prefix}.${property}` - : object - ? `${object}.${property}` - : `#object.${property}`)) - : ['#object']), - - steps: () => [ - { - dependencies: [input('object'), input('properties')], - compute: (continuation, { - [input('object')]: object, - [input('properties')]: properties, - }) => continuation({ - ['#entries']: - (object === null - ? properties.map(property => [property, null]) - : properties.map(property => [property, object[property]])), - }), - }, - - { - dependencies: [ - input.staticDependency('object'), - input.staticValue('properties'), - input.staticValue('prefix'), - '#entries', - ], - - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - ['#entries']: entries, - }) => - (properties - ? continuation( - Object.fromEntries( - entries.map(([property, value]) => [ - (prefix - ? `${prefix}.${property}` - : object - ? `${object}.${property}` - : `#object.${property}`), - value ?? null, - ]))) - : continuation({ - ['#object']: - Object.fromEntries(entries), - })), - }, - ], -}); - -// Gets a property from each of a list of objects (in a dependency) and -// provides the results. This doesn't alter any list indices, so positions -// which were null in the original list are kept null here. Objects which don't -// have the specified property are retained in-place as null. -export function withPropertyFromList({ - list, - property, - into = null, -}) { - into ??= - (list.startsWith('#') - ? `${list}.${property}` - : `#${list}.${property}`); - - return { - annotation: `withPropertyFromList`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {property}, - - compute(continuation, {list, '#options': {property}}) { - if (list === undefined || empty(list)) { - return continuation({into: []}); - } - - return continuation({ - into: - list.map(item => - (item === null || item === undefined - ? null - : item[property] ?? null)), - }); - }, - }, - }; -} - -// Gets the listed properties from each of a list of objects, providing lists -// of property values each into a dependency prefixed with the same name as the -// list (by default). Like withPropertyFromList, this doesn't alter indices. -export const withPropertiesFromList = templateCompositeFrom({ - annotation: `withPropertiesFromList`, - - inputs: { - list: input({type: 'array'}), - - properties: input({ - validate: validateArrayItems(isString), - }), - - prefix: input.staticValue({type: 'string', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - }) => - (properties - ? properties.map(property => - (prefix - ? `${prefix}.${property}` - : list - ? `${list}.${property}` - : `#list.${property}`)) - : ['#lists']), - - steps: () => [ - { - dependencies: [input('list'), input('properties')], - compute: (continuation, { - [input('list')]: list, - [input('properties')]: properties, - }) => continuation({ - ['#lists']: - Object.fromEntries( - properties.map(property => [ - property, - list.map(item => item[property] ?? null), - ])), - }), - }, - - { - dependencies: [ - input.staticDependency('list'), - input.staticValue('properties'), - input.staticValue('prefix'), - '#lists', - ], - - compute: (continuation, { - [input.staticDependency('list')]: list, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - ['#lists']: lists, - }) => - (properties - ? continuation( - Object.fromEntries( - properties.map(property => [ - (prefix - ? `${prefix}.${property}` - : list - ? `${list}.${property}` - : `#list.${property}`), - lists[property], - ]))) - : continuation({'#lists': lists})), - }, - ], -}); - -// Replaces items of a list, which are null or undefined, with some fallback -// value. By default, this replaces the passed dependency. -export const fillMissingListItems = templateCompositeFrom({ - annotation: `fillMissingListItems`, - - inputs: { - list: input({type: 'array'}), - fill: input({acceptsNull: true}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - - steps: () => [ - { - dependencies: [input('list'), input('fill')], - compute: (continuation, { - [input('list')]: list, - [input('fill')]: fill, - }) => continuation({ - ['#filled']: - list.map(item => item ?? fill), - }), - }, - - { - dependencies: [input.staticDependency('list'), '#filled'], - compute: (continuation, { - [input.staticDependency('list')]: list, - ['#filled']: filled, - }) => continuation({ - [list ?? '#list']: - filled, - }), - }, - ], -}); - -// Filters particular values out of a list. Note that this will always -// completely skip over null, but can be used to filter out any other -// primitive or object value. -export const excludeFromList = templateCompositeFrom({ - annotation: `excludeFromList`, - - inputs: { - list: input(), - - item: input({defaultValue: null}), - items: input({type: 'array', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - - steps: () => [ - { - dependencies: [ - input.staticDependency('list'), - input('list'), - input('item'), - input('items'), - ], - - compute: (continuation, { - [input.staticDependency('list')]: listName, - [input('list')]: listContents, - [input('item')]: excludeItem, - [input('items')]: excludeItems, - }) => continuation({ - [listName ?? '#list']: - listContents.filter(item => { - if (excludeItem !== null && item === excludeItem) return false; - if (!empty(excludeItems) && excludeItems.includes(item)) return false; - return true; - }), - }), - }, - ], -}); - -// Flattens an array with one level of nested arrays, providing as dependencies -// both the flattened array as well as the original starting indices of each -// successive source array. -export const withFlattenedList = templateCompositeFrom({ - annotation: `withFlattenedList`, - - inputs: { - list: input({type: 'array'}), - }, - - outputs: ['#flattenedList', '#flattenedIndices'], - - steps: () => [ - { - dependencies: [input('list')], - compute(continuation, { - [input('list')]: sourceList, - }) { - const flattenedList = sourceList.flat(); - const indices = []; - let lastEndIndex = 0; - for (const {length} of sourceList) { - indices.push(lastEndIndex); - lastEndIndex += length; - } - - return continuation({ - ['#flattenedList']: flattenedList, - ['#flattenedIndices']: indices, - }); - }, - }, - ], -}); - -// After mapping the contents of a flattened array in-place (being careful to -// retain the original indices by replacing unmatched results with null instead -// of filtering them out), this function allows for recombining them. It will -// filter out null and undefined items by default (pass {filter: false} to -// disable this). -export const withUnflattenedList = templateCompositeFrom({ - annotation: `withUnflattenedList`, - - inputs: { - list: input({ - type: 'array', - defaultDependency: '#flattenedList', - }), - - indices: input({ - validate: validateArrayItems(isWholeNumber), - defaultDependency: '#flattenedIndices', - }), - - filter: input({ - type: 'boolean', - defaultValue: true, - }), - }, - - outputs: ['#unflattenedList'], - - steps: () => [ - { - dependencies: [input('list'), input('indices'), input('filter')], - compute(continuation, { - [input('list')]: list, - [input('indices')]: indices, - [input('filter')]: filter, - }) { - const unflattenedList = []; - - for (let i = 0; i < indices.length; i++) { - const startIndex = indices[i]; - const endIndex = - (i === indices.length - 1 - ? list.length - : indices[i + 1]); - - const values = list.slice(startIndex, endIndex); - unflattenedList.push( - (filter - ? values.filter(value => value !== null && value !== undefined) - : values)); - } - - return continuation({ - ['#unflattenedList']: unflattenedList, - }); - }, - }, - ], -}); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index c3f90260..8fb1edfa 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,7 +9,7 @@ import { oneOf, } from '#validators'; -import Thing, { +import { color, contributionList, fileExtension, @@ -19,7 +19,9 @@ import Thing, { simpleString, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; diff --git a/src/data/things/group.js b/src/data/things/group.js index 0b117801..d5ae03e7 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,7 +1,7 @@ import {input} from '#composite'; import find from '#find'; -import Thing, { +import { color, directory, name, @@ -9,7 +9,9 @@ import Thing, { simpleString, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Group extends Thing { static [Thing.referenceType] = 'group'; diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index bcf99e80..de9d0e50 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,10 +1,6 @@ +import {input} from '#composite'; import find from '#find'; -import { - exposeDependency, - input, -} from '#composite'; - import { is, isCountingNumber, @@ -16,14 +12,18 @@ import { validateReference, } from '#validators'; -import Thing, { +import {exposeDependency} from '#composite/control-flow'; +import {withResolvedReference} from '#composite/wiki-data'; + +import { color, name, referenceList, simpleString, wikiData, - withResolvedReference, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class HomepageLayout extends Thing { static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ diff --git a/src/data/things/language.js b/src/data/things/language.js index a325d6a6..fe74f7bf 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,13 +1,14 @@ import {Tag} from '#html'; import {isLanguageCode} from '#validators'; -import CacheableObject from './cacheable-object.js'; - -import Thing, { +import { externalFunction, flag, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import CacheableObject from './cacheable-object.js'; +import Thing from './thing.js'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index 6984874e..ba065c25 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,9 +1,11 @@ -import Thing, { +import { directory, name, simpleDate, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class NewsEntry extends Thing { static [Thing.referenceType] = 'news-entry'; diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index 0133e0b6..f03e4405 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -1,10 +1,12 @@ import {isName} from '#validators'; -import Thing, { +import { directory, name, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f1302e17..a47f8506 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1,48 +1,9 @@ -// Thing: base class for wiki data types, providing wiki-specific utility -// functions on top of essential CacheableObject behavior. +// Thing: base class for wiki data types, providing interfaces generally useful +// to all wiki data objects on top of foundational CacheableObject behavior. import {inspect} from 'node:util'; import {colors} from '#cli'; -import find from '#find'; -import {stitchArrays, unique} from '#sugar'; -import {filterMultipleArrays, getKebabCase} from '#wiki-data'; -import {is} from '#validators'; - -import { - compositeFrom, - exitWithoutDependency, - exposeConstant, - exposeDependency, - exposeDependencyOrContinue, - input, - raiseOutputWithoutDependency, - templateCompositeFrom, - withResultOfAvailabilityCheck, - withPropertiesFromList, -} from '#composite'; - -import { - isAdditionalFileList, - isBoolean, - isColor, - isCommentary, - isContributionList, - isDate, - isDimensions, - isDirectory, - isDuration, - isFileExtension, - isName, - isString, - isType, - isURL, - validateArrayItems, - validateInstanceOf, - validateReference, - validateReferenceList, - validateWikiData, -} from '#validators'; import CacheableObject from './cacheable-object.js'; @@ -77,673 +38,3 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } } - -// Property descriptor templates -// -// Regularly reused property descriptors, for ease of access and generally -// duplicating less code across wiki data types. These are specialized utility -// functions, so check each for how its own arguments behave! - -export function name(defaultName) { - return { - flags: {update: true, expose: true}, - update: {validate: isName, default: defaultName}, - }; -} - -export function color() { - return { - flags: {update: true, expose: true}, - update: {validate: isColor}, - }; -} - -export function directory() { - return { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: { - dependencies: ['name'], - transform(directory, {name}) { - if (directory === null && name === null) return null; - else if (directory === null) return getKebabCase(name); - else return directory; - }, - }, - }; -} - -export function urls() { - return { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, - expose: {transform: (value) => value ?? []}, - }; -} - -// A file extension! Or the default, if provided when calling this. -export function fileExtension(defaultFileExtension = null) { - return { - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: {transform: (value) => value ?? defaultFileExtension}, - }; -} - -// Plain ol' image dimensions. This is a two-item array of positive integers, -// corresponding to width and height respectively. -export function dimensions() { - return { - flags: {update: true, expose: true}, - update: {validate: isDimensions}, - }; -} - -// Duration! This is a number of seconds, possibly floating point, always -// at minimum zero. -export function duration() { - return { - flags: {update: true, expose: true}, - update: {validate: isDuration}, - }; -} - -// Straightforward flag descriptor for a variety of property purposes. -// Provide a default value, true or false! -export function flag(defaultValue = false) { - // TODO: ^ Are you actually kidding me - if (typeof defaultValue !== 'boolean') { - throw new TypeError(`Always set explicit defaults for flags!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: defaultValue}, - }; -} - -// General date type, used as the descriptor for a bunch of properties. -// This isn't dynamic though - it won't inherit from a date stored on -// another object, for example. -export function simpleDate() { - return { - flags: {update: true, expose: true}, - update: {validate: isDate}, - }; -} - -// General string type. This should probably generally be avoided in favor -// of more specific validation, but using it makes it easy to find where we -// might want to improve later, and it's a useful shorthand meanwhile. -export function simpleString() { - return { - flags: {update: true, expose: true}, - update: {validate: isString}, - }; -} - -// External function. These should only be used as dependencies for other -// properties, so they're left unexposed. -export function externalFunction() { - return { - flags: {update: true}, - update: {validate: (t) => typeof t === 'function'}, - }; -} - -// Strong 'n sturdy contribution list, rolling a list of references (provided -// as this property's update value) and the resolved results (as get exposed) -// into one property. Update value will look something like this: -// -// [ -// {who: 'Artist Name', what: 'Viola'}, -// {who: 'artist:john-cena', what: null}, -// ... -// ] -// -// ...typically as processed from YAML, spreadsheet, or elsewhere. -// Exposes as the same, but with the "who" replaced with matches found in -// artistData - which means this always depends on an `artistData` property -// also existing on this object! -// -export function contributionList() { - return compositeFrom({ - annotation: `contributionList`, - - compose: false, - - update: {validate: isContributionList}, - - steps: [ - withResolvedContribs({from: input.updateValue()}), - exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({value: input.value([])}), - ], - }); -} - -// Artist commentary! Generally present on tracks and albums. -export function commentary() { - return { - flags: {update: true, expose: true}, - update: {validate: isCommentary}, - }; -} - -// This is a somewhat more involved data structure - it's for additional -// or "bonus" files associated with albums or tracks (or anything else). -// It's got this form: -// -// [ -// {title: 'Booklet', files: ['Booklet.pdf']}, -// { -// title: 'Wallpaper', -// description: 'Cool Wallpaper!', -// files: ['1440x900.png', '1920x1080.png'] -// }, -// {title: 'Alternate Covers', description: null, files: [...]}, -// ... -// ] -// -export function additionalFiles() { - return { - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }; -} - -const thingClassInput = { - validate(thingClass) { - isType(thingClass, 'function'); - - if (!Object.hasOwn(thingClass, Thing.referenceType)) { - throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); - } - - return true; - }, -}; - -// A reference list! Keep in mind this is for general references to wiki -// objects of (usually) other Thing subclasses, not specifically leitmotif -// references in tracks (although that property uses referenceList too!). -// -// The underlying function validateReferenceList expects a string like -// 'artist' or 'track', but this utility keeps from having to hard-code the -// string in multiple places by referencing the value saved on the class -// instead. -export const referenceList = templateCompositeFrom({ - annotation: `referenceList`, - - compose: false, - - inputs: { - class: input.staticValue(thingClassInput), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - }, - - update: ({ - [input.staticValue('class')]: thingClass, - }) => { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReferenceList(referenceType)}; - }, - - steps: () => [ - withResolvedReferenceList({ - list: input.updateValue(), - data: input('data'), - find: input('find'), - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), - ], -}); - -// Corresponding function for a single reference. -export const singleReference = templateCompositeFrom({ - annotation: `singleReference`, - - compose: false, - - inputs: { - class: input(thingClassInput), - find: input({type: 'function'}), - data: inputWikiData({allowMixedTypes: false}), - }, - - update: ({ - [input.staticValue('class')]: thingClass, - }) => { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReference(referenceType)}; - }, - - steps: () => [ - withResolvedReference({ - ref: input.updateValue(), - data: input('data'), - find: input('find'), - }), - - exposeDependency({dependency: '#resolvedReference'}), - ], -}); - -// Nice 'n simple shorthand for an exposed-only flag which is true when any -// contributions are present in the specified property. -export const contribsPresent = templateCompositeFrom({ - annotation: `contribsPresent`, - - compose: false, - - inputs: { - contribs: input.staticDependency({ - validate: isContributionList, - acceptsNull: true, - }), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('contribs'), - mode: input.value('empty'), - }), - - exposeDependency({dependency: '#availability'}), - ], -}); - -// Neat little shortcut for "reversing" the reference lists stored on other -// things - for example, tracks specify a "referenced tracks" property, and -// you would use this to compute a corresponding "referenced *by* tracks" -// property. Naturally, the passed ref list property is of the things in the -// wiki data provided, not the requesting Thing itself. -export const reverseReferenceList = templateCompositeFrom({ - annotation: `reverseReferenceList`, - - compose: false, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - steps: () => [ - withReverseReferenceList({ - data: input('data'), - list: input('list'), - }), - - exposeDependency({dependency: '#reverseReferenceList'}), - ], -}); - -// General purpose wiki data constructor, for properties like artistData, -// trackData, etc. -export function wikiData(thingClass) { - return { - flags: {update: true}, - update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), - }, - }; -} - -// This one's kinda tricky: it parses artist "references" from the -// commentary content, and finds the matching artist for each reference. -// This is mostly useful for credits and listings on artist pages. -export const commentatorArtists = templateCompositeFrom({ - annotation: `commentatorArtists`, - - compose: false, - - steps: () => [ - exitWithoutDependency({ - dependency: 'commentary', - mode: input.value('falsy'), - value: input.value([]), - }), - - { - dependencies: ['commentary'], - compute: (continuation, {commentary}) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/<i>(?<who>.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), - }, - - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - find: input.value(find.artist), - }).outputs({ - '#resolvedReferenceList': '#artists', - }), - - { - flags: {expose: true}, - - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), - }, - }, - ], -}); - -// Compositional utilities - -// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] -// value because classes aren't initialized by when templateCompositeFrom gets -// called (see: circular imports). So the reference types have to be hard-coded, -// which somewhat defeats the point of storing them on the class in the first -// place... -export function inputWikiData({ - referenceType = '', - allowMixedTypes = false, -} = {}) { - return input({ - validate: validateWikiData({referenceType, allowMixedTypes}), - acceptsNull: true, - }); -} - -// Resolves the contribsByRef contained in the provided dependency, -// providing (named by the second argument) the result. "Resolving" -// means mapping the "who" reference of each contribution to an artist -// object, and filtering out those whose "who" doesn't match any artist. -export const withResolvedContribs = templateCompositeFrom({ - annotation: `withResolvedContribs`, - - inputs: { - from: input({ - validate: isContributionList, - acceptsNull: true, - }), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#resolvedContribs'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: input('from'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedContribs']: [], - }), - }), - - withPropertiesFromList({ - list: input('from'), - properties: input.value(['who', 'what']), - prefix: input.value('#contribs'), - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - find: input.value(find.artist), - notFoundMode: input('notFoundMode'), - }).outputs({ - ['#resolvedReferenceList']: '#contribs.who', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - - compute(continuation, { - ['#contribs.who']: who, - ['#contribs.what']: what, - }) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - ['#resolvedContribs']: stitchArrays({who, what}), - }); - }, - }, - ], -}); - -// Shorthand for exiting if the contribution list (usually a property's update -// value) resolves to empty - ensuring that the later computed results are only -// returned if these contributions are present. -export const exitWithoutContribs = templateCompositeFrom({ - annotation: `exitWithoutContribs`, - - inputs: { - contribs: input({ - validate: isContributionList, - acceptsNull: true, - }), - - value: input({defaultValue: null}), - }, - - steps: () => [ - withResolvedContribs({ - from: input('contribs'), - }), - - withResultOfAvailabilityCheck({ - from: '#resolvedContribs', - mode: input.value('empty'), - }), - - { - dependencies: ['#availability', input('value')], - compute: (continuation, { - ['#availability']: availability, - [input('value')]: value, - }) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], -}); - -// Resolves a reference by using the provided find function to match it -// within the provided thingData dependency. This will early exit if the -// data dependency is null, or, if notFoundMode is set to 'exit', if the find -// function doesn't match anything for the reference. Otherwise, the data -// object is provided on the output dependency; or null, if the reference -// doesn't match anything or itself was null to begin with. -export const withResolvedReference = templateCompositeFrom({ - annotation: `withResolvedReference`, - - inputs: { - ref: input({type: 'string', acceptsNull: true}), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - - notFoundMode: input({ - validate: is('null', 'exit'), - defaultValue: 'null', - }), - }, - - outputs: ['#resolvedReference'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: input('ref'), - output: input.value({ - ['#resolvedReference']: null, - }), - }), - - exitWithoutDependency({ - dependency: input('data'), - }), - - { - dependencies: [ - input('ref'), - input('data'), - input('find'), - input('notFoundMode'), - ], - - compute(continuation, { - [input('ref')]: ref, - [input('data')]: data, - [input('find')]: findFunction, - [input('notFoundMode')]: notFoundMode, - }) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } - - return continuation.raiseOutput({ - ['#resolvedReference']: match ?? null, - }); - }, - }, - ], -}); - -// Resolves a list of references, with each reference matched with provided -// data in the same way as withResolvedReference. This will early exit if the -// data dependency is null (even if the reference list is empty). By default -// it will filter out references which don't match, but this can be changed -// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). -export const withResolvedReferenceList = templateCompositeFrom({ - annotation: `withResolvedReferenceList`, - - inputs: { - list: input({ - validate: validateArrayItems(isString), - acceptsNull: true, - }), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'filter', - }), - }, - - outputs: ['#resolvedReferenceList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - raiseOutputWithoutDependency({ - dependency: input('list'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedReferenceList']: [], - }), - }), - - { - dependencies: [input('list'), input('data'), input('find')], - compute: (continuation, { - [input('list')]: list, - [input('data')]: data, - [input('find')]: findFunction, - }) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), - }, - - { - dependencies: ['#matches'], - compute: (continuation, {'#matches': matches}) => - (matches.every(match => match) - ? continuation.raiseOutput({ - ['#resolvedReferenceList']: matches, - }) - : continuation()), - }, - - { - dependencies: ['#matches', input('notFoundMode')], - compute(continuation, { - ['#matches']: matches, - [input('notFoundMode')]: notFoundMode, - }) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.filter(match => match), - }); - - case 'null': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.map(match => match ?? null), - }); - - default: - throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); - } - }, - }, - ], -}); - -// Check out the info on reverseReferenceList! -// This is its composable form. -export const withReverseReferenceList = templateCompositeFrom({ - annotation: `withReverseReferenceList`, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - outputs: ['#reverseReferenceList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - { - dependencies: [input.myself(), input('data'), input('list')], - - compute: (continuation, { - [input.myself()]: thisThing, - [input('data')]: data, - [input('list')]: refListProperty, - }) => - continuation({ - ['#reverseReferenceList']: - data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ], -}); diff --git a/src/data/things/track.js b/src/data/things/track.js index c77bf889..193ad891 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -1,35 +1,28 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {input} from '#composite'; import find from '#find'; -import {empty} from '#sugar'; import { - exitWithoutDependency, - excludeFromList, - exposeConstant, - exposeDependency, - exposeDependencyOrContinue, - exposeUpdateValueOrContinue, - input, - raiseOutputWithoutDependency, - templateCompositeFrom, - withPropertyFromObject, -} from '#composite'; - -import { - is, - isBoolean, isColor, isContributionList, isDate, isFileExtension, - validateWikiData, } from '#validators'; -import CacheableObject from './cacheable-object.js'; +import {withPropertyFromObject} from '#composite/data'; +import {withResolvedContribs} from '#composite/wiki-data'; + +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; -import Thing, { +import { additionalFiles, commentary, commentatorArtists, @@ -45,10 +38,22 @@ import Thing, { simpleString, urls, wikiData, - withResolvedContribs, - withResolvedReference, - withReverseReferenceList, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import { + exitWithoutUniqueCoverArt, + inheritFromOriginalRelease, + trackReverseReferenceList, + withAlbum, + withAlwaysReferenceByDirectory, + withContainingTrackSection, + withHasUniqueCoverArt, + withOtherReleases, + withPropertyFromAlbum, +} from '#composite/things/track'; + +import CacheableObject from './cacheable-object.js'; +import Thing from './thing.js'; export class Track extends Thing { static [Thing.referenceType] = 'track'; @@ -84,39 +89,9 @@ export class Track extends Thing { exposeDependency({dependency: '#album.color'}), ], - // 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. alwaysReferenceByDirectory: [ - exposeUpdateValueOrContinue({ - validate: input.value(isBoolean), - }), - - excludeFromList({ - list: 'trackData', - item: input.myself(), - }), - - withOriginalRelease({ - data: '#trackData', - }), - - exitWithoutDependency({ - dependency: '#originalRelease', - value: input.value(false), - }), - - withPropertyFromObject({ - object: '#originalRelease', - property: input.value('name'), - }), - - { - dependencies: ['name', '#originalRelease.name'], - compute: ({name, '#originalRelease.name': originalName}) => - name === originalName, - }, + withAlwaysReferenceByDirectory(), + exposeDependency({dependency: '#alwaysReferenceByDirectory'}), ], // Disables presenting the track as though it has its own unique artwork. @@ -298,61 +273,20 @@ export class Track extends Thing { exposeDependency({dependency: '#album.date'}), ], - // Whether or not the track has "unique" cover artwork - a cover which is - // specifically associated with this track in particular, rather than with - // the track's album as a whole. This is typically used to select between - // displaying the track artwork and a fallback, such as the album artwork - // or a placeholder. (This property is named hasUniqueCoverArt instead of - // the usual hasCoverArt to emphasize that it does not inherit from the - // album.) hasUniqueCoverArt: [ withHasUniqueCoverArt(), exposeDependency({dependency: '#hasUniqueCoverArt'}), ], otherReleases: [ - exitWithoutDependency({ - dependency: 'trackData', - mode: input.value('empty'), - }), - - withOriginalRelease({ - selfIfOriginal: input.value(true), - }), - - { - flags: {expose: true}, - expose: { - dependencies: [input.myself(), '#originalRelease', 'trackData'], - compute: ({ - [input.myself()]: thisTrack, - ['#originalRelease']: originalRelease, - trackData, - }) => - (originalRelease === thisTrack - ? [] - : [originalRelease]) - .concat(trackData.filter(track => - track !== originalRelease && - track !== thisTrack && - track.originalReleaseTrack === originalRelease)), - }, - }, + withOtherReleases(), + exposeDependency({dependency: '#otherReleases'}), ], - // Specifically exclude re-releases from this list - while it's useful to - // get from a re-release to the tracks it references, re-releases aren't - // generally relevant from the perspective of the tracks being referenced. - // Filtering them from data here hides them from the corresponding field - // on the site (obviously), and has the bonus of not counting them when - // counting the number of times a track has been referenced, for use in - // the "Tracks - by Times Referenced" listing page (or other data - // processing). referencedByTracks: trackReverseReferenceList({ list: input.value('referencedTracks'), }), - // For the same reasoning, exclude re-releases from sampled tracks too. sampledByTracks: trackReverseReferenceList({ list: input.value('sampledTracks'), }), @@ -386,344 +320,3 @@ export class Track extends Thing { return parts.join(''); } } - -// Early exits with a value inherited from the original release, if -// this track is a rerelease, and otherwise continues with no further -// dependencies provided. If allowOverride is true, then the continuation -// will also be called if the original release exposed the requested -// property as null. -export const inheritFromOriginalRelease = templateCompositeFrom({ - annotation: `Track.inheritFromOriginalRelease`, - - inputs: { - property: input({type: 'string'}), - allowOverride: input({type: 'boolean', defaultValue: false}), - }, - - steps: () => [ - withOriginalRelease(), - - { - dependencies: [ - '#originalRelease', - input('property'), - input('allowOverride'), - ], - - compute: (continuation, { - ['#originalRelease']: originalRelease, - [input('property')]: originalProperty, - [input('allowOverride')]: allowOverride, - }) => { - if (!originalRelease) return continuation(); - - const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation(); - - return continuation.exit(value); - }, - }, - ], -}); - -// Gets the track's album. This will early exit if albumData is missing. -// By default, if there's no album whose list of tracks includes this track, -// the output dependency will be null; set {notFoundMode: 'exit'} to early -// exit instead. -export const withAlbum = templateCompositeFrom({ - annotation: `Track.withAlbum`, - - inputs: { - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#album'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: 'albumData', - mode: input.value('empty'), - output: input.value({ - ['#album']: null, - }), - }), - - { - dependencies: [input.myself(), 'albumData'], - compute: (continuation, { - [input.myself()]: track, - ['albumData']: albumData, - }) => - continuation({ - ['#album']: - albumData.find(album => album.tracks.includes(track)), - }), - }, - - raiseOutputWithoutDependency({ - dependency: '#album', - output: input.value({ - ['#album']: null, - }), - }), - - { - dependencies: ['#album'], - compute: (continuation, {'#album': album}) => - continuation.raiseOutput({'#album': album}), - }, - ], -}); - -// Gets a single property from this track's album, providing it as the same -// property name prefixed with '#album.' (by default). If the track's album -// isn't available, then by default, the property will be provided as null; -// set {notFoundMode: 'exit'} to early exit instead. -export const withPropertyFromAlbum = templateCompositeFrom({ - annotation: `withPropertyFromAlbum`, - - inputs: { - property: input.staticValue({type: 'string'}), - - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ({ - [input.staticValue('property')]: property, - }) => ['#album.' + property], - - steps: () => [ - withAlbum({ - notFoundMode: input('notFoundMode'), - }), - - withPropertyFromObject({ - object: '#album', - property: input('property'), - }), - - { - dependencies: ['#value', input.staticValue('property')], - compute: (continuation, { - ['#value']: value, - [input.staticValue('property')]: property, - }) => continuation({ - ['#album.' + property]: value, - }), - }, - ], -}); - -// Gets the track section containing this track from its album's track list. -// If notFoundMode is set to 'exit', this will early exit if the album can't be -// found or if none of its trackSections includes the track for some reason. -export const withContainingTrackSection = templateCompositeFrom({ - annotation: `withContainingTrackSection`, - - inputs: { - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#trackSection'], - - steps: () => [ - withPropertyFromAlbum({ - property: input.value('trackSections'), - notFoundMode: input('notFoundMode'), - }), - - { - dependencies: [ - input.myself(), - input('notFoundMode'), - '#album.trackSections', - ], - - compute(continuation, { - [input.myself()]: track, - [input('notFoundMode')]: notFoundMode, - ['#album.trackSections']: trackSections, - }) { - if (!trackSections) { - return continuation.raiseOutput({ - ['#trackSection']: null, - }); - } - - const trackSection = - trackSections.find(({tracks}) => tracks.includes(track)); - - if (trackSection) { - return continuation.raiseOutput({ - ['#trackSection']: trackSection, - }); - } else if (notFoundMode === 'exit') { - return continuation.exit(null); - } else { - return continuation.raiseOutput({ - ['#trackSection']: null, - }); - } - }, - }, - ], -}); - -// Just includes the original release of this track as a dependency. -// If this track isn't a rerelease, then it'll provide null, unless the -// {selfIfOriginal} option is set, in which case it'll provide this track -// itself. Note that this will early exit if the original release is -// specified by reference and that reference doesn't resolve to anything. -// Outputs to '#originalRelease' by default. -export const withOriginalRelease = templateCompositeFrom({ - annotation: `withOriginalRelease`, - - inputs: { - selfIfOriginal: input({type: 'boolean', defaultValue: false}), - - data: input({ - validate: validateWikiData({referenceType: 'track'}), - defaultDependency: 'trackData', - }), - }, - - outputs: ['#originalRelease'], - - steps: () => [ - withResolvedReference({ - ref: 'originalReleaseTrack', - data: input('data'), - find: input.value(find.track), - notFoundMode: input.value('exit'), - }).outputs({ - ['#resolvedReference']: '#originalRelease', - }), - - { - dependencies: [ - input.myself(), - input('selfIfOriginal'), - '#originalRelease', - ], - - compute: (continuation, { - [input.myself()]: track, - [input('selfIfOriginal')]: selfIfOriginal, - ['#originalRelease']: originalRelease, - }) => - continuation({ - ['#originalRelease']: - (originalRelease ?? - (selfIfOriginal - ? track - : null)), - }), - }, - ], -}); - -// The algorithm for checking if a track has unique cover art is used in a -// couple places, so it's defined in full as a compositional step. -export const withHasUniqueCoverArt = templateCompositeFrom({ - annotation: 'withHasUniqueCoverArt', - - outputs: ['#hasUniqueCoverArt'], - - steps: () => [ - { - dependencies: ['disableUniqueCoverArt'], - compute: (continuation, {disableUniqueCoverArt}) => - (disableUniqueCoverArt - ? continuation.raiseOutput({ - ['#hasUniqueCoverArt']: false, - }) - : continuation()), - }, - - withResolvedContribs({from: 'coverArtistContribs'}), - - { - dependencies: ['#resolvedContribs'], - compute: (continuation, { - ['#resolvedContribs']: contribsFromTrack, - }) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raiseOutput({ - ['#hasUniqueCoverArt']: true, - })), - }, - - withPropertyFromAlbum({ - property: input.value('trackCoverArtistContribs'), - }), - - { - dependencies: ['#album.trackCoverArtistContribs'], - compute: (continuation, { - ['#album.trackCoverArtistContribs']: contribsFromAlbum, - }) => - continuation.raiseOutput({ - ['#hasUniqueCoverArt']: - !empty(contribsFromAlbum), - }), - }, - ], -}); - -// Shorthand for checking if the track has unique cover art and exposing a -// fallback value if it isn't. -export const exitWithoutUniqueCoverArt = templateCompositeFrom({ - annotation: `exitWithoutUniqueCoverArt`, - - inputs: { - value: input({defaultValue: null}), - }, - - steps: () => [ - withHasUniqueCoverArt(), - - exitWithoutDependency({ - dependency: '#hasUniqueCoverArt', - mode: input.value('falsy'), - value: input('value'), - }), - ], -}); - -export const trackReverseReferenceList = templateCompositeFrom({ - annotation: `trackReverseReferenceList`, - - compose: false, - - inputs: { - list: input({type: 'string'}), - }, - - steps: () => [ - withReverseReferenceList({ - data: 'trackData', - list: input('list'), - }), - - { - flags: {expose: true}, - expose: { - dependencies: ['#reverseReferenceList'], - compute: ({ - ['#reverseReferenceList']: reverseReferenceList, - }) => - reverseReferenceList.filter(track => !track.originalReleaseTrack), - }, - }, - ], -}); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index c764b528..0460f272 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -2,14 +2,16 @@ import {input} from '#composite'; import find from '#find'; import {isLanguageCode, isName, isURL} from '#validators'; -import Thing, { +import { color, flag, name, referenceList, simpleString, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class WikiInfo extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ -- cgit 1.3.0-6-gf8a5 From d2174a01dda63ba233cbcdf48bb70ed50127d54d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 1 Oct 2023 17:30:39 -0300 Subject: data: obliterate composite.js explainer Poor (read: largely outdated) code documentation is worse than no code documentation. The various infrastructural systems specially designed for hsmusic should get more dedicated reference material, but that can't well be written before the systems are tested and used for longer. The compositional data processing style has just about settled, but it's still very young (compared to, say, the overarching data- to-page flow, content functions, or the HTML and content template systems). --- src/data/things/composite.js | 333 ------------------------------------------- 1 file changed, 333 deletions(-) (limited to 'src') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7e068dce..51525bc1 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -15,339 +15,6 @@ import { withAggregate, } from '#sugar'; -// Composes multiple compositional "steps" and a "base" to form a property -// descriptor out of modular building blocks. This is an extension to the -// more general-purpose CacheableObject property descriptor syntax, and -// aims to make modular data processing - which lends to declarativity - -// much easier, without fundamentally altering much of the typical syntax -// or terminology, nor building on it to an excessive degree. -// -// Think of a composition as being a chain of steps which lead into a final -// base property, which is usually responsible for returning the value that -// will actually get exposed when the property being described is accessed. -// -// == The compositional base: == -// -// The final item in a compositional list is its base, and it identifies -// the essential qualities of the property descriptor. The compositional -// steps preceding it may exit early, in which case the expose function -// defined on the base won't be called; or they will provide dependencies -// that the base may use to compute the final value that gets exposed for -// this property. -// -// The base indicates the capabilities of the composition as a whole. -// It should be {expose: true}, since that's the only area that preceding -// compositional steps (currently) can actually influence. If it's also -// {update: true}, then the composition as a whole accepts an update value -// just like normal update-flag property descriptors - meaning it can be -// set with `thing.someProperty = value` and that value will be paseed -// into each (implementing) step's transform() function, as well as the -// base. Bases usually aren't {compose: true}, but can be - check out the -// section on "nesting compositions" for details about that. -// -// Every composition always has exactly one compositional base, and it's -// always the last item in the composition list. All items preceding it -// are compositional steps, described below. -// -// == Compositional steps: == -// -// Compositional steps are, in essence, typical property descriptors with -// the extra flag {compose: true}. They operate on existing dependencies, -// and are typically dynamically constructed by "utility" functions (but -// can also be manually declared within the step list of a composition). -// Compositional steps serve two purposes: -// -// 1. exit early, if some condition is matched, returning and exposing -// some value directly from that step instead of continuing further -// down the step list; -// -// 2. and/or provide new, dynamically created "private" dependencies which -// can be accessed by further steps down the list, or at the base at -// the bottom, modularly supplying information that will contribute to -// the final value exposed for this property. -// -// Usually it's just one of those two, but it's fine for a step to perform -// both jobs if the situation benefits. -// -// Compositional steps are the real "modular" or "compositional" part of -// this data processing style - they're designed to be combined together -// in dynamic, versatile ways, as each property demands it. You usually -// define a compositional step to be returned by some ordinary static -// property-descriptor-returning function (customarily namespaced under -// the relevant Thing class's static `composite` field) - that lets you -// reuse it in multiple compositions later on. -// -// Compositional steps are implemented with "continuation passing style", -// meaning the connection to the next link on the chain is passed right to -// each step's compute (or transform) function, and the implementation gets -// to decide whether to continue on that chain or exit early by returning -// some other value. -// -// Every step along the chain, apart from the base at the bottom, has to -// have the {compose: true} step. That means its compute() or transform() -// function will be passed an extra argument at the end, `continuation`. -// To provide new dependencies to items further down the chain, just pass -// them directly to this continuation() function, customarily with a hash -// ('#') prefixing each name - for example: -// -// compute({..some dependencies..}, continuation) { -// return continuation({ -// '#excitingProperty': (..a value made from dependencies..), -// }); -// } -// -// Performing an early exit is as simple as returning some other value, -// instead of the continuation. You may also use `continuation.exit(value)` -// to perform the exact same kind of early exit - it's just a different -// syntax that might fit in better in certain longer compositions. -// -// It may be fine to simply provide new dependencies under a hard-coded -// name, such as '#excitingProperty' above, but if you're writing a utility -// that dynamically returns the compositional step and you suspect you -// might want to use this step multiple times in a single composition, -// it's customary to accept a name for the result. -// -// Here's a detailed example showing off early exit, dynamically operating -// on a provided dependency name, and then providing a result in another -// also-provided dependency name: -// -// withResolvedContribs = ({ -// from: contribsByRefDependency, -// into: outputDependency, -// }) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: [contribsByRefDependency, 'artistData'], -// compute({ -// [contribsByRefDependency]: contribsByRef, -// artistData, -// }, continuation) { -// if (!artistData) return null; /* early exit! */ -// return continuation({ -// [outputDependency]: /* this is the important part */ -// (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// And how you might work that into a composition: -// -// Track.coverArtists = -// compositeFrom([ -// doSomethingWhichMightEarlyExit(), -// -// withResolvedContribs({ -// from: 'coverArtistContribsByRef', -// into: '#coverArtistContribs', -// }), -// -// { -// flags: {expose: true}, -// expose: { -// dependencies: ['#coverArtistContribs'], -// compute: ({'#coverArtistContribs': coverArtistContribs}) => -// coverArtistContribs.map(({who}) => who), -// }, -// }, -// ]); -// -// One last note! A super common code pattern when creating more complex -// compositions is to have several steps which *only* expose and compose. -// As a syntax shortcut, you can skip the outer section. It's basically -// like writing out just the {expose: {...}} part. Remember that this -// indicates that the step you're defining is compositional, so you have -// to specify the flags manually for the base, even if this property isn't -// going to get an {update: true} flag. -// -// == Cache-safe dependency names: == -// -// [Disclosure: The caching engine hasn't actually been implemented yet. -// As such, this section is subject to change, and simply provides sound -// forward-facing advice and interfaces.] -// -// It's a good idea to write individual compositional steps in such a way -// that they're "cache-safe" - meaning the same input (dependency) values -// will always result in the same output (continuation or early exit). -// -// In order to facilitate this, compositional step descriptors may specify -// unique `mapDependencies`, `mapContinuation`, and `options` values. -// -// Consider the `withResolvedContribs` example adjusted to make use of -// two of these options below: -// -// withResolvedContribs = ({ -// from: contribsByRefDependency, -// into: outputDependency, -// }) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: ['artistData'], -// mapDependencies: {contribsByRef: contribsByRefDependency}, -// mapContinuation: {outputDependency}, -// compute({ -// contribsByRef, /* no longer in square brackets */ -// artistData, -// }, continuation) { -// if (!artistData) return null; -// return continuation({ -// outputDependency: /* no longer in square brackets */ -// (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// With a little destructuring and restructuring JavaScript sugar, the -// above can be simplified some more: -// -// withResolvedContribs = ({from, to}) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: ['artistData'], -// mapDependencies: {from}, -// mapContinuation: {into}, -// compute({artistData, from: contribsByRef}, continuation) { -// if (!artistData) return null; -// return continuation({ -// into: (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// These two properties let you separate the name-mapping behavior (for -// dependencies and the continuation) from the main body of the compute -// function. That means the compute function will *always* get inputs in -// the same form (dependencies 'artistData' and 'from' above), and will -// *always* provide its output in the same form (early return or 'to'). -// -// Thanks to that, this `compute` function is cache-safe! Its outputs can -// be cached corresponding to each set of mapped inputs. So it won't matter -// whether the `from` dependency is named `coverArtistContribsByRef` or -// `contributorContribsByRef` or something else - the compute function -// doesn't care, and only expects that value to be provided via its `from` -// argument. Likewise, it doesn't matter if the output should be sent to -// '#coverArtistContribs` or `#contributorContribs` or some other name; -// the mapping is handled automatically outside, and compute will always -// output its value to the continuation's `to`. -// -// Note that `mapDependencies` and `mapContinuation` should be objects of -// the same "shape" each run - that is, the values will change depending on -// outside context, but the keys are always the same. You shouldn't use -// `mapDependencies` to dynamically select more or fewer dependencies. -// If you need to dynamically select a range of dependencies, just specify -// them in the `dependencies` array like usual. The caching engine will -// understand that differently named `dependencies` indicate separate -// input-output caches should be used. -// -// The 'options' property makes it possible to specify external arguments -// that fundamentally change the behavior of the `compute` function, while -// still remaining cache-safe. It indicates that the caching engine should -// use a completely different input-to-output cache for each permutation -// of the 'options' values. This way, those functions are still cacheable -// at all; they'll just be cached separately for each set of option values. -// Values on the 'options' property will always be provided in compute's -// dependencies under '#options' (to avoid name conflicts with other -// dependencies). -// -// == To compute or to transform: == -// -// A compositional step can work directly on a property's stored update -// value, transforming it in place and either early exiting with it or -// passing it on (via continuation) to the next item(s) in the -// compositional step list. (If needed, these can provide dependencies -// the same way as compute functions too - just pass that object after -// the updated (or same) transform value in your call to continuation().) -// -// But in order to make them more versatile, compositional steps have an -// extra trick up their sleeve. If a compositional step implements compute -// and *not* transform, it can still be used in a composition targeting a -// property which updates! These retain their full dependency-providing and -// early exit functionality - they just won't be provided the update value. -// If a compute-implementing step returns its continuation, then whichever -// later step (or the base) next implements transform() will receive the -// update value that had so far been running - as well as any dependencies -// the compute() step returned, of course! -// -// Please note that a compositional step which transforms *should not* -// specify, in its flags, {update: true}. Just provide the transform() -// function in its expose descriptor; it will be automatically detected -// and used when appropriate. -// -// It's actually possible for a step to specify both transform and compute, -// in which case the transform() implementation will only be selected if -// the composition's base is {update: true}. It's not exactly known why you -// would want to specify unique-but-related transform and compute behavior, -// but the basic possibility was too cool to skip out on. -// -// == Nesting compositions: == -// -// Compositional steps are so convenient that you just might want to bundle -// them together, and form a whole new step-shaped unit of its own! -// -// In order to allow for this while helping to ensure internal dependencies -// remain neatly isolated from the composition which nests your bundle, -// the compositeFrom() function will accept and adapt to a base that -// specifies the {compose: true} flag, just like the steps preceding it. -// -// The continuation function that gets provided to the base will be mildly -// special - after all, nothing follows the base within the composition's -// own list! Instead of appending dependencies alongside any previously -// provided ones to be available to the next step, the base's continuation -// function should be used to define "exports" of the composition as a -// whole. It's similar to the usual behavior of the continuation, just -// expanded to the scope of the composition instead of following steps. -// -// For example, suppose your composition (which you expect to include in -// other compositions) brings about several private, hash-prefixed -// dependencies to contribute to its own results. Those dependencies won't -// end up "bleeding" into the dependency list of whichever composition is -// nesting this one - they will totally disappear once all the steps in -// the nested composition have finished up. -// -// To "export" the results of processing all those dependencies (provided -// that's something you want to do and this composition isn't used purely -// for a conditional early-exit), you'll want to define them in the -// continuation passed to the base. (Customarily, those should start with -// a hash just like the exports from any other compositional step; they're -// still dynamically provided dependencies!) -// -// Another way to "export" dependencies is by using calling *any* step's -// `continuation.raise()` function. This is sort of like early exiting, -// but instead of quitting out the whole entire property, it will just -// break out of the current, nested composition's list of steps, acting -// as though the composition had finished naturally. The dependencies -// passed to `raise` will be the ones which get exported. -// -// Since `raise` is another way to export dependencies, if you're using -// dynamic export names, you should specify `mapContinuation` on the step -// calling `continuation.raise` as well. -// -// An important note on `mapDependencies` here: A nested composition gets -// free access to all the ordinary properties defined on the thing it's -// working on, but if you want it to depend on *private* dependencies - -// ones prefixed with '#' - which were provided by some other compositional -// step preceding wherever this one gets nested, then you *have* to use -// `mapDependencies` to gain access. Check out the section on "cache-safe -// dependency names" for information on this syntax! -// -// Also - on rare occasion - you might want to make a reusable composition -// that itself causes the composition *it's* nested in to raise. If that's -// the case, give `composition.raiseAbove()` a go! This effectively means -// kicking out of *two* layers of nested composition - the one including -// the step with the `raiseAbove` call, and the composition which that one -// is nested within. You don't need to use `raiseAbove` if the reusable -// utility function just returns a single compositional step, but if you -// want to make use of other compositional steps, it gives you access to -// the same conditional-raise capabilities. -// -// Have some syntax sugar! Since nested compositions are defined by having -// the base be {compose: true}, the composition will infer as much if you -// don't specifying the base's flags at all. Simply use the same shorthand -// syntax as for other compositional steps, and it'll work out cleanly! -// - const globalCompositeCache = {}; const _valueIntoToken = shape => -- cgit 1.3.0-6-gf8a5 From a60c8906ed7580a21527c9f96cd0e6e277978263 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 1 Oct 2023 17:58:56 -0300 Subject: data, test: expose track section names --- src/data/composite/things/album/withTrackSections.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js index c99b94d2..baa3cb4a 100644 --- a/src/data/composite/things/album/withTrackSections.js +++ b/src/data/composite/things/album/withTrackSections.js @@ -41,6 +41,7 @@ export default templateCompositeFrom({ 'tracks', 'dateOriginallyReleased', 'isDefaultTrackSection', + 'name', 'color', ]), }), @@ -55,6 +56,11 @@ export default templateCompositeFrom({ fill: input.value(false), }), + fillMissingListItems({ + list: '#sections.name', + fill: input.value('Unnamed Track Section'), + }), + fillMissingListItems({ list: '#sections.color', fill: input.dependency('color'), @@ -86,6 +92,7 @@ export default templateCompositeFrom({ { dependencies: [ '#sections.tracks', + '#sections.name', '#sections.color', '#sections.dateOriginallyReleased', '#sections.isDefaultTrackSection', @@ -94,19 +101,21 @@ export default templateCompositeFrom({ compute: (continuation, { '#sections.tracks': tracks, + '#sections.name': name, '#sections.color': color, '#sections.dateOriginallyReleased': dateOriginallyReleased, '#sections.isDefaultTrackSection': isDefaultTrackSection, '#sections.startIndex': startIndex, }) => { filterMultipleArrays( - tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, + tracks, name, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, tracks => !empty(tracks)); return continuation({ ['#trackSections']: stitchArrays({ tracks, + name, color, dateOriginallyReleased, isDefaultTrackSection, -- cgit 1.3.0-6-gf8a5 From 963e04f124f98464a986487208ee4f9edd893984 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 1 Oct 2023 17:59:25 -0300 Subject: data: misc. composite fixes --- src/data/composite/things/track/withAlwaysReferenceByDirectory.js | 2 ++ src/data/composite/wiki-data/withReverseReferenceList.js | 1 + src/data/things/album.js | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index 0aeac788..7c59393c 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -15,6 +15,8 @@ import withOriginalRelease from './withOriginalRelease.js'; export default templateCompositeFrom({ annotation: `withAlwaysReferenceByDirectory`, + outputs: ['#alwaysReferenceByDirectory'], + steps: () => [ exposeUpdateValueOrContinue({ validate: input.value(isBoolean), diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js index 113a6c40..a025b5ed 100644 --- a/src/data/composite/wiki-data/withReverseReferenceList.js +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -21,6 +21,7 @@ export default templateCompositeFrom({ exitWithoutDependency({ dependency: input('data'), value: input.value([]), + mode: input.value('empty'), }), { diff --git a/src/data/things/album.js b/src/data/things/album.js index e3ac1651..f451a7e9 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -182,7 +182,7 @@ export class Album extends Thing { export class TrackSectionHelper extends Thing { static [Thing.getPropertyDescriptors] = () => ({ - name: name('Unnamed Track Group'), + name: name('Unnamed Track Section'), color: color(), dateOriginallyReleased: simpleDate(), isDefaultTrackGroup: flag(false), -- cgit 1.3.0-6-gf8a5 From 26a147bff746dea09ec68df184fe0ea0e2920608 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 1 Oct 2023 17:59:54 -0300 Subject: content: image: fix CW-decorating images which are missing --- src/content/dependencies/image.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 64fe8533..6c0aeecd 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -102,6 +102,7 @@ export default { const willReveal = slots.reveal && originalSrc && + !isMissingImageFile && !empty(data.contentWarnings); const willSquare = slots.square; -- cgit 1.3.0-6-gf8a5 From d30bc62956358637522d636b4454aee39e7b3d03 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 2 Oct 2023 09:28:07 -0300 Subject: find: accept arrays... experimentally... --- src/find.js | 236 +++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 188 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/find.js b/src/find.js index 66f705e4..2959ed56 100644 --- a/src/find.js +++ b/src/find.js @@ -32,85 +32,223 @@ function findHelper(keys, findFns = {}) { // console. return (fullRef, data, {mode = 'warn'} = {}) => { if (!fullRef) return null; - if (typeof fullRef !== 'string') { - throw new Error(`Got a reference that is ${typeof fullRef}, not string: ${fullRef}`); + + if (typeof fullRef !== 'string' && !Array.isArray(fullRef)) { + throw new Error(`Got a reference that is ${typeof fullRef}, not string or array: ${fullRef}`); } if (!data) { throw new Error(`Expected data to be present`); } - if (!Array.isArray(data) && data.wikiData) { - throw new Error(`Old {wikiData: {...}} format provided`); - } - let cacheForThisData = cache.get(data); - const cachedValue = cacheForThisData?.[fullRef]; - if (cachedValue) { - globalThis.NUM_CACHE = (globalThis.NUM_CACHE || 0) + 1; - return cachedValue; - } if (!cacheForThisData) { cacheForThisData = Object.create(null); cache.set(data, cacheForThisData); } - const match = fullRef.match(keyRefRegex); - if (!match) { - return warnOrThrow(mode, `Malformed link reference: "${fullRef}"`); - } + const parseFullRef = fullRef => { + const regexMatch = fullRef.match(keyRefRegex); + if (!regexMatch) { + warnOrThrow(mode, `Malformed link reference: "${fullRef[i]}"`); + return {error: true, key: null, ref: null}; + } + + const key = regexMatch[1]; + const ref = regexMatch[2]; - const key = match[1]; - const ref = match[2]; + return {error: false, key, ref}; + }; - const found = key ? byDirectory(ref, data, mode) : byName(ref, data, mode); + if (typeof fullRef === 'string') { + const cachedMatch = cacheForThisData[fullRef]; + if (cachedMatch) return cachedMatch; - if (!found) { - warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`); + const {error: regexError, key, ref} = parseFullRef(fullRef); + if (regexError) return null; + + const match = + (key + ? byDirectory(ref, data, mode) + : byName(ref, data, mode)); + + if (!match) { + warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`); + } + + cacheForThisData[fullRef] = match; + + return match; } - cacheForThisData[fullRef] = found; + const fullRefList = fullRef; + if (Array.isArray(fullRefList)) { + const byDirectoryUncachedIndices = []; + const byDirectoryUncachedRefs = []; + const byNameUncachedIndices = []; + const byNameUncachedRefs = []; + + for (let index = 0; index < fullRefList.length; index++) { + const cachedMatch = cacheForThisData[fullRefList[index]]; + if (cachedMatch) return cachedMatch; - return found; + const {error: regexError, key, ref} = parseFullRef(fullRefList[index]); + if (regexError) return null; + + if (key) { + byDirectoryUncachedIndices.push(index); + byDirectoryUncachedRefs.push(ref); + } else { + byNameUncachedIndices.push(index); + byNameUncachedRefs.push(ref); + } + } + + const byDirectoryMatches = byDirectory(byDirectoryUncachedRefs, data, mode); + const byNameMatches = byName(byNameUncachedRefs, data, mode); + + const results = []; + + const processMatch = (match, sourceIndex) => { + if (match) { + cacheForThisData[fullRefList[sourceIndex]] = match; + results[sourceIndex] = match; + } else { + // TODO: Aggregate errors + warnOrThrow(mode, `Didn't match anything for ${fullRefList[sourceIndex]}`); + results[sourceIndex] = null; + } + }; + + for (let index = 0; index < byDirectoryMatches.length; index++) { + const sourceIndex = byDirectoryUncachedIndices[index]; + const match = byDirectoryMatches[index]; + processMatch(match, sourceIndex); + } + + for (let index = 0; index < byNameMatches.length; index++) { + const sourceIndex = byNameUncachedIndices[index]; + const match = byNameMatches[index]; + processMatch(match, sourceIndex); + } + + return results; + } }; } function matchDirectory(ref, data) { - return data.find(({directory}) => directory === ref); + if (typeof ref === 'string') { + return data.find(({directory}) => directory === ref); + } + + const refList = ref; + if (Array.isArray(refList)) { + const refSet = new Set(refList); + const refMap = new Map(); + + for (const thing of data) { + const {directory} = thing; + if (refSet.has(directory)) { + refMap.set(directory, thing); + } + } + + return refList.map(ref => refMap.get(ref) ?? null); + } } function matchName(ref, data, mode) { - const matches = - data - .filter(({name}) => name.toLowerCase() === ref.toLowerCase()) - .filter(thing => - (Object.hasOwn(thing, 'alwaysReferenceByDirectory') - ? !thing.alwaysReferenceByDirectory - : true)); - - if (matches.length > 1) { - return warnOrThrow(mode, - `Multiple matches for reference "${ref}". Please resolve:\n` + - matches.map(match => `- ${inspect(match)}\n`).join('') + - `Returning null for this reference.`); - } + if (typeof ref === 'string') { + const matches = + data + .filter(({name}) => name.toLowerCase() === ref.toLowerCase()) + .filter(thing => + (Object.hasOwn(thing, 'alwaysReferenceByDirectory') + ? !thing.alwaysReferenceByDirectory + : true)); - if (matches.length === 0) { - return null; - } + if (matches.length > 1) { + return warnOrThrow(mode, + `Multiple matches for reference "${ref}". Please resolve:\n` + + matches.map(match => `- ${inspect(match)}\n`).join('') + + `Returning null for this reference.`); + } - const thing = matches[0]; + if (matches.length === 0) { + return null; + } + + const match = matches[0]; - if (ref !== thing.name) { - warnOrThrow(mode, - `Bad capitalization: ${colors.red(ref)} -> ${colors.green(thing.name)}`); + if (ref !== match.name) { + warnOrThrow(mode, + `Bad capitalization: ${colors.red(ref)} -> ${colors.green(match.name)}`); + } + + return match; } - return thing; + const refList = ref; + if (Array.isArray(refList)) { + const refSet = new Set(refList.map(name => name.toLowerCase())); + const refMap = new Map(); + const multipleMatchesMap = new Map(); + + for (const thing of data) { + if (thing.alwaysReferenceByDirectory) continue; + const name = thing.name.toLowerCase(); + if (refSet.has(name)) { + if (refMap.has(name)) { + refMap.set(name, null); // .has() will still return true + if (multipleMatchesMap.has(name)) { + multipleMatchesMap.get(name).push(thing); + } else { + multipleMatchesMap.set(name, [thing]); + } + } else { + refMap.set(name, thing); + } + } + } + + // TODO: Aggregate errors + for (const [name, matches] of multipleMatchesMap.entries()) { + warnOrThrow(mode, + `Multiple matches for reference "${ref}". Please resolve:\n` + + matches.map(match => `- ${inspect(match)}\n`).join('') + + `Returning null for this reference.`); + } + + return refList.map(ref => { + const match = refMap.get(ref); + if (!match) return null; + + // TODO: Aggregate errors + if (ref !== match.name) { + warnOrThrow(mode, + `Bad capitalization: ${colors.red(ref)} -> ${colors.green(match.name)}`); + } + + return match; + }); + } } -function matchTagName(ref, data, quiet) { - return matchName(ref.startsWith('cw: ') ? ref.slice(4) : ref, data, quiet); +function matchTagName(ref, data, mode) { + if (typeof ref === 'string') { + return matchName( + ref.startsWith('cw: ') ? ref.slice(4) : ref, + data, + mode); + } + + if (Array.isArray(ref)) { + return matchName( + ref.map(ref => ref.startsWith('cw: ') ? ref.slice(4) : ref), + data, + mode); + } } const find = { @@ -155,7 +293,9 @@ export function bindFind(wikiData, opts1) { ? findFn(ref, thingData, {...opts1, ...opts2}) : findFn(ref, thingData, opts1) : (ref, opts2) => - opts2 ? findFn(ref, thingData, opts2) : findFn(ref, thingData), + opts2 + ? findFn(ref, thingData, opts2) + : findFn(ref, thingData), ]; }) ); -- cgit 1.3.0-6-gf8a5 From 7c938421035502484ed0e15b11064421bd7fcfce Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 2 Oct 2023 10:37:10 -0300 Subject: find: always read matches from pre-cached hash map --- src/find.js | 331 +++++++++++++++++++++--------------------------------------- 1 file changed, 115 insertions(+), 216 deletions(-) (limited to 'src') diff --git a/src/find.js b/src/find.js index 2959ed56..78775457 100644 --- a/src/find.js +++ b/src/find.js @@ -1,6 +1,7 @@ import {inspect} from 'node:util'; import {colors, logWarn} from '#cli'; +import {typeAppearance} from '#sugar'; function warnOrThrow(mode, message) { if (mode === 'error') { @@ -14,253 +15,151 @@ function warnOrThrow(mode, message) { return null; } -function findHelper(keys, findFns = {}) { +export function processAllAvailableMatches(data, { + getMatchableNames = thing => [thing.name], +} = {}) { + const byName = Object.create(null); + const byDirectory = Object.create(null); + const multipleNameMatches = Object.create(null); + + for (const thing of data) { + for (const name of getMatchableNames(thing)) { + const normalizedName = name.toLowerCase(); + if (normalizedName in byName) { + byName[normalizedName] = null; + if (normalizedName in multipleNameMatches) { + multipleNameMatches[normalizedName].push(thing); + } else { + multipleNameMatches[normalizedName] = [thing]; + } + } else { + byName[normalizedName] = thing; + } + } + + byDirectory[thing.directory] = thing; + } + + return {byName, byDirectory, multipleNameMatches}; +} + +function findHelper({ + referenceTypes, + + getMatchableNames = undefined, +}) { + const keyRefRegex = + new RegExp(String.raw`^(?:(${referenceTypes.join('|')}):(?=\S))?(.*)$`); + // Note: This cache explicitly *doesn't* support mutable data arrays. If the // data array is modified, make sure it's actually a new array object, not // the original, or the cache here will break and act as though the data // hasn't changed! const cache = new WeakMap(); - const byDirectory = findFns.byDirectory || matchDirectory; - const byName = findFns.byName || matchName; - - const keyRefRegex = new RegExp(String.raw`^(?:(${keys.join('|')}):(?=\S))?(.*)$`); - // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws // errors for null matches (with details about the error), while 'warn' and // 'quiet' both return null, with 'warn' logging details directly to the // console. - return (fullRef, data, {mode = 'warn'} = {}) => { + return (fullRef, data, {mode = 'warn'}) => { if (!fullRef) return null; - - if (typeof fullRef !== 'string' && !Array.isArray(fullRef)) { - throw new Error(`Got a reference that is ${typeof fullRef}, not string or array: ${fullRef}`); + if (typeof fullRef !== 'string') { + throw new TypeError(`Expected a string, got ${typeAppearance(fullRef)}`); } if (!data) { - throw new Error(`Expected data to be present`); + throw new TypeError(`Expected data to be present`); } - let cacheForThisData = cache.get(data); - if (!cacheForThisData) { - cacheForThisData = Object.create(null); - cache.set(data, cacheForThisData); - } - - const parseFullRef = fullRef => { - const regexMatch = fullRef.match(keyRefRegex); - if (!regexMatch) { - warnOrThrow(mode, `Malformed link reference: "${fullRef[i]}"`); - return {error: true, key: null, ref: null}; - } - - const key = regexMatch[1]; - const ref = regexMatch[2]; - - return {error: false, key, ref}; - }; + let subcache = cache.get(data); + if (!subcache) { + subcache = + processAllAvailableMatches(data, { + getMatchableNames, + }); - if (typeof fullRef === 'string') { - const cachedMatch = cacheForThisData[fullRef]; - if (cachedMatch) return cachedMatch; - - const {error: regexError, key, ref} = parseFullRef(fullRef); - if (regexError) return null; - - const match = - (key - ? byDirectory(ref, data, mode) - : byName(ref, data, mode)); - - if (!match) { - warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`); - } - - cacheForThisData[fullRef] = match; - - return match; + cache.set(data, subcache); } - const fullRefList = fullRef; - if (Array.isArray(fullRefList)) { - const byDirectoryUncachedIndices = []; - const byDirectoryUncachedRefs = []; - const byNameUncachedIndices = []; - const byNameUncachedRefs = []; - - for (let index = 0; index < fullRefList.length; index++) { - const cachedMatch = cacheForThisData[fullRefList[index]]; - if (cachedMatch) return cachedMatch; - - const {error: regexError, key, ref} = parseFullRef(fullRefList[index]); - if (regexError) return null; - - if (key) { - byDirectoryUncachedIndices.push(index); - byDirectoryUncachedRefs.push(ref); - } else { - byNameUncachedIndices.push(index); - byNameUncachedRefs.push(ref); - } - } - - const byDirectoryMatches = byDirectory(byDirectoryUncachedRefs, data, mode); - const byNameMatches = byName(byNameUncachedRefs, data, mode); - - const results = []; - - const processMatch = (match, sourceIndex) => { - if (match) { - cacheForThisData[fullRefList[sourceIndex]] = match; - results[sourceIndex] = match; - } else { - // TODO: Aggregate errors - warnOrThrow(mode, `Didn't match anything for ${fullRefList[sourceIndex]}`); - results[sourceIndex] = null; - } - }; - - for (let index = 0; index < byDirectoryMatches.length; index++) { - const sourceIndex = byDirectoryUncachedIndices[index]; - const match = byDirectoryMatches[index]; - processMatch(match, sourceIndex); - } - - for (let index = 0; index < byNameMatches.length; index++) { - const sourceIndex = byNameUncachedIndices[index]; - const match = byNameMatches[index]; - processMatch(match, sourceIndex); - } - - return results; + const regexMatch = fullRef.match(keyRefRegex); + if (!regexMatch) { + warnOrThrow(mode, `Malformed link reference: "${fullRef}"`); } - }; -} - -function matchDirectory(ref, data) { - if (typeof ref === 'string') { - return data.find(({directory}) => directory === ref); - } - - const refList = ref; - if (Array.isArray(refList)) { - const refSet = new Set(refList); - const refMap = new Map(); - for (const thing of data) { - const {directory} = thing; - if (refSet.has(directory)) { - refMap.set(directory, thing); + const typePart = regexMatch[1]; + const refPart = regexMatch[2]; + + const match = + (typePart + ? subcache.byDirectory[refPart] + : subcache.byName[refPart.toLowerCase()]); + + if (!match && !typePart) { + if (subcache.multipleNameMatches[refPart]) { + return warnOrThrow(mode, + `Multiple matches for reference "${fullRef}". Please resolve:\n` + + subcache.multipleNameMatches[refPart] + .map(match => `- ${inspect(match)}\n`) + .join('') + + `Returning null for this reference.`); } } - return refList.map(ref => refMap.get(ref) ?? null); - } -} - -function matchName(ref, data, mode) { - if (typeof ref === 'string') { - const matches = - data - .filter(({name}) => name.toLowerCase() === ref.toLowerCase()) - .filter(thing => - (Object.hasOwn(thing, 'alwaysReferenceByDirectory') - ? !thing.alwaysReferenceByDirectory - : true)); - - if (matches.length > 1) { - return warnOrThrow(mode, - `Multiple matches for reference "${ref}". Please resolve:\n` + - matches.map(match => `- ${inspect(match)}\n`).join('') + - `Returning null for this reference.`); - } - - if (matches.length === 0) { + if (!match) { + warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`); return null; } - const match = matches[0]; - - if (ref !== match.name) { - warnOrThrow(mode, - `Bad capitalization: ${colors.red(ref)} -> ${colors.green(match.name)}`); - } - return match; - } - - const refList = ref; - if (Array.isArray(refList)) { - const refSet = new Set(refList.map(name => name.toLowerCase())); - const refMap = new Map(); - const multipleMatchesMap = new Map(); - - for (const thing of data) { - if (thing.alwaysReferenceByDirectory) continue; - const name = thing.name.toLowerCase(); - if (refSet.has(name)) { - if (refMap.has(name)) { - refMap.set(name, null); // .has() will still return true - if (multipleMatchesMap.has(name)) { - multipleMatchesMap.get(name).push(thing); - } else { - multipleMatchesMap.set(name, [thing]); - } - } else { - refMap.set(name, thing); - } - } - } - - // TODO: Aggregate errors - for (const [name, matches] of multipleMatchesMap.entries()) { - warnOrThrow(mode, - `Multiple matches for reference "${ref}". Please resolve:\n` + - matches.map(match => `- ${inspect(match)}\n`).join('') + - `Returning null for this reference.`); - } - - return refList.map(ref => { - const match = refMap.get(ref); - if (!match) return null; - - // TODO: Aggregate errors - if (ref !== match.name) { - warnOrThrow(mode, - `Bad capitalization: ${colors.red(ref)} -> ${colors.green(match.name)}`); - } - - return match; - }); - } -} - -function matchTagName(ref, data, mode) { - if (typeof ref === 'string') { - return matchName( - ref.startsWith('cw: ') ? ref.slice(4) : ref, - data, - mode); - } - - if (Array.isArray(ref)) { - return matchName( - ref.map(ref => ref.startsWith('cw: ') ? ref.slice(4) : ref), - data, - mode); - } + }; } const find = { - album: findHelper(['album', 'album-commentary', 'album-gallery']), - artist: findHelper(['artist', 'artist-gallery']), - artTag: findHelper(['tag'], {byName: matchTagName}), - flash: findHelper(['flash']), - group: findHelper(['group', 'group-gallery']), - listing: findHelper(['listing']), - newsEntry: findHelper(['news-entry']), - staticPage: findHelper(['static']), - track: findHelper(['track']), + album: findHelper({ + referenceTypes: ['album', 'album-commentary', 'album-gallery'], + }), + + artist: findHelper({ + referenceTypes: ['artist', 'artist-gallery'], + }), + + artTag: findHelper({ + referenceTypes: ['tag'], + + getMatchableNames: tag => + (tag.isContentWarning + ? [`cw: ${tag.name}`] + : [tag.name]), + }), + + flash: findHelper({ + referenceTypes: ['flash'], + }), + + group: findHelper({ + referenceTypes: ['group', 'group-gallery'], + }), + + listing: findHelper({ + referenceTypes: ['listing'], + }), + + newsEntry: findHelper({ + referenceTypes: ['news-entry'], + }), + + staticPage: findHelper({ + referenceTypes: ['static'], + }), + + track: findHelper({ + referenceTypes: ['track'], + + getMatchableNames: track => + (track.alwaysReferenceByDirectory + ? [] + : [track.name]), + }), }; export default find; -- cgit 1.3.0-6-gf8a5 From 72cc4d62a32e40c1dcb75e868c51991075cc03e7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 2 Oct 2023 10:40:27 -0300 Subject: data: withAlwaysReferenceByDirectory: kludge to avoid infinite recursion --- .../things/track/withAlwaysReferenceByDirectory.js | 51 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index 7c59393c..d27f7b23 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -2,15 +2,22 @@ // 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. +// +// See the implementation for an important caveat about matching the original +// track against other tracks, which uses a custom implementation pulling (and +// duplicating) details from #find instead of using withOriginalRelease and the +// usual withResolvedReference / find.track() utilities. +// import {input, templateCompositeFrom} from '#composite'; import {isBoolean} from '#validators'; import {exitWithoutDependency, exposeUpdateValueOrContinue} from '#composite/control-flow'; -import {excludeFromList, withPropertyFromObject} from '#composite/data'; +import {withPropertyFromObject} from '#composite/data'; -import withOriginalRelease from './withOriginalRelease.js'; +// TODO: Kludge. (The usage of this, not so much the import.) +import CacheableObject from '../../../things/cacheable-object.js'; export default templateCompositeFrom({ annotation: `withAlwaysReferenceByDirectory`, @@ -22,15 +29,45 @@ export default templateCompositeFrom({ validate: input.value(isBoolean), }), - excludeFromList({ - list: 'trackData', - item: input.myself(), + // 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 originalReleaseTrack. + + exitWithoutDependency({ + dependency: 'trackData', + mode: input.value('empty'), + value: input.value(false), }), - withOriginalRelease({ - data: '#trackData', + exitWithoutDependency({ + dependency: 'originalReleaseTrack', + value: input.value(false), }), + // "Slow" / uncached, manual search from trackData (with this track + // excluded). Otherwise there end up being pretty bad recursion issues + // (track1.alwaysReferencedByDirectory depends on searching through data + // including track2, which depends on evaluating track2.alwaysReferenced- + // ByDirectory, which depends on searcing through data including track1...) + // That said, this is 100% a kludge, since it involves duplicating find + // logic on a completely unrelated context. + { + dependencies: [input.myself(), 'trackData', 'originalReleaseTrack'], + compute: (continuation, { + [input.myself()]: thisTrack, + ['trackData']: trackData, + ['originalReleaseTrack']: ref, + }) => continuation({ + ['#originalRelease']: + (ref.startsWith('track:') + ? trackData.find(track => track.directory === ref.slice('track:'.length)) + : trackData.find(track => + track !== thisTrack && + !CacheableObject.getUpdateValue(track, 'originalReleaseTrack') && + track.name.toLowerCase() === ref.toLowerCase())), + }) + }, + exitWithoutDependency({ dependency: '#originalRelease', value: input.value(false), -- cgit 1.3.0-6-gf8a5 From 44e47fb3316c5452d277166215bc7522b404047f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 2 Oct 2023 10:41:42 -0300 Subject: data: custom cache for validateWikiData --- src/data/things/validators.js | 72 ++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index bdb22058..ee301f15 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -405,6 +405,8 @@ export function validateReferenceList(type = '') { return validateArrayItems(validateReference(type)); } +const validateWikiData_cache = {}; + export function validateWikiData({ referenceType = '', allowMixedTypes = false, @@ -413,51 +415,63 @@ export function validateWikiData({ throw new TypeError(`Don't specify both referenceType and allowMixedTypes`); } + validateWikiData_cache[referenceType] ??= {}; + validateWikiData_cache[referenceType][allowMixedTypes] ??= new WeakMap(); + const isArrayOfObjects = validateArrayItems(isObject); return (array) => { - isArrayOfObjects(array); + const subcache = validateWikiData_cache[referenceType][allowMixedTypes]; + if (subcache.has(array)) return subcache.get(array); - if (empty(array)) { - return true; - } + let OK = false; - const allRefTypes = - new Set(array.map(object => - object.constructor[Symbol.for('Thing.referenceType')])); + try { + isArrayOfObjects(array); - if (allRefTypes.has(undefined)) { - if (allRefTypes.size === 1) { - throw new TypeError(`Expected array of wiki data objects, got array of other objects`); - } else { - throw new TypeError(`Expected array of wiki data objects, got mixed items`); + if (empty(array)) { + OK = true; return true; } - } - if (allRefTypes.size > 1) { - if (allowMixedTypes) { - return true; + const allRefTypes = + new Set(array.map(object => + object.constructor[Symbol.for('Thing.referenceType')])); + + if (allRefTypes.has(undefined)) { + if (allRefTypes.size === 1) { + throw new TypeError(`Expected array of wiki data objects, got array of other objects`); + } else { + throw new TypeError(`Expected array of wiki data objects, got mixed items`); + } } - const types = () => Array.from(allRefTypes).join(', '); + if (allRefTypes.size > 1) { + if (allowMixedTypes) { + OK = true; return true; + } - if (referenceType) { - if (allRefTypes.has(referenceType)) { - allRefTypes.remove(referenceType); - throw new TypeError(`Expected array of only ${referenceType}, also got other types: ${types()}`) - } else { - throw new TypeError(`Expected array of only ${referenceType}, got other types: ${types()}`); + const types = () => Array.from(allRefTypes).join(', '); + + if (referenceType) { + if (allRefTypes.has(referenceType)) { + allRefTypes.remove(referenceType); + throw new TypeError(`Expected array of only ${referenceType}, also got other types: ${types()}`) + } else { + throw new TypeError(`Expected array of only ${referenceType}, got other types: ${types()}`); + } } + + throw new TypeError(`Expected array of unmixed reference types, got multiple: ${types()}`); } - throw new TypeError(`Expected array of unmixed reference types, got multiple: ${types()}`); - } + if (referenceType && !allRefTypes.has(referenceType)) { + throw new TypeError(`Expected array of ${referenceType}, got array of ${allRefTypes[0]}`) + } - if (referenceType && !allRefTypes.has(referenceType)) { - throw new TypeError(`Expected array of ${referenceType}, got array of ${allRefTypes[0]}`) + OK = true; return true; + } finally { + subcache.set(array, OK); } - - return true; }; } -- cgit 1.3.0-6-gf8a5 From 24fc9a842159c16a2d4bb3dc01c5640218caff06 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 2 Oct 2023 11:03:28 -0300 Subject: find: use more reasonable getMatchableNames default Also warn with some details when getMatchableNames returns values besides strings. This was getting caught on links to listings, which don't have (data-represented) names. --- src/find.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/find.js b/src/find.js index 78775457..c8edce98 100644 --- a/src/find.js +++ b/src/find.js @@ -16,7 +16,10 @@ function warnOrThrow(mode, message) { } export function processAllAvailableMatches(data, { - getMatchableNames = thing => [thing.name], + getMatchableNames = thing => + (Object.hasOwn(thing, 'name') + ? [thing.name] + : []), } = {}) { const byName = Object.create(null); const byDirectory = Object.create(null); @@ -24,6 +27,11 @@ export function processAllAvailableMatches(data, { for (const thing of data) { for (const name of getMatchableNames(thing)) { + if (typeof name !== 'string') { + logWarn`Unexpected ${typeAppearance(name)} returned in names for ${inspect(thing)}`; + continue; + } + const normalizedName = name.toLowerCase(); if (normalizedName in byName) { byName[normalizedName] = null; -- cgit 1.3.0-6-gf8a5 From 97d9a13846654b8fa5b7520254f0f5ebc575305b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 2 Oct 2023 17:09:26 -0300 Subject: write: live-dev-server: default to quiet responses --- src/write/build-modes/live-dev-server.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 3986de32..d4efd177 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -42,8 +42,8 @@ export function getCLIOptions() { }, }, - 'quiet-responses': { - help: `Disables outputting [200] and [404] responses in the server log`, + 'loud-responses': { + help: `Enables outputting [200] and [404] responses in the server log, which are suppressed by default`, type: 'flag', }, }; @@ -78,7 +78,7 @@ export async function go({ const host = cliOptions['host'] ?? defaultHost; const port = parseInt(cliOptions['port'] ?? defaultPort); - const quietResponses = cliOptions['quiet-responses'] ?? false; + const loudResponses = cliOptions['loud-responses'] ?? false; const contentDependenciesWatcher = await watchContentDependencies(); const {contentDependencies} = contentDependenciesWatcher; @@ -160,7 +160,7 @@ export async function go({ }); response.writeHead(200, contentTypeJSON); response.end(json); - if (!quietResponses) console.log(`${requestHead} [200] /data.json`); + if (loudResponses) console.log(`${requestHead} [200] /data.json`); } catch (error) { response.writeHead(500, contentTypeJSON); response.end(`Internal error serializing wiki JSON`); @@ -256,7 +256,7 @@ export async function go({ await pipeline( createReadStream(filePath), response); - if (!quietResponses) console.log(`${requestHead} [200] ${pathname}`); + if (loudResponses) console.log(`${requestHead} [200] ${pathname}`); } catch (error) { response.writeHead(500, contentTypePlain); response.end(`Failed during file-to-response pipeline`); @@ -274,7 +274,7 @@ export async function go({ if (!Object.hasOwn(urlToPageMap, pathnameKey)) { response.writeHead(404, contentTypePlain); response.end(`No page found for: ${pathnameKey}\n`); - if (!quietResponses) console.log(`${requestHead} [404] ${pathname}`); + if (loudResponses) console.log(`${requestHead} [404] ${pathname}`); return; } @@ -358,7 +358,7 @@ export async function go({ const {pageHTML} = html.resolve(topLevelResult); - if (!quietResponses) console.log(`${requestHead} [200] ${pathname}`); + if (loudResponses) console.log(`${requestHead} [200] ${pathname}`); response.writeHead(200, contentTypeHTML); response.end(pageHTML); } catch (error) { @@ -388,8 +388,11 @@ export async function go({ server.on('listening', () => { logInfo`${'All done!'} Listening at: ${address}`; logInfo`Press ^C here (control+C) to stop the server and exit.`; - if (quietResponses) { - logInfo`Suppressing [200] and [404] response logging.`; + if (loudResponses) { + logInfo`Printing [200] and [404] responses.` + } else { + logInfo`Suppressing [200] and [404] response logging.` + logInfo`(Pass --loud-responses to show these.)`; } }); -- cgit 1.3.0-6-gf8a5 From d21a899ec62d0298f298a9fbdd8b74e5bc44c681 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 6 Oct 2023 09:01:35 -0300 Subject: css: intervene with default scrollbar styling on sticky sidebar --- src/static/site4.css | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/static/site4.css b/src/static/site4.css index 0e6166b4..ab17bf0c 100644 --- a/src/static/site4.css +++ b/src/static/site4.css @@ -1451,12 +1451,27 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r .sidebar-column.sidebar.sticky-column { max-height: calc(100vh - 20px); - overflow-y: scroll; align-self: start; padding-bottom: 0; box-sizing: border-box; flex-basis: 275px; padding-top: 0; + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: var(--dark-color); +} + +.sidebar-column.sidebar.sticky-column::-webkit-scrollbar { + background: var(--dark-color); + width: 12px; +} + +.sidebar-column.sidebar.sticky-column::-webkit-scrollbar-thumb { + transition: background 0.2s; + background: rgba(255, 255, 255, 0.2); + border: 3px solid transparent; + border-radius: 10px; + background-clip: content-box; } .sidebar-column.sidebar.sticky-column > h1 { -- cgit 1.3.0-6-gf8a5 From b37d81240307f3e38faaa781c3932feff53e9aac Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 6 Oct 2023 12:06:15 -0300 Subject: data, test: fix track contribs not inheriting properly --- src/data/things/track.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/things/track.js b/src/data/things/track.js index 193ad891..db325a17 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -178,7 +178,10 @@ export class Track extends Thing { '#resolvedContribs': '#artistContribs', }), - exposeDependencyOrContinue({dependency: '#artistContribs'}), + exposeDependencyOrContinue({ + dependency: '#artistContribs', + mode: input.value('empty'), + }), withPropertyFromAlbum({ property: input.value('artistContribs'), @@ -199,7 +202,9 @@ export class Track extends Thing { // typically varies by release and isn't defined by the musical qualities // of the track. coverArtistContribs: [ - exitWithoutUniqueCoverArt(), + exitWithoutUniqueCoverArt({ + value: input.value([]), + }), withResolvedContribs({ from: input.updateValue({validate: isContributionList}), @@ -207,7 +212,10 @@ export class Track extends Thing { '#resolvedContribs': '#coverArtistContribs', }), - exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), + exposeDependencyOrContinue({ + dependency: '#coverArtistContribs', + mode: input.value('empty'), + }), withPropertyFromAlbum({ property: input.value('trackCoverArtistContribs'), -- cgit 1.3.0-6-gf8a5 From 08238e0673cc12f642bed0ad0a8f78d784a064df Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 6 Oct 2023 18:35:19 -0300 Subject: content: generateAlbumGalleryPage: stub album galleries --- .../generateAlbumGalleryNoTrackArtworksLine.js | 7 +++++++ src/content/dependencies/generateAlbumGalleryPage.js | 7 +++++++ src/content/dependencies/generateAlbumNavAccent.js | 16 ++++++++-------- src/page/album.js | 3 +-- src/strings-default.json | 1 + 5 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js (limited to 'src') diff --git a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js new file mode 100644 index 00000000..ad99cb87 --- /dev/null +++ b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js @@ -0,0 +1,7 @@ +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 b98abc46..9551eb98 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -3,6 +3,7 @@ import {compareArrays, stitchArrays} from '#sugar'; export default { contentDependencies: [ 'generateAlbumGalleryCoverArtistsLine', + 'generateAlbumGalleryNoTrackArtworksLine', 'generateAlbumGalleryStatsLine', 'generateAlbumNavAccent', 'generateAlbumStyleRules', @@ -62,6 +63,11 @@ export default { relations.statsLine = relation('generateAlbumGalleryStatsLine', album); + if (album.tracks.every(track => !track.hasUniqueCoverArt)) { + relations.noTrackArtworksLine = + relation('generateAlbumGalleryNoTrackArtworksLine'); + } + if (query.coverArtistsForAllTracks) { relations.coverArtistsLine = relation('generateAlbumGalleryCoverArtistsLine', query.coverArtistsForAllTracks); @@ -131,6 +137,7 @@ export default { mainContent: [ relations.statsLine, relations.coverArtistsLine, + relations.noTrackArtworksLine, relations.coverGrid .slots({ diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index c79219bb..7eb1dac0 100644 --- a/src/content/dependencies/generateAlbumNavAccent.js +++ b/src/content/dependencies/generateAlbumNavAccent.js @@ -33,10 +33,8 @@ export default { } } - if (album.tracks.some(t => t.hasUniqueCoverArt)) { - relations.albumGalleryLink = - relation('linkAlbumGallery', album); - } + relations.albumGalleryLink = + relation('linkAlbumGallery', album); if (album.commentary || album.tracks.some(t => t.commentary)) { relations.albumCommentaryLink = @@ -49,6 +47,7 @@ export default { data(album, track) { return { hasMultipleTracks: album.tracks.length > 1, + galleryIsStub: album.tracks.every(t => !t.hasUniqueCoverArt), isTrackPage: !!track, }; }, @@ -66,10 +65,11 @@ export default { const {content: extraLinks = []} = slots.showExtraLinks && {content: [ - relations.albumGalleryLink?.slots({ - attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('albumPage.nav.gallery'), - }), + (!data.galleryIsStub || slots.currentExtra === 'gallery') && + relations.albumGalleryLink?.slots({ + attributes: {class: slots.currentExtra === 'gallery' && 'current'}, + content: language.$('albumPage.nav.gallery'), + }), relations.albumCommentaryLink?.slots({ attributes: {class: slots.currentExtra === 'commentary' && 'current'}, diff --git a/src/page/album.js b/src/page/album.js index 69fcabcf..af410763 100644 --- a/src/page/album.js +++ b/src/page/album.js @@ -5,7 +5,6 @@ export function targets({wikiData}) { } export function pathsForTarget(album) { - const hasGalleryPage = album.tracks.some(t => t.hasUniqueCoverArt); const hasCommentaryPage = !!album.commentary || album.tracks.some(t => t.commentary); return [ @@ -19,7 +18,7 @@ export function pathsForTarget(album) { }, }, - hasGalleryPage && { + { type: 'page', path: ['albumGallery', album.directory], diff --git a/src/strings-default.json b/src/strings-default.json index 0ad7a516..af44fc7e 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -267,6 +267,7 @@ "albumGalleryPage.statsLine": "{TRACKS} totaling {DURATION}.", "albumGalleryPage.statsLine.withDate": "{TRACKS} totaling {DURATION}. Released {DATE}.", "albumGalleryPage.coverArtistsLine": "All track artwork by {ARTISTS}.", + "albumGalleryPage.noTrackArtworksLine": "This album doesn't have any track artwork.", "albumCommentaryPage.title": "{ALBUM} - Commentary", "albumCommentaryPage.infoLine": "{WORDS} across {ENTRIES}.", "albumCommentaryPage.nav.album": "Album: {ALBUM}", -- cgit 1.3.0-6-gf8a5 From d7dcbbfe67ab7e8728da3f0503a6b1a20e9e2664 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 6 Oct 2023 18:37:08 -0300 Subject: content, css: supporting changes for better secondary nav dynamics --- src/content/dependencies/generatePageLayout.js | 47 +- src/static/site4.css | 1745 ------------------------ src/static/site5.css | 1745 ++++++++++++++++++++++++ src/upd8.js | 2 +- 4 files changed, 1774 insertions(+), 1765 deletions(-) delete mode 100644 src/static/site4.css create mode 100644 src/static/site5.css (limited to 'src') diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 5377f80d..cd831ba7 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -394,6 +394,10 @@ export default { const sidebarLeftHTML = generateSidebarHTML('leftSidebar', 'sidebar-left'); const sidebarRightHTML = generateSidebarHTML('rightSidebar', 'sidebar-right'); + + const hasSidebarLeft = !html.isBlank(sidebarLeftHTML); + const hasSidebarRight = !html.isBlank(sidebarRightHTML); + const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse; const hasID = (() => { @@ -422,20 +426,20 @@ export default { processSkippers([ {condition: true, id: 'content', string: 'content'}, { - condition: !html.isBlank(sidebarLeftHTML), + condition: hasSidebarLeft, id: 'sidebar-left', string: - (html.isBlank(sidebarRightHTML) - ? 'sidebar' - : 'sidebar.left'), + (hasSidebarRight + ? 'sidebar.left' + : 'sidebar'), }, { - condition: !html.isBlank(sidebarRightHTML), + condition: hasSidebarRight, id: 'sidebar-right', string: - (html.isBlank(sidebarLeftHTML) - ? 'sidebar' - : 'sidebar.right'), + (hasSidebarLeft + ? 'sidebar.right' + : 'sidebar'), }, {condition: navHTML, id: 'header', string: 'header'}, {condition: footerHTML, id: 'footer', string: 'footer'}, @@ -507,11 +511,6 @@ export default { class: [ 'layout-columns', !collapseSidebars && 'vertical-when-thin', - (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar', - (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars', - !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars', - sidebarLeftHTML && 'has-sidebar-left', - sidebarRightHTML && 'has-sidebar-right', ], }, [ @@ -609,7 +608,7 @@ export default { html.tag('link', { rel: 'stylesheet', - href: to('shared.staticFile', 'site4.css', cachebust), + href: to('shared.staticFile', 'site5.css', cachebust), }), html.tag('style', [ @@ -624,12 +623,22 @@ export default { ]), html.tag('body', - // {style: body.style || ''}, [ - html.tag('div', {id: 'page-container'}, [ - skippersHTML, - layoutHTML, - ]), + html.tag('div', + { + id: 'page-container', + class: [ + (hasSidebarLeft || hasSidebarRight) && 'has-one-sidebar', + (hasSidebarLeft && hasSidebarRight) && 'has-two-sidebars', + !(hasSidebarLeft || hasSidebarRight) && 'has-zero-sidebars', + hasSidebarLeft && 'has-sidebar-left', + hasSidebarRight && 'has-sidebar-right', + ], + }, + [ + skippersHTML, + layoutHTML, + ]), // infoCardHTML, imageOverlayHTML, diff --git a/src/static/site4.css b/src/static/site4.css deleted file mode 100644 index ab17bf0c..00000000 --- a/src/static/site4.css +++ /dev/null @@ -1,1745 +0,0 @@ -/* A frontend file! Wow. - * This file is just loaded statically 8y <link>s in the HTML files, so there's - * no need to re-run upd8.js when tweaking values here. Handy! - */ - -:root { - --primary-color: #0088ff; -} - -/* Layout - Common - * - */ - -body { - margin: 10px; - overflow-y: scroll; -} - -body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: -1; - - /* NB: these are 100 LVW, "largest view width", etc. - * Stabilizes background on viewports with modal dimensions, - * e.g. expanding/shrinking tab bar or collapsible find bar. - * 100% dimensions are kept above for browser compatibility. - */ - width: 100lvw; - height: 100lvh; -} - -#page-container { - max-width: 1100px; - margin: 10px auto 50px; - padding: 15px 0; -} - -#page-container > * { - margin-left: 15px; - margin-right: 15px; -} - -#skippers:focus-within { - position: static; - width: unset; - height: unset; -} - -#banner { - margin: 10px 0; - width: 100%; - position: relative; -} - -#banner::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -#banner img { - display: block; - width: 100%; - height: auto; -} - -#skippers { - position: absolute; - left: -10000px; - top: auto; - width: 1px; - height: 1px; -} - -.layout-columns { - display: flex; - align-items: stretch; -} - -#header, -#secondary-nav, -#skippers, -#footer { - padding: 5px; -} - -#header, -#secondary-nav, -#skippers { - margin-bottom: 10px; -} - -#footer { - margin-top: 10px; -} - -#header { - display: grid; -} - -#header.nav-has-main-links.nav-has-content { - grid-template-columns: 2.5fr 3fr; - grid-template-rows: min-content 1fr; - grid-template-areas: - "main-links content" - "bottom-row content"; -} - -#header.nav-has-main-links:not(.nav-has-content) { - grid-template-columns: 1fr; - grid-template-areas: - "main-links" - "bottom-row"; -} - -.nav-main-links { - grid-area: main-links; - margin-right: 20px; -} - -.nav-content { - grid-area: content; -} - -.nav-bottom-row { - grid-area: bottom-row; - align-self: start; -} - -.sidebar-column { - flex: 1 1 20%; - min-width: 150px; - max-width: 250px; - flex-basis: 250px; - align-self: flex-start; -} - -.sidebar-column.wide { - max-width: 350px; - flex-basis: 300px; - flex-shrink: 0; - flex-grow: 1; -} - -.sidebar-multiple { - display: flex; - flex-direction: column; -} - -.sidebar-multiple .sidebar:not(:first-child) { - margin-top: 15px; -} - -.sidebar { - --content-padding: 5px; - padding: var(--content-padding); -} - -#sidebar-left { - margin-right: 10px; -} - -#sidebar-right { - margin-left: 10px; -} - -#content { - position: relative; - --content-padding: 20px; - box-sizing: border-box; - padding: var(--content-padding); - flex-grow: 1; - flex-shrink: 3; -} - -.footer-content { - margin: 5px 12%; -} - -.footer-content > :first-child { - margin-top: 0; -} - -.footer-content > :last-child { - margin-bottom: 0; -} - -.footer-localization-links { - margin: 5px 12%; -} - -/* Design & Appearance - Layout elements */ - -body { - background: black; -} - -body::before { - background-image: url("../media/bg.jpg"); - background-position: center; - background-size: cover; - opacity: 0.5; -} - -#page-container { - background-color: var(--bg-color, rgba(35, 35, 35, 0.8)); - color: #ffffff; - box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); -} - -#skippers > * { - display: inline-block; -} - -#skippers > .skipper-list:not(:last-child)::after { - display: inline-block; - content: "\00a0"; - margin-left: 2px; - margin-right: -2px; - border-left: 1px dotted; -} - -#skippers .skipper-list > .skipper:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -#banner { - background: black; - background-color: var(--dim-color); - border-bottom: 1px solid var(--primary-color); -} - -#banner::after { - box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35); - pointer-events: none; -} - -#banner.dim img { - opacity: 0.8; -} - -#header, -#secondary-nav, -#skippers, -#footer, -.sidebar { - font-size: 0.85em; -} - -.sidebar, -#content, -#header, -#secondary-nav, -#skippers, -#footer { - background-color: rgba(0, 0, 0, 0.6); - border: 1px dotted var(--primary-color); - border-radius: 3px; - transition: background-color 0.2s; -} - -/* -.sidebar:focus-within, -#content:focus-within, -#header:focus-within, -#secondary-nav:focus-within, -#skippers:focus-within, -#footer:focus-within { - background-color: rgba(0, 0, 0, 0.85); - border-style: solid; -} -*/ - -.sidebar > h1, -.sidebar > h2, -.sidebar > h3, -.sidebar > p { - text-align: center; -} - -.sidebar h1 { - font-size: 1.25em; -} - -.sidebar h2 { - font-size: 1.1em; - margin: 0; -} - -.sidebar h3 { - font-size: 1.1em; - font-style: oblique; - font-variant: small-caps; - margin-top: 0.3em; - margin-bottom: 0em; -} - -.sidebar > p { - margin: 0.5em 0; - padding: 0 5px; -} - -.sidebar hr { - color: #555; - margin: 10px 5px; -} - -.sidebar > ol, -.sidebar > ul { - padding-left: 30px; - padding-right: 15px; -} - -.sidebar > dl { - padding-right: 15px; - padding-left: 0; -} - -.sidebar > dl dt { - padding-left: 10px; - margin-top: 0.5em; -} - -.sidebar > dl dt.current { - font-weight: 800; -} - -.sidebar > dl dd { - margin-left: 0; -} - -.sidebar > dl dd ul { - padding-left: 30px; - margin-left: 0; -} - -.sidebar > dl .side { - padding-left: 10px; -} - -.sidebar li.current { - font-weight: 800; -} - -.sidebar li { - overflow-wrap: break-word; -} - -.sidebar > details.current summary { - font-weight: 800; -} - -.sidebar > details summary { - margin-top: 0.5em; - padding-left: 5px; -} - -summary > span:hover { - cursor: pointer; - text-decoration: underline; - text-decoration-color: var(--primary-color); -} - -summary .group-name { - color: var(--primary-color); -} - -.sidebar > details ul, -.sidebar > details ol { - margin-top: 0; - margin-bottom: 0; -} - -.sidebar > details:last-child { - margin-bottom: 10px; -} - -.sidebar > details[open] { - margin-bottom: 1em; -} - -.sidebar article { - text-align: left; - margin: 5px 5px 15px 5px; -} - -.sidebar article:last-child { - margin-bottom: 5px; -} - -.sidebar article h2, -.news-index h2 { - border-bottom: 1px dotted; -} - -.sidebar article h2 time, -.news-index time { - float: right; - font-weight: normal; -} - -#content { - overflow-wrap: anywhere; -} - -footer { - text-align: center; - font-style: oblique; -} - -.footer-localization-links > span:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -/* Design & Appearance - Content elements */ - -a { - color: var(--primary-color); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -a.current { - font-weight: 800; -} - -.nav-main-links > span > span { - white-space: nowrap; -} - -.nav-main-links > span.current > span.nav-link-content > a { - font-weight: 800; -} - -.nav-links-index > span:not(:first-child):not(.no-divider)::before, -.nav-links-groups > span:not(:first-child):not(.no-divider)::before { - content: "\0020\00b7\0020"; - font-weight: 800; -} - -.nav-links-hierarchical > span:not(:first-child):not(.no-divider)::before { - content: "\0020/\0020"; -} - -#header .chronology .heading, -#header .chronology .buttons { - white-space: nowrap; -} - -#secondary-nav { - text-align: center; -} - -.nowrap { - white-space: nowrap; -} - -.icons { - font-style: normal; - white-space: nowrap; -} - -.icon { - display: inline-block; - width: 24px; - height: 1em; - position: relative; -} - -.icon > svg { - width: 24px; - height: 24px; - top: -0.25em; - position: absolute; - fill: var(--primary-color); -} - -.rerelease, -.other-group-accent { - opacity: 0.7; - font-style: oblique; -} - -.other-group-accent { - white-space: nowrap; -} - -.content-columns { - columns: 2; -} - -.content-columns .column { - break-inside: avoid; -} - -.content-columns .column h2 { - margin-top: 0; - font-size: 1em; -} - -p .current { - font-weight: 800; -} - -#cover-art-container { - font-size: 0.8em; -} - -#cover-art .square { - box-shadow: 0 0 3px 6px rgba(0, 0, 0, 0.35); -} - -#cover-art img { - display: block; - width: 100%; - height: 100%; -} - -#cover-art-container p { - margin-top: 5px; -} - -.commentary-art { - float: right; - width: 30%; - max-width: 250px; - margin: 15px 0 10px 20px; -} - -.js-hide, -.js-show-once-data, -.js-hide-once-data { - display: none; -} - -.content-image { - margin-top: 1em; - margin-bottom: 1em; -} - -a.box:focus { - outline: 3px double var(--primary-color); -} - -a.box:focus:not(:focus-visible) { - outline: none; -} - -a.box img { - display: block; - max-width: 100%; - height: auto; -} - -.square .image-container { - width: 100%; - height: 100%; -} - -h1 { - font-size: 1.5em; -} - -#content li { - margin-bottom: 4px; -} - -#content li i { - white-space: nowrap; -} - -#content.top-index h1, -#content.flash-index h1 { - text-align: center; - font-size: 2em; -} - -html[data-url-key="localized.home"] #content h1 { - text-align: center; - font-size: 2.5em; -} - -#content.flash-index h2 { - text-align: center; - font-size: 2.5em; - font-variant: small-caps; - font-style: oblique; - margin-bottom: 0; - text-align: center; - width: 100%; -} - -#content.top-index h2 { - text-align: center; - font-size: 2em; - font-weight: normal; - margin-bottom: 0.25em; -} - -.quick-info { - text-align: center; -} - -ul.quick-info { - list-style: none; - padding-left: 0; -} - -ul.quick-info li { - display: inline-block; -} - -ul.quick-info li:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -.carousel-container + .quick-info { - margin-top: 25px; -} - -#intro-menu { - margin: 24px 0; - padding: 10px; - background-color: #222222; - text-align: center; - border: 1px dotted var(--primary-color); - border-radius: 2px; -} - -#intro-menu p { - margin: 12px 0; -} - -#intro-menu a { - margin: 0 6px; -} - -li .by { - display: inline-block; - font-style: oblique; -} - -li .by a { - display: inline-block; -} - -p code { - font-size: 1em; - font-family: "courier new"; - font-weight: 800; -} - -#content blockquote { - margin-left: 40px; - max-width: 600px; - margin-right: 0; -} - -#content blockquote blockquote { - margin-left: 10px; - padding-left: 10px; - margin-right: 20px; - border-left: dotted 1px; - padding-top: 6px; - padding-bottom: 6px; -} - -#content blockquote blockquote > :first-child { - margin-top: 0; -} - -#content blockquote blockquote > :last-child { - margin-bottom: 0; -} - -main.long-content .main-content-container, -main.long-content > h1 { - padding-left: 12%; - padding-right: 12%; -} - -dl dt { - padding-left: 40px; - max-width: 600px; -} - -dl dt { - margin-bottom: 2px; -} - -dl dd { - margin-bottom: 1em; -} - -dl ul, -dl ol { - margin-top: 0; - margin-bottom: 0; -} - -ul > li.has-details { - list-style-type: none; - margin-left: -17px; -} - -.album-group-list dt { - font-style: oblique; - padding-left: 0; -} - -.album-group-list dd { - margin-left: 0; -} - -.group-chronology-link { - font-style: oblique; -} - -#content hr { - border: 1px inset #808080; - width: 100%; -} - -#content hr.split::before { - content: "(split)"; - color: #808080; -} - -#content hr.split { - position: relative; - overflow: hidden; - border: none; -} - -#content hr.split::after { - display: inline-block; - content: ""; - border: 1px inset #808080; - width: 100%; - position: absolute; - top: 50%; - margin-top: -2px; - margin-left: 10px; -} - -li > ul { - margin-top: 5px; -} - -.group-contributions-table { - display: inline-block; -} - -.group-contributions-table .group-contributions-row { - display: flex; - justify-content: space-between; -} - -.group-contributions-table .group-contributions-metrics { - margin-left: 1.5ch; - white-space: nowrap; -} - -.group-contributions-sorted-by-count:not(.visible), -.group-contributions-sorted-by-duration:not(.visible) { - display: none; -} - -/* Images */ - -.image-container { - border: 2px solid var(--primary-color); - box-sizing: border-box; - position: relative; - padding: 5px; - text-align: left; - background-color: var(--dim-color); - color: white; - display: inline-block; - height: 100%; -} - -.image-text-area { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - padding: 5px 15px; - background: rgba(0, 0, 0, 0.65); - box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset; - line-height: 1.35em; - color: var(--primary-color); - font-style: oblique; - text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75); -} - -.image-inner-area { - width: 100%; - height: 100%; -} - -img { - object-fit: cover; -} - -.reveal { - position: relative; - width: 100%; - height: 100%; -} - -.reveal img { - filter: blur(20px); - opacity: 0.4; -} - -.reveal-text-container { - position: absolute; - top: 15px; - left: 10px; - right: 10px; - bottom: 10px; - display: flex; - flex-direction: column; - justify-content: flex-start; -} - -.reveal-text { - color: white; - text-align: center; - font-weight: bold; -} - -.reveal-interaction { - opacity: 0.8; -} - -.reveal.revealed img { - filter: none; - opacity: 1; -} - -.reveal.revealed .reveal-text { - display: none; -} - -.sidebar .image-container { - max-width: 350px; -} - -/* Grid listings */ - -.grid-listing { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: flex-start; - padding: 5px 15px; -} - -.grid-item { - font-size: 0.9em; -} - -.grid-item { - display: inline-block; - text-align: center; - background-color: #111111; - border: 1px dotted var(--primary-color); - border-radius: 2px; - padding: 5px; - margin: 10px; -} - -.grid-item img { - width: 100%; - height: 100% !important; - margin-top: auto; - margin-bottom: auto; -} - -.grid-item:hover { - text-decoration: none; -} - -.grid-actions .grid-item:hover { - text-decoration: underline; -} - -.grid-item > span { - display: block; - overflow-wrap: break-word; - hyphens: auto; -} - -.grid-item > span:not(:first-child) { - margin-top: 2px; -} - -.grid-item > span:first-of-type { - margin-top: 6px; -} - -.grid-item > span:not(:first-of-type) { - font-size: 0.9em; - opacity: 0.8; -} - -.grid-item:hover > span:first-of-type { - text-decoration: underline; -} - -.grid-listing > .grid-item { - flex: 1 25%; - max-width: 200px; -} - -.grid-actions { - display: flex; - flex-direction: row; - margin: 15px; - align-self: center; - flex-wrap: wrap; - justify-content: center; -} - -.grid-actions > .grid-item { - flex-basis: unset !important; - margin: 5px; - width: 120px; - --primary-color: inherit !important; - --dim-color: inherit !important; -} - -/* Carousel */ - -.carousel-container { - --carousel-tile-min-width: 120px; - --carousel-row-count: 3; - --carousel-column-count: 6; - - position: relative; - overflow: hidden; - margin: 20px 0 5px 0; - padding: 8px 0; -} - -.carousel-container::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: -20; - background-color: var(--dim-color); - filter: brightness(0.6); -} - -.carousel-container::after { - content: ""; - pointer-events: none; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: 1px solid var(--primary-color); - border-radius: 4px; - z-index: 40; - box-shadow: - inset 20px 2px 40px var(--shadow-color), - inset -20px -2px 40px var(--shadow-color); -} - -.carousel-container:hover .carousel-grid { - animation-play-state: running; -} - -html[data-url-key="localized.home"] .carousel-container { - --carousel-tile-size: 140px; -} - -.carousel-container[data-carousel-rows="1"] { --carousel-row-count: 1; } -.carousel-container[data-carousel-rows="2"] { --carousel-row-count: 2; } -.carousel-container[data-carousel-rows="3"] { --carousel-row-count: 3; } -.carousel-container[data-carousel-columns="4"] { --carousel-column-count: 4; } -.carousel-container[data-carousel-columns="5"] { --carousel-column-count: 5; } -.carousel-container[data-carousel-columns="6"] { --carousel-column-count: 6; } - -.carousel-grid:nth-child(2), -.carousel-grid:nth-child(3) { - position: absolute; - top: 8px; - left: 0; - right: 0; -} - -.carousel-grid:nth-child(2) { - animation-name: carousel-marquee2; -} - -.carousel-grid:nth-child(3) { - animation-name: carousel-marquee3; -} - -@keyframes carousel-marquee1 { - 0% { - transform: translateX(-100%) translateX(70px); - } - - 100% { - transform: translateX(-200%) translateX(70px); - } -} - -@keyframes carousel-marquee2 { - 0% { - transform: translateX(0%) translateX(70px); - } - - 100% { - transform: translateX(-100%) translateX(70px); - } -} - -@keyframes carousel-marquee3 { - 0% { - transform: translateX(100%) translateX(70px); - } - - 100% { - transform: translateX(0%) translateX(70px); - } -} - -.carousel-grid { - /* Thanks to: https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ */ - --carousel-gap-count: calc(var(--carousel-column-count) - 1); - --carousel-total-gap-width: calc(var(--carousel-gap-count) * 10px); - --carousel-calculated-tile-max-width: calc((100% - var(--carousel-total-gap-width)) / var(--carousel-column-count)); - - display: grid; - grid-template-columns: repeat(auto-fill, minmax(max(var(--carousel-tile-min-width), var(--carousel-calculated-tile-max-width)), 1fr)); - grid-template-rows: repeat(var(--carousel-row-count), auto); - grid-auto-flow: dense; - grid-auto-rows: 0; - overflow: hidden; - margin: auto; - - transform: translateX(0); - animation: carousel-marquee1 40s linear infinite; - animation-play-state: paused; - z-index: 5; -} - -.carousel-item { - display: inline-block; - margin: 0; - flex: 1 1 150px; - padding: 3px; - border-radius: 10px; - filter: brightness(0.8); -} - -.carousel-item .image-container { - border: none; - padding: 0; -} - -.carousel-item img { - width: 100%; - height: 100%; - margin-top: auto; - margin-bottom: auto; - border-radius: 6px; -} - -.carousel-item:hover { - filter: brightness(1); - background: var(--dim-color); -} - -/* Squares */ - -.square { - position: relative; - width: 100%; -} - -.square::after { - content: ""; - display: block; - padding-bottom: 100%; -} - -.square-content { - position: absolute; - width: 100%; - height: 100%; -} - -/* Info card */ - -#info-card-container { - position: absolute; - - left: 0; - right: 10px; - - pointer-events: none; /* Padding area shouldn't 8e interactive. */ - display: none; -} - -#info-card-container.show, -#info-card-container.hide { - display: flex; -} - -#info-card-container > * { - flex-basis: 400px; -} - -#info-card-container.show { - animation: 0.2s linear forwards info-card-show; - transition: top 0.1s, left 0.1s; -} - -#info-card-container.hide { - animation: 0.2s linear forwards info-card-hide; -} - -@keyframes info-card-show { - 0% { - opacity: 0; - margin-top: -5px; - } - - 100% { - opacity: 1; - margin-top: 0; - } -} - -@keyframes info-card-hide { - 0% { - opacity: 1; - margin-top: 0; - } - - 100% { - opacity: 0; - margin-top: 5px; - display: none !important; - } -} - -.info-card-decor { - padding-left: 3ch; - border-top: 1px solid white; -} - -.info-card { - background-color: black; - color: white; - - border: 1px dotted var(--primary-color); - border-radius: 3px; - box-shadow: 0 5px 5px black; - - padding: 5px; - font-size: 0.9em; - - pointer-events: none; -} - -.info-card::after { - content: ""; - display: block; - clear: both; -} - -#info-card-container.show .info-card { - animation: 0.01s linear 0.2s forwards info-card-become-interactive; -} - -@keyframes info-card-become-interactive { - to { - pointer-events: auto; - } -} - -.info-card-art-container { - float: right; - - width: 40%; - margin: 5px; - font-size: 0.8em; - - /* Dynamically shown. */ - display: none; -} - -.info-card-art-container .image-container { - padding: 2px; -} - -.info-card-art { - display: block; - width: 100%; - height: 100%; -} - -.info-card-name { - font-size: 1em; - border-bottom: 1px dotted; - margin: 0; -} - -.info-card p { - margin-top: 0.25em; - margin-bottom: 0.25em; -} - -.info-card p:last-child { - margin-bottom: 0; -} - -/* Custom hash links */ - -.content-heading { - border-bottom: 3px double transparent; - margin-bottom: -3px; -} - -.content-heading.highlight-hash-link { - animation: highlight-hash-link 4s; - animation-delay: 125ms; -} - -h3.content-heading { - clear: both; -} - -/* This animation's name is referenced in JavaScript */ -@keyframes highlight-hash-link { - 0% { - border-bottom-color: transparent; - } - - 10% { - border-bottom-color: white; - } - - 25% { - border-bottom-color: white; - } - - 100% { - border-bottom-color: transparent; - } -} - -/* Sticky heading */ - -#content [id] { - /* Adjust scroll margin. */ - scroll-margin-top: calc( - 74px /* Sticky heading */ - + 33px /* Sticky subheading */ - - 1em /* One line of text (align bottom) */ - - 12px /* Padding for hanging letters & focus ring */ - ); -} - -.content-sticky-heading-container { - position: sticky; - top: 0; - - margin: calc(-1 * var(--content-padding)); - margin-bottom: calc(0.5 * var(--content-padding)); - - transform: translateY(-5px); -} - -main.long-content .content-sticky-heading-container { - padding-left: 0; - padding-right: 0; -} - -main.long-content .content-sticky-heading-container .content-sticky-heading-row, -main.long-content .content-sticky-heading-container .content-sticky-subheading-row { - padding-left: calc(0.12 * (100% - 2 * var(--content-padding)) + var(--content-padding)); - padding-right: calc(0.12 * (100% - 2 * var(--content-padding)) + var(--content-padding)); -} - -.content-sticky-heading-row { - box-sizing: border-box; - padding: - calc(1.25 * var(--content-padding) + 5px) - 20px - calc(0.75 * var(--content-padding)) - 20px; - - width: 100%; - margin: 0; - - background: var(--bg-black-color); - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - - -webkit-backdrop-filter: blur(6px); - backdrop-filter: blur(6px); -} - -.content-sticky-heading-container.has-cover .content-sticky-heading-row, -.content-sticky-heading-container.has-cover .content-sticky-subheading-row { - display: grid; - grid-template-areas: - "title cover"; - grid-template-columns: 1fr min(40%, 400px); -} - -.content-sticky-heading-row h1 { - margin: 0; - padding-right: 10px; -} - -.content-sticky-heading-cover-container { - position: relative; - height: 0; - margin: -15px 0px -5px -5px; -} - -.content-sticky-heading-cover-needs-reveal { - display: none; -} - -.content-sticky-heading-cover { - position: absolute; - top: 0; - width: 80px; - right: 10px; - box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.25); - transition: transform 0.35s, opacity 0.25s; -} - -.content-sticky-heading-cover-container:not(.visible) .content-sticky-heading-cover { - opacity: 0; - transform: translateY(15px); -} - -.content-sticky-heading-cover .image-container { - border-width: 1px; - padding: 2px; -} - -.content-sticky-heading-cover img { - display: block; - width: 100%; - height: 100%; -} - -.content-sticky-subheading-row { - position: absolute; - width: 100%; - box-sizing: border-box; - padding: 10px 40px 5px 20px; - margin-top: 0; - z-index: -1; - - background: var(--bg-black-color); - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - - -webkit-backdrop-filter: blur(3px); - backdrop-filter: blur(3px); - - transition: margin-top 0.35s, opacity 0.25s; -} - -.content-sticky-subheading-row h2 { - margin: 0; - - font-size: 0.9em !important; - font-weight: normal; - font-style: oblique; - color: #eee; -} - -.content-sticky-subheading-row:not(.visible) { - margin-top: -20px; - opacity: 0; -} - -.content-sticky-heading-container h2.visible { - margin-top: 0; - opacity: 1; -} - -.content-sticky-heading-row { - box-shadow: - inset 0 10px 10px -5px var(--shadow-color), - 0 4px 4px rgba(0, 0, 0, 0.8); -} - -.content-sticky-heading-container h2.visible { - box-shadow: - inset 0 10px 10px -5px var(--shadow-color), - 0 4px 4px rgba(0, 0, 0, 0.8); -} - -#content, .sidebar { - contain: paint; -} - -/* Sticky sidebar */ - -.sidebar-column.sidebar.sticky-column, -.sidebar-column.sidebar.sticky-last, -.sidebar-multiple.sticky-last > .sidebar:last-child, -.sidebar-multiple.sticky-column { - position: sticky; - top: 10px; -} - -.sidebar-multiple.sticky-last { - align-self: stretch; -} - -.sidebar-multiple.sticky-column { - align-self: flex-start; -} - -.sidebar-column.sidebar.sticky-column { - max-height: calc(100vh - 20px); - align-self: start; - padding-bottom: 0; - box-sizing: border-box; - flex-basis: 275px; - padding-top: 0; - overflow-y: scroll; - scrollbar-width: thin; - scrollbar-color: var(--dark-color); -} - -.sidebar-column.sidebar.sticky-column::-webkit-scrollbar { - background: var(--dark-color); - width: 12px; -} - -.sidebar-column.sidebar.sticky-column::-webkit-scrollbar-thumb { - transition: background 0.2s; - background: rgba(255, 255, 255, 0.2); - border: 3px solid transparent; - border-radius: 10px; - background-clip: content-box; -} - -.sidebar-column.sidebar.sticky-column > h1 { - position: sticky; - top: 0; - margin: 0 calc(-1 * var(--content-padding)); - margin-bottom: 10px; - - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - padding: 10px 5px; - - background: var(--bg-black-color); - -webkit-backdrop-filter: blur(3px); - backdrop-filter: blur(3px); -} - -/* Image overlay */ - -#image-overlay-container { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 20px 40px; - box-sizing: border-box; - - opacity: 0; - pointer-events: none; - transition: opacity 0.4s; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -#image-overlay-container.visible { - opacity: 1; - pointer-events: auto; -} - -#image-overlay-content-container { - border-radius: 0 0 8px 8px; - border: 2px solid var(--primary-color); - background: var(--dim-ghost-color); - padding: 3px; - overflow: hidden; - - -webkit-backdrop-filter: blur(3px); - backdrop-filter: blur(3px); -} - -#image-overlay-image-container { - display: block; - position: relative; - overflow: hidden; - width: 80vmin; - height: 80vmin; -} - -#image-overlay-image, -#image-overlay-image-thumb { - display: inline-block; - object-fit: contain; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.65); -} - -#image-overlay-image { - position: absolute; - top: 0; - left: 0; -} - -#image-overlay-image-thumb { - filter: blur(16px); - transform: scale(1.5); -} - -#image-overlay-container.loaded #image-overlay-image-thumb { - opacity: 0; - pointer-events: none; - transition: opacity 0.25s; -} - -#image-overlay-image-container::after { - content: ""; - display: block; - position: absolute; - bottom: 0; - left: 0; - height: 4px; - width: var(--download-progress); - background: var(--primary-color); - box-shadow: 0 -3px 12px 4px var(--primary-color); - transition: 0.25s; -} - -#image-overlay-container.loaded #image-overlay-image-container::after { - width: 100%; - background: white; - opacity: 0; -} - -#image-overlay-container.errored #image-overlay-image-container::after { - width: 100%; - background: red; -} - -#image-overlay-container:not(.visible) #image-overlay-image-container::after { - width: 0 !important; -} - -#image-overlay-action-container { - padding: 4px 4px 6px 4px; - border-radius: 0 0 5px 5px; - background: var(--bg-black-color); - color: white; - font-style: oblique; - text-align: center; -} - -#image-overlay-container #image-overlay-action-content-without-size:not(.visible), -#image-overlay-container #image-overlay-action-content-with-size:not(.visible), -#image-overlay-container #image-overlay-file-size-warning:not(.visible), -#image-overlay-container #image-overlay-file-size-kilobytes:not(.visible), -#image-overlay-container #image-overlay-file-size-megabytes:not(.visible) { - display: none; -} - -#image-overlay-file-size-warning { - opacity: 0.8; - font-size: 0.9em; -} - -/* important easter egg mode */ - -html[data-language-code="preview-en"][data-url-key="localized.home"] #content - h1::after { - font-family: cursive; - display: block; - content: "(Preview Build)"; - font-size: 0.8em; -} - -/* Layout - Wide (most computers) */ - -@media (min-width: 900px) { - #secondary-nav:not(.no-hide) { - display: none; - } -} - -/* Layout - Medium (tablets, some landscape mobiles) - * - * Note: Rules defined here are exclusive to "medium" width, i.e. they don't - * additionally apply to "thin". Use the later section which applies to both - * if so desired. - */ - -@media (min-width: 600px) and (max-width: 899.98px) { -} - -/* Layout - Wide or Medium */ - -@media (min-width: 600px) { - .content-sticky-heading-container { - /* Safari doesn't always play nicely with position: sticky, - * this seems to fix images sometimes displaying above the - * position: absolute subheading (h2) child - * - * See also: https://stackoverflow.com/questions/50224855/ - */ - transform: translate3d(0, 0, 0); - z-index: 1; - } - - /* 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 { - float: right; - width: 40%; - max-width: 400px; - margin: -60px 0 10px 10px; - - position: relative; - z-index: 2; - } - - html[data-url-key="localized.home"] .layout-columns.has-one-sidebar .grid-listing > .grid-item:not(:nth-child(n+10)) { - flex-basis: 23%; - margin: 15px; - } - - html[data-url-key="localized.home"] .layout-columns.has-one-sidebar .grid-listing > .grid-item:nth-child(n+10) { - flex-basis: 18%; - margin: 10px; - } -} - -/* Layout - Medium or Thin */ - -@media (max-width: 899.98px) { - .sidebar-column:not(.no-hide) { - display: none; - } - - #secondary-nav { - display: block; - } - - .layout-columns.vertical-when-thin { - flex-direction: column; - } - - .layout-columns.vertical-when-thin > *:not(:last-child) { - margin-bottom: 10px; - } - - .sidebar-column.no-hide { - max-width: unset !important; - flex-basis: unset !important; - margin-right: 0 !important; - margin-left: 0 !important; - width: 100%; - } - - .sidebar .news-entry:not(.first-news-entry) { - display: none; - } - - .grid-listing > .grid-item { - flex-basis: 40%; - } -} - -/* Layout - Thin (phones) */ - -@media (max-width: 600px) { - .content-columns { - columns: 1; - } - - #cover-art-container { - margin: 25px 0 5px 0; - width: 100%; - max-width: unset; - } - - /* Show sticky heading above cover art */ - - .content-sticky-heading-container { - z-index: 2; - } - - /* Disable grid features, just line header children up vertically */ - - #header { - display: block; - } - - #header > div:not(:first-child) { - margin-top: 0.5em; - } -} diff --git a/src/static/site5.css b/src/static/site5.css new file mode 100644 index 00000000..7b3e3e03 --- /dev/null +++ b/src/static/site5.css @@ -0,0 +1,1745 @@ +/* A frontend file! Wow. + * This file is just loaded statically 8y <link>s in the HTML files, so there's + * no need to re-run upd8.js when tweaking values here. Handy! + */ + +:root { + --primary-color: #0088ff; +} + +/* Layout - Common + * + */ + +body { + margin: 10px; + overflow-y: scroll; +} + +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + + /* NB: these are 100 LVW, "largest view width", etc. + * Stabilizes background on viewports with modal dimensions, + * e.g. expanding/shrinking tab bar or collapsible find bar. + * 100% dimensions are kept above for browser compatibility. + */ + width: 100lvw; + height: 100lvh; +} + +#page-container { + max-width: 1100px; + margin: 10px auto 50px; + padding: 15px 0; +} + +#page-container > * { + margin-left: 15px; + margin-right: 15px; +} + +#skippers:focus-within { + position: static; + width: unset; + height: unset; +} + +#banner { + margin: 10px 0; + width: 100%; + position: relative; +} + +#banner::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +#banner img { + display: block; + width: 100%; + height: auto; +} + +#skippers { + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; +} + +.layout-columns { + display: flex; + align-items: stretch; +} + +#header, +#secondary-nav, +#skippers, +#footer { + padding: 5px; +} + +#header, +#secondary-nav, +#skippers { + margin-bottom: 10px; +} + +#footer { + margin-top: 10px; +} + +#header { + display: grid; +} + +#header.nav-has-main-links.nav-has-content { + grid-template-columns: 2.5fr 3fr; + grid-template-rows: min-content 1fr; + grid-template-areas: + "main-links content" + "bottom-row content"; +} + +#header.nav-has-main-links:not(.nav-has-content) { + grid-template-columns: 1fr; + grid-template-areas: + "main-links" + "bottom-row"; +} + +.nav-main-links { + grid-area: main-links; + margin-right: 20px; +} + +.nav-content { + grid-area: content; +} + +.nav-bottom-row { + grid-area: bottom-row; + align-self: start; +} + +.sidebar-column { + flex: 1 1 20%; + min-width: 150px; + max-width: 250px; + flex-basis: 250px; + align-self: flex-start; +} + +.sidebar-column.wide { + max-width: 350px; + flex-basis: 300px; + flex-shrink: 0; + flex-grow: 1; +} + +.sidebar-multiple { + display: flex; + flex-direction: column; +} + +.sidebar-multiple .sidebar:not(:first-child) { + margin-top: 15px; +} + +.sidebar { + --content-padding: 5px; + padding: var(--content-padding); +} + +#sidebar-left { + margin-right: 10px; +} + +#sidebar-right { + margin-left: 10px; +} + +#content { + position: relative; + --content-padding: 20px; + box-sizing: border-box; + padding: var(--content-padding); + flex-grow: 1; + flex-shrink: 3; +} + +.footer-content { + margin: 5px 12%; +} + +.footer-content > :first-child { + margin-top: 0; +} + +.footer-content > :last-child { + margin-bottom: 0; +} + +.footer-localization-links { + margin: 5px 12%; +} + +/* Design & Appearance - Layout elements */ + +body { + background: black; +} + +body::before { + background-image: url("../media/bg.jpg"); + background-position: center; + background-size: cover; + opacity: 0.5; +} + +#page-container { + background-color: var(--bg-color, rgba(35, 35, 35, 0.8)); + color: #ffffff; + box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); +} + +#skippers > * { + display: inline-block; +} + +#skippers > .skipper-list:not(:last-child)::after { + display: inline-block; + content: "\00a0"; + margin-left: 2px; + margin-right: -2px; + border-left: 1px dotted; +} + +#skippers .skipper-list > .skipper:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; +} + +#banner { + background: black; + background-color: var(--dim-color); + border-bottom: 1px solid var(--primary-color); +} + +#banner::after { + box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35); + pointer-events: none; +} + +#banner.dim img { + opacity: 0.8; +} + +#header, +#secondary-nav, +#skippers, +#footer, +.sidebar { + font-size: 0.85em; +} + +.sidebar, +#content, +#header, +#secondary-nav, +#skippers, +#footer { + background-color: rgba(0, 0, 0, 0.6); + border: 1px dotted var(--primary-color); + border-radius: 3px; + transition: background-color 0.2s; +} + +/* +.sidebar:focus-within, +#content:focus-within, +#header:focus-within, +#secondary-nav:focus-within, +#skippers:focus-within, +#footer:focus-within { + background-color: rgba(0, 0, 0, 0.85); + border-style: solid; +} +*/ + +.sidebar > h1, +.sidebar > h2, +.sidebar > h3, +.sidebar > p { + text-align: center; +} + +.sidebar h1 { + font-size: 1.25em; +} + +.sidebar h2 { + font-size: 1.1em; + margin: 0; +} + +.sidebar h3 { + font-size: 1.1em; + font-style: oblique; + font-variant: small-caps; + margin-top: 0.3em; + margin-bottom: 0em; +} + +.sidebar > p { + margin: 0.5em 0; + padding: 0 5px; +} + +.sidebar hr { + color: #555; + margin: 10px 5px; +} + +.sidebar > ol, +.sidebar > ul { + padding-left: 30px; + padding-right: 15px; +} + +.sidebar > dl { + padding-right: 15px; + padding-left: 0; +} + +.sidebar > dl dt { + padding-left: 10px; + margin-top: 0.5em; +} + +.sidebar > dl dt.current { + font-weight: 800; +} + +.sidebar > dl dd { + margin-left: 0; +} + +.sidebar > dl dd ul { + padding-left: 30px; + margin-left: 0; +} + +.sidebar > dl .side { + padding-left: 10px; +} + +.sidebar li.current { + font-weight: 800; +} + +.sidebar li { + overflow-wrap: break-word; +} + +.sidebar > details.current summary { + font-weight: 800; +} + +.sidebar > details summary { + margin-top: 0.5em; + padding-left: 5px; +} + +summary > span:hover { + cursor: pointer; + text-decoration: underline; + text-decoration-color: var(--primary-color); +} + +summary .group-name { + color: var(--primary-color); +} + +.sidebar > details ul, +.sidebar > details ol { + margin-top: 0; + margin-bottom: 0; +} + +.sidebar > details:last-child { + margin-bottom: 10px; +} + +.sidebar > details[open] { + margin-bottom: 1em; +} + +.sidebar article { + text-align: left; + margin: 5px 5px 15px 5px; +} + +.sidebar article:last-child { + margin-bottom: 5px; +} + +.sidebar article h2, +.news-index h2 { + border-bottom: 1px dotted; +} + +.sidebar article h2 time, +.news-index time { + float: right; + font-weight: normal; +} + +#content { + overflow-wrap: anywhere; +} + +footer { + text-align: center; + font-style: oblique; +} + +.footer-localization-links > span:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; +} + +/* Design & Appearance - Content elements */ + +a { + color: var(--primary-color); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a.current { + font-weight: 800; +} + +.nav-main-links > span > span { + white-space: nowrap; +} + +.nav-main-links > span.current > span.nav-link-content > a { + font-weight: 800; +} + +.nav-links-index > span:not(:first-child):not(.no-divider)::before, +.nav-links-groups > span:not(:first-child):not(.no-divider)::before { + content: "\0020\00b7\0020"; + font-weight: 800; +} + +.nav-links-hierarchical > span:not(:first-child):not(.no-divider)::before { + content: "\0020/\0020"; +} + +#header .chronology .heading, +#header .chronology .buttons { + white-space: nowrap; +} + +#secondary-nav { + text-align: center; +} + +.nowrap { + white-space: nowrap; +} + +.icons { + font-style: normal; + white-space: nowrap; +} + +.icon { + display: inline-block; + width: 24px; + height: 1em; + position: relative; +} + +.icon > svg { + width: 24px; + height: 24px; + top: -0.25em; + position: absolute; + fill: var(--primary-color); +} + +.rerelease, +.other-group-accent { + opacity: 0.7; + font-style: oblique; +} + +.other-group-accent { + white-space: nowrap; +} + +.content-columns { + columns: 2; +} + +.content-columns .column { + break-inside: avoid; +} + +.content-columns .column h2 { + margin-top: 0; + font-size: 1em; +} + +p .current { + font-weight: 800; +} + +#cover-art-container { + font-size: 0.8em; +} + +#cover-art .square { + box-shadow: 0 0 3px 6px rgba(0, 0, 0, 0.35); +} + +#cover-art img { + display: block; + width: 100%; + height: 100%; +} + +#cover-art-container p { + margin-top: 5px; +} + +.commentary-art { + float: right; + width: 30%; + max-width: 250px; + margin: 15px 0 10px 20px; +} + +.js-hide, +.js-show-once-data, +.js-hide-once-data { + display: none; +} + +.content-image { + margin-top: 1em; + margin-bottom: 1em; +} + +a.box:focus { + outline: 3px double var(--primary-color); +} + +a.box:focus:not(:focus-visible) { + outline: none; +} + +a.box img { + display: block; + max-width: 100%; + height: auto; +} + +.square .image-container { + width: 100%; + height: 100%; +} + +h1 { + font-size: 1.5em; +} + +#content li { + margin-bottom: 4px; +} + +#content li i { + white-space: nowrap; +} + +#content.top-index h1, +#content.flash-index h1 { + text-align: center; + font-size: 2em; +} + +html[data-url-key="localized.home"] #content h1 { + text-align: center; + font-size: 2.5em; +} + +#content.flash-index h2 { + text-align: center; + font-size: 2.5em; + font-variant: small-caps; + font-style: oblique; + margin-bottom: 0; + text-align: center; + width: 100%; +} + +#content.top-index h2 { + text-align: center; + font-size: 2em; + font-weight: normal; + margin-bottom: 0.25em; +} + +.quick-info { + text-align: center; +} + +ul.quick-info { + list-style: none; + padding-left: 0; +} + +ul.quick-info li { + display: inline-block; +} + +ul.quick-info li:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; +} + +.carousel-container + .quick-info { + margin-top: 25px; +} + +#intro-menu { + margin: 24px 0; + padding: 10px; + background-color: #222222; + text-align: center; + border: 1px dotted var(--primary-color); + border-radius: 2px; +} + +#intro-menu p { + margin: 12px 0; +} + +#intro-menu a { + margin: 0 6px; +} + +li .by { + display: inline-block; + font-style: oblique; +} + +li .by a { + display: inline-block; +} + +p code { + font-size: 1em; + font-family: "courier new"; + font-weight: 800; +} + +#content blockquote { + margin-left: 40px; + max-width: 600px; + margin-right: 0; +} + +#content blockquote blockquote { + margin-left: 10px; + padding-left: 10px; + margin-right: 20px; + border-left: dotted 1px; + padding-top: 6px; + padding-bottom: 6px; +} + +#content blockquote blockquote > :first-child { + margin-top: 0; +} + +#content blockquote blockquote > :last-child { + margin-bottom: 0; +} + +main.long-content .main-content-container, +main.long-content > h1 { + padding-left: 12%; + padding-right: 12%; +} + +dl dt { + padding-left: 40px; + max-width: 600px; +} + +dl dt { + margin-bottom: 2px; +} + +dl dd { + margin-bottom: 1em; +} + +dl ul, +dl ol { + margin-top: 0; + margin-bottom: 0; +} + +ul > li.has-details { + list-style-type: none; + margin-left: -17px; +} + +.album-group-list dt { + font-style: oblique; + padding-left: 0; +} + +.album-group-list dd { + margin-left: 0; +} + +.group-chronology-link { + font-style: oblique; +} + +#content hr { + border: 1px inset #808080; + width: 100%; +} + +#content hr.split::before { + content: "(split)"; + color: #808080; +} + +#content hr.split { + position: relative; + overflow: hidden; + border: none; +} + +#content hr.split::after { + display: inline-block; + content: ""; + border: 1px inset #808080; + width: 100%; + position: absolute; + top: 50%; + margin-top: -2px; + margin-left: 10px; +} + +li > ul { + margin-top: 5px; +} + +.group-contributions-table { + display: inline-block; +} + +.group-contributions-table .group-contributions-row { + display: flex; + justify-content: space-between; +} + +.group-contributions-table .group-contributions-metrics { + margin-left: 1.5ch; + white-space: nowrap; +} + +.group-contributions-sorted-by-count:not(.visible), +.group-contributions-sorted-by-duration:not(.visible) { + display: none; +} + +/* Images */ + +.image-container { + border: 2px solid var(--primary-color); + box-sizing: border-box; + position: relative; + padding: 5px; + text-align: left; + background-color: var(--dim-color); + color: white; + display: inline-block; + height: 100%; +} + +.image-text-area { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 5px 15px; + background: rgba(0, 0, 0, 0.65); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset; + line-height: 1.35em; + color: var(--primary-color); + font-style: oblique; + text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75); +} + +.image-inner-area { + width: 100%; + height: 100%; +} + +img { + object-fit: cover; +} + +.reveal { + position: relative; + width: 100%; + height: 100%; +} + +.reveal img { + filter: blur(20px); + opacity: 0.4; +} + +.reveal-text-container { + position: absolute; + top: 15px; + left: 10px; + right: 10px; + bottom: 10px; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.reveal-text { + color: white; + text-align: center; + font-weight: bold; +} + +.reveal-interaction { + opacity: 0.8; +} + +.reveal.revealed img { + filter: none; + opacity: 1; +} + +.reveal.revealed .reveal-text { + display: none; +} + +.sidebar .image-container { + max-width: 350px; +} + +/* Grid listings */ + +.grid-listing { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + padding: 5px 15px; +} + +.grid-item { + font-size: 0.9em; +} + +.grid-item { + display: inline-block; + text-align: center; + background-color: #111111; + border: 1px dotted var(--primary-color); + border-radius: 2px; + padding: 5px; + margin: 10px; +} + +.grid-item img { + width: 100%; + height: 100% !important; + margin-top: auto; + margin-bottom: auto; +} + +.grid-item:hover { + text-decoration: none; +} + +.grid-actions .grid-item:hover { + text-decoration: underline; +} + +.grid-item > span { + display: block; + overflow-wrap: break-word; + hyphens: auto; +} + +.grid-item > span:not(:first-child) { + margin-top: 2px; +} + +.grid-item > span:first-of-type { + margin-top: 6px; +} + +.grid-item > span:not(:first-of-type) { + font-size: 0.9em; + opacity: 0.8; +} + +.grid-item:hover > span:first-of-type { + text-decoration: underline; +} + +.grid-listing > .grid-item { + flex: 1 25%; + max-width: 200px; +} + +.grid-actions { + display: flex; + flex-direction: row; + margin: 15px; + align-self: center; + flex-wrap: wrap; + justify-content: center; +} + +.grid-actions > .grid-item { + flex-basis: unset !important; + margin: 5px; + width: 120px; + --primary-color: inherit !important; + --dim-color: inherit !important; +} + +/* Carousel */ + +.carousel-container { + --carousel-tile-min-width: 120px; + --carousel-row-count: 3; + --carousel-column-count: 6; + + position: relative; + overflow: hidden; + margin: 20px 0 5px 0; + padding: 8px 0; +} + +.carousel-container::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -20; + background-color: var(--dim-color); + filter: brightness(0.6); +} + +.carousel-container::after { + content: ""; + pointer-events: none; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px solid var(--primary-color); + border-radius: 4px; + z-index: 40; + box-shadow: + inset 20px 2px 40px var(--shadow-color), + inset -20px -2px 40px var(--shadow-color); +} + +.carousel-container:hover .carousel-grid { + animation-play-state: running; +} + +html[data-url-key="localized.home"] .carousel-container { + --carousel-tile-size: 140px; +} + +.carousel-container[data-carousel-rows="1"] { --carousel-row-count: 1; } +.carousel-container[data-carousel-rows="2"] { --carousel-row-count: 2; } +.carousel-container[data-carousel-rows="3"] { --carousel-row-count: 3; } +.carousel-container[data-carousel-columns="4"] { --carousel-column-count: 4; } +.carousel-container[data-carousel-columns="5"] { --carousel-column-count: 5; } +.carousel-container[data-carousel-columns="6"] { --carousel-column-count: 6; } + +.carousel-grid:nth-child(2), +.carousel-grid:nth-child(3) { + position: absolute; + top: 8px; + left: 0; + right: 0; +} + +.carousel-grid:nth-child(2) { + animation-name: carousel-marquee2; +} + +.carousel-grid:nth-child(3) { + animation-name: carousel-marquee3; +} + +@keyframes carousel-marquee1 { + 0% { + transform: translateX(-100%) translateX(70px); + } + + 100% { + transform: translateX(-200%) translateX(70px); + } +} + +@keyframes carousel-marquee2 { + 0% { + transform: translateX(0%) translateX(70px); + } + + 100% { + transform: translateX(-100%) translateX(70px); + } +} + +@keyframes carousel-marquee3 { + 0% { + transform: translateX(100%) translateX(70px); + } + + 100% { + transform: translateX(0%) translateX(70px); + } +} + +.carousel-grid { + /* Thanks to: https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ */ + --carousel-gap-count: calc(var(--carousel-column-count) - 1); + --carousel-total-gap-width: calc(var(--carousel-gap-count) * 10px); + --carousel-calculated-tile-max-width: calc((100% - var(--carousel-total-gap-width)) / var(--carousel-column-count)); + + display: grid; + grid-template-columns: repeat(auto-fill, minmax(max(var(--carousel-tile-min-width), var(--carousel-calculated-tile-max-width)), 1fr)); + grid-template-rows: repeat(var(--carousel-row-count), auto); + grid-auto-flow: dense; + grid-auto-rows: 0; + overflow: hidden; + margin: auto; + + transform: translateX(0); + animation: carousel-marquee1 40s linear infinite; + animation-play-state: paused; + z-index: 5; +} + +.carousel-item { + display: inline-block; + margin: 0; + flex: 1 1 150px; + padding: 3px; + border-radius: 10px; + filter: brightness(0.8); +} + +.carousel-item .image-container { + border: none; + padding: 0; +} + +.carousel-item img { + width: 100%; + height: 100%; + margin-top: auto; + margin-bottom: auto; + border-radius: 6px; +} + +.carousel-item:hover { + filter: brightness(1); + background: var(--dim-color); +} + +/* Squares */ + +.square { + position: relative; + width: 100%; +} + +.square::after { + content: ""; + display: block; + padding-bottom: 100%; +} + +.square-content { + position: absolute; + width: 100%; + height: 100%; +} + +/* Info card */ + +#info-card-container { + position: absolute; + + left: 0; + right: 10px; + + pointer-events: none; /* Padding area shouldn't 8e interactive. */ + display: none; +} + +#info-card-container.show, +#info-card-container.hide { + display: flex; +} + +#info-card-container > * { + flex-basis: 400px; +} + +#info-card-container.show { + animation: 0.2s linear forwards info-card-show; + transition: top 0.1s, left 0.1s; +} + +#info-card-container.hide { + animation: 0.2s linear forwards info-card-hide; +} + +@keyframes info-card-show { + 0% { + opacity: 0; + margin-top: -5px; + } + + 100% { + opacity: 1; + margin-top: 0; + } +} + +@keyframes info-card-hide { + 0% { + opacity: 1; + margin-top: 0; + } + + 100% { + opacity: 0; + margin-top: 5px; + display: none !important; + } +} + +.info-card-decor { + padding-left: 3ch; + border-top: 1px solid white; +} + +.info-card { + background-color: black; + color: white; + + border: 1px dotted var(--primary-color); + border-radius: 3px; + box-shadow: 0 5px 5px black; + + padding: 5px; + font-size: 0.9em; + + pointer-events: none; +} + +.info-card::after { + content: ""; + display: block; + clear: both; +} + +#info-card-container.show .info-card { + animation: 0.01s linear 0.2s forwards info-card-become-interactive; +} + +@keyframes info-card-become-interactive { + to { + pointer-events: auto; + } +} + +.info-card-art-container { + float: right; + + width: 40%; + margin: 5px; + font-size: 0.8em; + + /* Dynamically shown. */ + display: none; +} + +.info-card-art-container .image-container { + padding: 2px; +} + +.info-card-art { + display: block; + width: 100%; + height: 100%; +} + +.info-card-name { + font-size: 1em; + border-bottom: 1px dotted; + margin: 0; +} + +.info-card p { + margin-top: 0.25em; + margin-bottom: 0.25em; +} + +.info-card p:last-child { + margin-bottom: 0; +} + +/* Custom hash links */ + +.content-heading { + border-bottom: 3px double transparent; + margin-bottom: -3px; +} + +.content-heading.highlight-hash-link { + animation: highlight-hash-link 4s; + animation-delay: 125ms; +} + +h3.content-heading { + clear: both; +} + +/* This animation's name is referenced in JavaScript */ +@keyframes highlight-hash-link { + 0% { + border-bottom-color: transparent; + } + + 10% { + border-bottom-color: white; + } + + 25% { + border-bottom-color: white; + } + + 100% { + border-bottom-color: transparent; + } +} + +/* Sticky heading */ + +#content [id] { + /* Adjust scroll margin. */ + scroll-margin-top: calc( + 74px /* Sticky heading */ + + 33px /* Sticky subheading */ + - 1em /* One line of text (align bottom) */ + - 12px /* Padding for hanging letters & focus ring */ + ); +} + +.content-sticky-heading-container { + position: sticky; + top: 0; + + margin: calc(-1 * var(--content-padding)); + margin-bottom: calc(0.5 * var(--content-padding)); + + transform: translateY(-5px); +} + +main.long-content .content-sticky-heading-container { + padding-left: 0; + padding-right: 0; +} + +main.long-content .content-sticky-heading-container .content-sticky-heading-row, +main.long-content .content-sticky-heading-container .content-sticky-subheading-row { + padding-left: calc(0.12 * (100% - 2 * var(--content-padding)) + var(--content-padding)); + padding-right: calc(0.12 * (100% - 2 * var(--content-padding)) + var(--content-padding)); +} + +.content-sticky-heading-row { + box-sizing: border-box; + padding: + calc(1.25 * var(--content-padding) + 5px) + 20px + calc(0.75 * var(--content-padding)) + 20px; + + width: 100%; + margin: 0; + + background: var(--bg-black-color); + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); +} + +.content-sticky-heading-container.has-cover .content-sticky-heading-row, +.content-sticky-heading-container.has-cover .content-sticky-subheading-row { + display: grid; + grid-template-areas: + "title cover"; + grid-template-columns: 1fr min(40%, 400px); +} + +.content-sticky-heading-row h1 { + margin: 0; + padding-right: 10px; +} + +.content-sticky-heading-cover-container { + position: relative; + height: 0; + margin: -15px 0px -5px -5px; +} + +.content-sticky-heading-cover-needs-reveal { + display: none; +} + +.content-sticky-heading-cover { + position: absolute; + top: 0; + width: 80px; + right: 10px; + box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.25); + transition: transform 0.35s, opacity 0.25s; +} + +.content-sticky-heading-cover-container:not(.visible) .content-sticky-heading-cover { + opacity: 0; + transform: translateY(15px); +} + +.content-sticky-heading-cover .image-container { + border-width: 1px; + padding: 2px; +} + +.content-sticky-heading-cover img { + display: block; + width: 100%; + height: 100%; +} + +.content-sticky-subheading-row { + position: absolute; + width: 100%; + box-sizing: border-box; + padding: 10px 40px 5px 20px; + margin-top: 0; + z-index: -1; + + background: var(--bg-black-color); + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); + + transition: margin-top 0.35s, opacity 0.25s; +} + +.content-sticky-subheading-row h2 { + margin: 0; + + font-size: 0.9em !important; + font-weight: normal; + font-style: oblique; + color: #eee; +} + +.content-sticky-subheading-row:not(.visible) { + margin-top: -20px; + opacity: 0; +} + +.content-sticky-heading-container h2.visible { + margin-top: 0; + opacity: 1; +} + +.content-sticky-heading-row { + box-shadow: + inset 0 10px 10px -5px var(--shadow-color), + 0 4px 4px rgba(0, 0, 0, 0.8); +} + +.content-sticky-heading-container h2.visible { + box-shadow: + inset 0 10px 10px -5px var(--shadow-color), + 0 4px 4px rgba(0, 0, 0, 0.8); +} + +#content, .sidebar { + contain: paint; +} + +/* Sticky sidebar */ + +.sidebar-column.sidebar.sticky-column, +.sidebar-column.sidebar.sticky-last, +.sidebar-multiple.sticky-last > .sidebar:last-child, +.sidebar-multiple.sticky-column { + position: sticky; + top: 10px; +} + +.sidebar-multiple.sticky-last { + align-self: stretch; +} + +.sidebar-multiple.sticky-column { + align-self: flex-start; +} + +.sidebar-column.sidebar.sticky-column { + max-height: calc(100vh - 20px); + align-self: start; + padding-bottom: 0; + box-sizing: border-box; + flex-basis: 275px; + padding-top: 0; + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: var(--dark-color); +} + +.sidebar-column.sidebar.sticky-column::-webkit-scrollbar { + background: var(--dark-color); + width: 12px; +} + +.sidebar-column.sidebar.sticky-column::-webkit-scrollbar-thumb { + transition: background 0.2s; + background: rgba(255, 255, 255, 0.2); + border: 3px solid transparent; + border-radius: 10px; + background-clip: content-box; +} + +.sidebar-column.sidebar.sticky-column > h1 { + position: sticky; + top: 0; + margin: 0 calc(-1 * var(--content-padding)); + margin-bottom: 10px; + + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + padding: 10px 5px; + + background: var(--bg-black-color); + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); +} + +/* Image overlay */ + +#image-overlay-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 20px 40px; + box-sizing: border-box; + + opacity: 0; + pointer-events: none; + transition: opacity 0.4s; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#image-overlay-container.visible { + opacity: 1; + pointer-events: auto; +} + +#image-overlay-content-container { + border-radius: 0 0 8px 8px; + border: 2px solid var(--primary-color); + background: var(--dim-ghost-color); + padding: 3px; + overflow: hidden; + + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); +} + +#image-overlay-image-container { + display: block; + position: relative; + overflow: hidden; + width: 80vmin; + height: 80vmin; +} + +#image-overlay-image, +#image-overlay-image-thumb { + display: inline-block; + object-fit: contain; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.65); +} + +#image-overlay-image { + position: absolute; + top: 0; + left: 0; +} + +#image-overlay-image-thumb { + filter: blur(16px); + transform: scale(1.5); +} + +#image-overlay-container.loaded #image-overlay-image-thumb { + opacity: 0; + pointer-events: none; + transition: opacity 0.25s; +} + +#image-overlay-image-container::after { + content: ""; + display: block; + position: absolute; + bottom: 0; + left: 0; + height: 4px; + width: var(--download-progress); + background: var(--primary-color); + box-shadow: 0 -3px 12px 4px var(--primary-color); + transition: 0.25s; +} + +#image-overlay-container.loaded #image-overlay-image-container::after { + width: 100%; + background: white; + opacity: 0; +} + +#image-overlay-container.errored #image-overlay-image-container::after { + width: 100%; + background: red; +} + +#image-overlay-container:not(.visible) #image-overlay-image-container::after { + width: 0 !important; +} + +#image-overlay-action-container { + padding: 4px 4px 6px 4px; + border-radius: 0 0 5px 5px; + background: var(--bg-black-color); + color: white; + font-style: oblique; + text-align: center; +} + +#image-overlay-container #image-overlay-action-content-without-size:not(.visible), +#image-overlay-container #image-overlay-action-content-with-size:not(.visible), +#image-overlay-container #image-overlay-file-size-warning:not(.visible), +#image-overlay-container #image-overlay-file-size-kilobytes:not(.visible), +#image-overlay-container #image-overlay-file-size-megabytes:not(.visible) { + display: none; +} + +#image-overlay-file-size-warning { + opacity: 0.8; + font-size: 0.9em; +} + +/* important easter egg mode */ + +html[data-language-code="preview-en"][data-url-key="localized.home"] #content + h1::after { + font-family: cursive; + display: block; + content: "(Preview Build)"; + font-size: 0.8em; +} + +/* Layout - Wide (most computers) */ + +@media (min-width: 900px) { + #page-container:not(.has-zero-sidebars) #secondary-nav { + display: none; + } +} + +/* Layout - Medium (tablets, some landscape mobiles) + * + * Note: Rules defined here are exclusive to "medium" width, i.e. they don't + * additionally apply to "thin". Use the later section which applies to both + * if so desired. + */ + +@media (min-width: 600px) and (max-width: 899.98px) { +} + +/* Layout - Wide or Medium */ + +@media (min-width: 600px) { + .content-sticky-heading-container { + /* Safari doesn't always play nicely with position: sticky, + * this seems to fix images sometimes displaying above the + * position: absolute subheading (h2) child + * + * See also: https://stackoverflow.com/questions/50224855/ + */ + transform: translate3d(0, 0, 0); + z-index: 1; + } + + /* 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 { + float: right; + width: 40%; + max-width: 400px; + margin: -60px 0 10px 10px; + + position: relative; + z-index: 2; + } + + html[data-url-key="localized.home"] #page-container.has-one-sidebar .grid-listing > .grid-item:not(:nth-child(n+10)) { + flex-basis: 23%; + margin: 15px; + } + + html[data-url-key="localized.home"] #page-container.has-one-sidebar .grid-listing > .grid-item:nth-child(n+10) { + flex-basis: 18%; + margin: 10px; + } +} + +/* Layout - Medium or Thin */ + +@media (max-width: 899.98px) { + .sidebar-column:not(.no-hide) { + display: none; + } + + #secondary-nav { + display: block; + } + + .layout-columns.vertical-when-thin { + flex-direction: column; + } + + .layout-columns.vertical-when-thin > *:not(:last-child) { + margin-bottom: 10px; + } + + .sidebar-column.no-hide { + max-width: unset !important; + flex-basis: unset !important; + margin-right: 0 !important; + margin-left: 0 !important; + width: 100%; + } + + .sidebar .news-entry:not(.first-news-entry) { + display: none; + } + + .grid-listing > .grid-item { + flex-basis: 40%; + } +} + +/* Layout - Thin (phones) */ + +@media (max-width: 600px) { + .content-columns { + columns: 1; + } + + #cover-art-container { + margin: 25px 0 5px 0; + width: 100%; + max-width: unset; + } + + /* Show sticky heading above cover art */ + + .content-sticky-heading-container { + z-index: 2; + } + + /* Disable grid features, just line header children up vertically */ + + #header { + display: block; + } + + #header > div:not(:first-child) { + margin-top: 0.5em; + } +} diff --git a/src/upd8.js b/src/upd8.js index 2cc8f554..9cd2c509 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -81,7 +81,7 @@ import * as buildModes from './write/build-modes/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const CACHEBUST = 21; +const CACHEBUST = 22; let COMMIT; try { -- cgit 1.3.0-6-gf8a5 From 7cca97b865b7d9e299a7e1e6be9947a0fc0fb9c4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 6 Oct 2023 18:38:31 -0300 Subject: content: generateAlbumGalleryPage: secondary nav on gallery page --- src/content/dependencies/generateAlbumGalleryPage.js | 6 ++++++ src/content/dependencies/generateAlbumSecondaryNav.js | 6 +++--- src/content/dependencies/linkAlbumDynamically.js | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 src/content/dependencies/linkAlbumDynamically.js (limited to 'src') diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index 9551eb98..34581a16 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -6,6 +6,7 @@ export default { 'generateAlbumGalleryNoTrackArtworksLine', 'generateAlbumGalleryStatsLine', 'generateAlbumNavAccent', + 'generateAlbumSecondaryNav', 'generateAlbumStyleRules', 'generateCoverGrid', 'generatePageLayout', @@ -60,6 +61,9 @@ export default { relations.albumNavAccent = relation('generateAlbumNavAccent', album, null); + relations.secondaryNav = + relation('generateAlbumSecondaryNav', album); + relations.statsLine = relation('generateAlbumGalleryStatsLine', album); @@ -179,6 +183,8 @@ export default { }), }, ], + + secondaryNav: relations.secondaryNav, }); }, }; diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js index 705dec51..8cf36fa4 100644 --- a/src/content/dependencies/generateAlbumSecondaryNav.js +++ b/src/content/dependencies/generateAlbumSecondaryNav.js @@ -5,7 +5,7 @@ export default { 'generateColorStyleVariables', 'generatePreviousNextLinks', 'generateSecondaryNav', - 'linkAlbum', + 'linkAlbumDynamically', 'linkGroup', 'linkTrack', ], @@ -64,14 +64,14 @@ export default { query.adjacentGroupInfo .map(({previousAlbum}) => (previousAlbum - ? relation('linkAlbum', previousAlbum) + ? relation('linkAlbumDynamically', previousAlbum) : null)); relations.nextAlbumLinks = query.adjacentGroupInfo .map(({nextAlbum}) => (nextAlbum - ? relation('linkAlbum', nextAlbum) + ? relation('linkAlbumDynamically', nextAlbum) : null)); } diff --git a/src/content/dependencies/linkAlbumDynamically.js b/src/content/dependencies/linkAlbumDynamically.js new file mode 100644 index 00000000..3adc64df --- /dev/null +++ b/src/content/dependencies/linkAlbumDynamically.js @@ -0,0 +1,14 @@ +export default { + contentDependencies: ['linkAlbumGallery', 'linkAlbum'], + extraDependencies: ['pagePath'], + + relations: (relation, album) => ({ + galleryLink: relation('linkAlbumGallery', album), + infoLink: relation('linkAlbum', album), + }), + + generate: (relations, {pagePath}) => + (pagePath[0] === 'albumGallery' + ? relations.galleryLink + : relations.infoLink), +}; -- cgit 1.3.0-6-gf8a5 From 388b3d381833edc40bbcabee0611b96a3e8f2879 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 7 Oct 2023 09:17:30 -0300 Subject: content: generateGroupSecondaryNav --- .../dependencies/generateGroupGalleryPage.js | 7 ++ src/content/dependencies/generateGroupInfoPage.js | 6 ++ .../dependencies/generateGroupSecondaryNav.js | 99 ++++++++++++++++++++++ src/content/dependencies/linkGroupDynamically.js | 14 +++ 4 files changed, 126 insertions(+) create mode 100644 src/content/dependencies/generateGroupSecondaryNav.js create mode 100644 src/content/dependencies/linkGroupDynamically.js (limited to 'src') diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index 47239f55..71b9f508 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -11,6 +11,7 @@ export default { 'generateCoverCarousel', 'generateCoverGrid', 'generateGroupNavLinks', + 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', 'image', @@ -46,6 +47,9 @@ export default { relation('generateGroupNavLinks', group); if (sprawl.enableGroupUI) { + relations.secondaryNav = + relation('generateGroupSecondaryNav', group); + relations.sidebar = relation('generateGroupSidebar', group); } @@ -208,6 +212,9 @@ export default { relations.navLinks .slot('currentExtra', 'gallery') .content, + + secondaryNav: + relations.secondaryNav ?? null, }); }, }; diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js index e162a26a..0583755e 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -4,6 +4,7 @@ export default { contentDependencies: [ 'generateContentHeading', 'generateGroupNavLinks', + 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', 'linkAlbum', @@ -32,6 +33,9 @@ export default { relation('generateGroupNavLinks', group); if (sprawl.enableGroupUI) { + relations.secondaryNav = + relation('generateGroupSecondaryNav', group); + relations.sidebar = relation('generateGroupSidebar', group); } @@ -161,6 +165,8 @@ export default { navLinkStyle: 'hierarchical', navLinks: relations.navLinks.content, + + secondaryNav: relations.secondaryNav ?? null, }); }, }; diff --git a/src/content/dependencies/generateGroupSecondaryNav.js b/src/content/dependencies/generateGroupSecondaryNav.js new file mode 100644 index 00000000..e3b28099 --- /dev/null +++ b/src/content/dependencies/generateGroupSecondaryNav.js @@ -0,0 +1,99 @@ +export default { + contentDependencies: [ + 'generateColorStyleVariables', + 'generatePreviousNextLinks', + 'generateSecondaryNav', + 'linkGroupDynamically', + 'linkListing', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({listingSpec, wikiInfo}) => ({ + groupsByCategoryListing: + (wikiInfo.enableListings + ? listingSpec + .find(l => l.directory === 'groups/by-category') + : null), + }), + + query(sprawl, group) { + const groups = group.category.groups; + const index = groups.indexOf(group); + + return { + previousGroup: + (index > 0 + ? groups[index - 1] + : null), + + nextGroup: + (index < groups.length - 1 + ? groups[index + 1] + : null), + }; + }, + + relations(relation, query, sprawl, _group) { + const relations = {}; + + relations.secondaryNav = + relation('generateSecondaryNav'); + + if (sprawl.groupsByCategoryListing) { + relations.categoryLink = + relation('linkListing', sprawl.groupsByCategoryListing); + } + + relations.colorVariables = + relation('generateColorStyleVariables'); + + if (query.previousGroup || query.nextGroup) { + relations.previousNextLinks = + relation('generatePreviousNextLinks'); + } + + relations.previousGroupLink = + (query.previousGroup + ? relation('linkGroupDynamically', query.previousGroup) + : null); + + relations.nextGroupLink = + (query.nextGroup + ? relation('linkGroupDynamically', query.nextGroup) + : null); + + return relations; + }, + + data: (query, sprawl, group) => ({ + categoryName: group.category.name, + categoryColor: group.category.color, + }), + + generate(data, relations, {html, language}) { + const {content: previousNextPart} = + relations.previousNextLinks.slots({ + previousLink: relations.previousGroupLink, + nextLink: relations.nextGroupLink, + id: true, + }); + + const {categoryLink} = relations; + + categoryLink?.setSlot('content', data.categoryName); + + return relations.secondaryNav.slots({ + class: 'nav-links-groups', + content: + (!relations.previousGroupLink && !relations.nextGroupLink + ? categoryLink + : html.tag('span', + {style: relations.colorVariables.slot('color', data.categoryColor).content}, + [ + categoryLink.slot('color', false), + `(${language.formatUnitList(previousNextPart)})`, + ])), + }); + }, +}; diff --git a/src/content/dependencies/linkGroupDynamically.js b/src/content/dependencies/linkGroupDynamically.js new file mode 100644 index 00000000..90303ed1 --- /dev/null +++ b/src/content/dependencies/linkGroupDynamically.js @@ -0,0 +1,14 @@ +export default { + contentDependencies: ['linkGroupGallery', 'linkGroup'], + extraDependencies: ['pagePath'], + + relations: (relation, group) => ({ + galleryLink: relation('linkGroupGallery', group), + infoLink: relation('linkGroup', group), + }), + + generate: (relations, {pagePath}) => + (pagePath[0] === 'groupGallery' + ? relations.galleryLink + : relations.infoLink), +}; -- cgit 1.3.0-6-gf8a5 From 849bfa93f880a8611597e87f13eeb168795c6452 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sat, 7 Oct 2023 09:20:46 -0300 Subject: content: generateGroupGalleryPage: remove "another group" content link --- .../dependencies/generateGroupGalleryPage.js | 28 ++-------------------- src/strings-default.json | 2 -- 2 files changed, 2 insertions(+), 28 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index 71b9f508..259f5dce 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -21,18 +21,8 @@ export default { extraDependencies: ['html', 'language', 'wikiData'], - sprawl({listingSpec, wikiInfo}) { - const sprawl = {}; - sprawl.enableGroupUI = wikiInfo.enableGroupUI; - - if (wikiInfo.enableListings && wikiInfo.enableGroupUI) { - sprawl.groupsByCategoryListing = - listingSpec - .find(l => l.directory === 'groups/by-category'); - } - - return sprawl; - }, + sprawl: ({wikiInfo}) => + ({enableGroupUI: wikiInfo.enableGroupUI}), relations(relation, sprawl, group) { const relations = {}; @@ -54,11 +44,6 @@ export default { relation('generateGroupSidebar', group); } - if (sprawl.groupsByCategoryListing) { - relations.groupListingLink = - relation('linkListing', sprawl.groupsByCategoryListing); - } - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); if (!empty(carouselAlbums)) { @@ -164,15 +149,6 @@ export default { })), })), - relations.groupListingLink && - html.tag('p', - {class: 'quick-info'}, - language.$('groupGalleryPage.anotherGroupLine', { - link: - relations.groupListingLink - .slot('content', language.$('groupGalleryPage.anotherGroupLine.link')), - })), - relations.coverGrid .slots({ links: relations.gridLinks, diff --git a/src/strings-default.json b/src/strings-default.json index af44fc7e..904d25d4 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -337,8 +337,6 @@ "groupInfoPage.albumList.item.otherGroupAccent": "(from {GROUP})", "groupGalleryPage.title": "{GROUP} - Gallery", "groupGalleryPage.infoLine": "{TRACKS} across {ALBUMS}, totaling {TIME}.", - "groupGalleryPage.anotherGroupLine": "({LINK})", - "groupGalleryPage.anotherGroupLine.link": "Choose another group to filter by!", "listingIndex.title": "Listings", "listingIndex.infoLine": "{WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}.", "listingIndex.exploreList": "Feel free to explore any of the listings linked below and in the sidebar!", -- cgit 1.3.0-6-gf8a5 From 680cc3acdcabfe8d765275a7f78c4b254670fe2e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 6 Oct 2023 09:13:35 -0300 Subject: content: generateAlbumCommentaryPage: don't break for coverless album --- src/content/dependencies/generateAlbumCommentaryPage.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 5c057b80..2982b037 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -33,8 +33,10 @@ export default { relation('generateAlbumNavAccent', album, null); if (album.commentary) { - relations.albumCommentaryCover = - relation('generateAlbumCoverArtwork', album); + if (album.hasCoverArt) { + relations.albumCommentaryCover = + relation('generateAlbumCoverArtwork', album); + } relations.albumCommentaryContent = relation('transformContent', album.commentary); -- cgit 1.3.0-6-gf8a5 From 822d0ad890cbdb2a780a2fdf7d1c1aa053fc1d77 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 8 Oct 2023 12:43:00 -0300 Subject: infra: tweak how empty content function modules are handled --- src/content/dependencies/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index 71802050..58bac0d2 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -177,7 +177,14 @@ export function watchContentDependencies({ // Just skip newly created files. They'll be processed again when // written. if (spec === undefined) { - contentDependencies[functionName] = null; + // For practical purposes the file is treated as though it doesn't + // even exist (undefined), rather than not being ready yet (null). + // Apart from if existing contents of the file were erased (but not + // the file itself), this value might already be set (to null!) by + // the readdir performed at the beginning to evaluate which files + // should be read and processed at least once before reporting all + // dependencies as ready. + delete contentDependencies[functionName]; return; } -- cgit 1.3.0-6-gf8a5 From cc4c12ad31be6b6d8432f257e112195179f7eafa Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 10 Oct 2023 07:59:49 -0300 Subject: find: fix error reporting for multiple name matches --- src/find.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/find.js b/src/find.js index c8edce98..5a249c28 100644 --- a/src/find.js +++ b/src/find.js @@ -34,11 +34,12 @@ export function processAllAvailableMatches(data, { const normalizedName = name.toLowerCase(); if (normalizedName in byName) { + const alreadyMatchesByName = byName[normalizedName]; byName[normalizedName] = null; if (normalizedName in multipleNameMatches) { multipleNameMatches[normalizedName].push(thing); } else { - multipleNameMatches[normalizedName] = [thing]; + multipleNameMatches[normalizedName] = [alreadyMatchesByName, thing]; } } else { byName[normalizedName] = thing; @@ -97,16 +98,21 @@ function findHelper({ const typePart = regexMatch[1]; const refPart = regexMatch[2]; + const normalizedName = + (typePart + ? null + : refPart.toLowerCase()); + const match = (typePart ? subcache.byDirectory[refPart] - : subcache.byName[refPart.toLowerCase()]); + : subcache.byName[normalizedName]); if (!match && !typePart) { - if (subcache.multipleNameMatches[refPart]) { + if (subcache.multipleNameMatches[normalizedName]) { return warnOrThrow(mode, `Multiple matches for reference "${fullRef}". Please resolve:\n` + - subcache.multipleNameMatches[refPart] + subcache.multipleNameMatches[normalizedName] .map(match => `- ${inspect(match)}\n`) .join('') + `Returning null for this reference.`); -- cgit 1.3.0-6-gf8a5 From eb7b9f1a7af122e8dc9282f14cc1e211ef068417 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 10 Oct 2023 09:19:11 -0300 Subject: content: generateAlbumGalleryPage: show album cover at start --- .../dependencies/generateAlbumGalleryPage.js | 76 ++++++++++++++-------- 1 file changed, 50 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index 34581a16..f61b1983 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -80,15 +80,25 @@ export default { relations.coverGrid = relation('generateCoverGrid'); - relations.links = - album.tracks.map(track => - relation('linkTrack', track)); - - relations.images = - album.tracks.map(track => - (track.hasUniqueCoverArt - ? relation('image', track.artTags) - : relation('image'))); + relations.links = [ + 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'))), + ]; return relations; }, @@ -99,27 +109,41 @@ export default { data.name = album.name; data.color = album.color; - data.names = - album.tracks.map(track => track.name); + data.names = [ + album.name, + ...album.tracks.map(track => track.name), + ]; - data.coverArtists = - album.tracks.map(track => { - if (query.coverArtistsForAllTracks) { - return null; - } + data.coverArtists = [ + (album.hasCoverArt + ? album.coverArtistContribs.map(({who: artist}) => artist.name) + : null), - if (track.hasUniqueCoverArt) { - return track.coverArtistContribs.map(({who: artist}) => artist.name); - } + ... + album.tracks.map(track => { + if (query.coverArtistsForAllTracks) { + return null; + } - return null; - }); + if (track.hasUniqueCoverArt) { + return track.coverArtistContribs.map(({who: artist}) => artist.name); + } - data.paths = - album.tracks.map(track => - (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : null)); + return null; + }), + ]; + + data.paths = [ + (album.hasCoverArt + ? ['media.albumCover', album.directory, album.coverArtFileExtension] + : null), + + ... + album.tracks.map(track => + (track.hasUniqueCoverArt + ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] + : null)), + ]; return data; }, -- cgit 1.3.0-6-gf8a5 From 962e6d1777feb69711e33b16d11d322edd1744cd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 10 Oct 2023 09:37:14 -0300 Subject: content: generateGroupNavLinks: remove previous/next --- src/content/dependencies/generateGroupNavLinks.js | 44 ++--------------------- 1 file changed, 3 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateGroupNavLinks.js b/src/content/dependencies/generateGroupNavLinks.js index 68341e0a..5cde2ab4 100644 --- a/src/content/dependencies/generateGroupNavLinks.js +++ b/src/content/dependencies/generateGroupNavLinks.js @@ -2,10 +2,8 @@ import {empty} from '#sugar'; export default { contentDependencies: [ - 'generatePreviousNextLinks', 'linkGroup', 'linkGroupGallery', - 'linkGroupExtra', ], extraDependencies: ['html', 'language', 'wikiData'], @@ -28,24 +26,6 @@ export default { relations.mainLink = relation('linkGroup', group); - relations.previousNextLinks = - relation('generatePreviousNextLinks'); - - const groups = sprawl.groupCategoryData - .flatMap(category => category.groups); - - const index = groups.indexOf(group); - - if (index > 0) { - relations.previousLink = - relation('linkGroupExtra', groups[index - 1]); - } - - if (index < groups.length - 1) { - relations.nextLink = - relation('linkGroupExtra', groups[index + 1]); - } - relations.infoLink = relation('linkGroup', group); @@ -80,26 +60,6 @@ export default { ]; } - const previousNextLinks = - (relations.previousLink || relations.nextLink) && - relations.previousNextLinks.slots({ - previousLink: - relations.previousLink - ?.slot('extra', slots.currentExtra) - ?.content - ?? null, - nextLink: - relations.nextLink - ?.slot('extra', slots.currentExtra) - ?.content - ?? null, - }); - - const previousNextPart = - previousNextLinks && - language.formatUnitList( - previousNextLinks.content.filter(Boolean)); - const infoLink = relations.infoLink.slots({ attributes: {class: slots.currentExtra === null && 'current'}, @@ -119,7 +79,9 @@ export default { : language.formatUnitList([infoLink, ...extraLinks])); const accent = - `(${[extrasPart, previousNextPart].filter(Boolean).join('; ')})`; + (extrasPart + ? `(${extrasPart})` + : null); return [ {auto: 'home'}, -- cgit 1.3.0-6-gf8a5 From 86068ab18e4a82754dbd055ebf21e3a1ec5ead07 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 11 Oct 2023 09:47:00 -0300 Subject: write: live-dev-server: end file responses less enthusiastically This seems to be a bit more reliable, although it'll probably take a little more memory while serving larger files. --- src/write/build-modes/live-dev-server.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index d4efd177..1339c322 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -1,8 +1,6 @@ import * as http from 'node:http'; -import {createReadStream} from 'node:fs'; -import {stat} from 'node:fs/promises'; +import {readFile, stat} from 'node:fs/promises'; import * as path from 'node:path'; -import {pipeline} from 'node:stream/promises' import {logInfo, logWarn, progressCallAll} from '#cli'; import {watchContentDependencies} from '#content-dependencies'; @@ -224,7 +222,7 @@ export async function go({ 'gif': 'image/gif', 'ico': 'image/vnd.microsoft.icon', 'jpg': 'image/jpeg', - 'jpeg:': 'image/jpeg', + 'jpeg': 'image/jpeg', 'js': 'text/javascript', 'mjs': 'text/javascript', 'mp3': 'audio/mpeg', @@ -249,13 +247,12 @@ export async function go({ try { const {size} = await stat(filePath); + const buffer = await readFile(filePath) response.writeHead(200, contentType ? { 'Content-Type': contentType, 'Content-Length': size, } : {}); - await pipeline( - createReadStream(filePath), - response); + response.end(buffer); if (loudResponses) console.log(`${requestHead} [200] ${pathname}`); } catch (error) { response.writeHead(500, contentTypePlain); -- cgit 1.3.0-6-gf8a5 From 3cd6f9edc58171e33ed6af565db84113e2488f25 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 9 Oct 2023 14:58:14 -0300 Subject: data: language: allow passing multiple key parts directly --- src/data/things/language.js | 66 ++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/data/things/language.js b/src/data/things/language.js index fe74f7bf..646eb6d1 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -101,6 +101,7 @@ export class Language extends Thing { }, }, + // TODO: This currently isn't used. Is it still needed? strings_htmlEscaped: { flags: {expose: true}, expose: { @@ -130,8 +131,8 @@ export class Language extends Thing { }; } - $(key, args = {}) { - return this.formatString(key, args); + $(...args) { + return this.formatString(...args); } assertIntlAvailable(property) { @@ -145,8 +146,20 @@ export class Language extends Thing { return this.intl_pluralCardinal.select(value); } - formatString(key, args = {}) { - const strings = this.strings_htmlEscaped; + formatString(...args) { + const hasOptions = + typeof args.at(-1) === 'object' && + args.at(-1) !== null; + + const key = + (hasOptions ? args.slice(0, -1) : args) + .filter(Boolean) + .join('.'); + + const options = + (hasOptions + ? args.at(-1) + : null); if (!this.strings) { throw new Error(`Strings unavailable`); @@ -158,27 +171,36 @@ export class Language extends Thing { const template = this.strings[key]; - // Convert the keys on the args dict from camelCase to CONSTANT_CASE. - // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut - // like, who cares, dude?) Also, this is an array, 8ecause it's handy - // for the iterating we're a8out to do. Also strip HTML from arguments - // that are literal strings - real HTML content should always be proper - // HTML objects (see html.js). - const processedArgs = - Object.entries(args).map(([k, v]) => [ - k.replace(/[A-Z]/g, '_$&').toUpperCase(), - this.#sanitizeStringArg(v), - ]); - - // Replacement time! Woot. Reduce comes in handy here! - const output = - processedArgs.reduce( - (x, [k, v]) => x.replaceAll(`{${k}}`, v), - template); + let output; + + if (hasOptions) { + // Convert the keys on the options dict from camelCase to CONSTANT_CASE. + // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut + // like, who cares, dude?) Also, this is an array, 8ecause it's handy + // for the iterating we're a8out to do. Also strip HTML from arguments + // that are literal strings - real HTML content should always be proper + // HTML objects (see html.js). + const processedOptions = + Object.entries(options).map(([k, v]) => [ + k.replace(/[A-Z]/g, '_$&').toUpperCase(), + this.#sanitizeStringArg(v), + ]); + + // Replacement time! Woot. Reduce comes in handy here! + output = + processedOptions.reduce( + (x, [k, v]) => x.replaceAll(`{${k}}`, v), + template); + } else { + // Without any options provided, just use the template as-is. This will + // still error if the template expected arguments, and otherwise will be + // the right value. + output = template; + } // Post-processing: if any expected arguments *weren't* replaced, that // is almost definitely an error. - if (output.match(/\{[A-Z_]+\}/)) { + if (output.match(/\{[A-Z][A-Z0-9_]*\}/)) { throw new Error(`Args in ${key} were missing - output: ${output}`); } -- cgit 1.3.0-6-gf8a5 From 3a871cf43a11b87392d26320c736b516925da684 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 11 Oct 2023 14:32:28 -0300 Subject: implement flash act pages, rework flash sidebar layout --- .../dependencies/generateFlashActGalleryPage.js | 91 ++++++++ .../dependencies/generateFlashActNavAccent.js | 74 +++++++ .../dependencies/generateFlashActSidebar.js | 209 ++++++++++++++++++ src/content/dependencies/generateFlashIndexPage.js | 21 +- src/content/dependencies/generateFlashInfoPage.js | 17 +- src/content/dependencies/generateFlashNavAccent.js | 7 +- src/content/dependencies/generateFlashSidebar.js | 236 --------------------- src/content/dependencies/linkFlashAct.js | 14 ++ src/content/dependencies/listArtTagNetwork.js | 1 + src/data/things/flash.js | 6 +- src/data/yaml.js | 4 +- src/find.js | 5 + src/page/flash-act.js | 23 ++ src/page/index.js | 1 + src/static/site5.css | 2 + src/strings-default.json | 6 + src/url-spec.js | 2 + 17 files changed, 452 insertions(+), 267 deletions(-) create mode 100644 src/content/dependencies/generateFlashActGalleryPage.js create mode 100644 src/content/dependencies/generateFlashActNavAccent.js create mode 100644 src/content/dependencies/generateFlashActSidebar.js delete mode 100644 src/content/dependencies/generateFlashSidebar.js create mode 100644 src/content/dependencies/linkFlashAct.js create mode 100644 src/content/dependencies/listArtTagNetwork.js create mode 100644 src/page/flash-act.js (limited to 'src') diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js new file mode 100644 index 00000000..8eea58bb --- /dev/null +++ b/src/content/dependencies/generateFlashActGalleryPage.js @@ -0,0 +1,91 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateCoverGrid', + 'generateFlashActNavAccent', + 'generateFlashActSidebar', + 'generatePageLayout', + 'image', + 'linkFlash', + 'linkFlashIndex', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, act) => ({ + layout: + relation('generatePageLayout'), + + flashIndexLink: + relation('linkFlashIndex'), + + flashActNavAccent: + relation('generateFlashActNavAccent', act), + + sidebar: + relation('generateFlashActSidebar', act, null), + + coverGrid: + relation('generateCoverGrid'), + + coverGridImages: + act.flashes + .map(_flash => relation('image')), + + flashLinks: + act.flashes + .map(flash => relation('linkFlash', flash)), + }), + + data: (act) => ({ + name: act.name, + color: act.color, + + flashNames: + act.flashes.map(flash => flash.name), + + flashCoverPaths: + act.flashes.map(flash => + ['media.flashArt', flash.directory, flash.coverArtFileExtension]) + }), + + generate(data, relations, {html, language}) { + return relations.layout.slots({ + title: + language.$('flashPage.title', { + flash: new html.Tag(null, null, data.name), + }), + + color: data.color, + headingMode: 'static', + + mainClasses: ['flash-index'], + mainContent: [ + relations.coverGrid.slots({ + links: relations.flashLinks, + names: data.flashNames, + lazy: 6, + + images: + stitchArrays({ + image: relations.coverGridImages, + path: data.flashCoverPaths, + }).map(({image, path}) => + image.slot('path', path)), + }), + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {html: relations.flashIndexLink}, + {auto: 'current'}, + ], + + navBottomRowContent: relations.flashActNavAccent, + + ...relations.sidebar, + }); + }, +}; diff --git a/src/content/dependencies/generateFlashActNavAccent.js b/src/content/dependencies/generateFlashActNavAccent.js new file mode 100644 index 00000000..98504385 --- /dev/null +++ b/src/content/dependencies/generateFlashActNavAccent.js @@ -0,0 +1,74 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generatePreviousNextLinks', + 'linkFlashAct', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl({flashActData}) { + return {flashActData}; + }, + + query(sprawl, flashAct) { + // Like with generateFlashNavAccent, don't sort chronologically here. + const flashActs = + sprawl.flashActData; + + const index = flashActs.indexOf(flashAct); + + const previousFlashAct = + (index > 0 + ? flashActs[index - 1] + : null); + + const nextFlashAct = + (index < flashActs.length - 1 + ? flashActs[index + 1] + : null); + + return {previousFlashAct, nextFlashAct}; + }, + + relations(relation, query) { + const relations = {}; + + if (query.previousFlashAct || query.nextFlashAct) { + relations.previousNextLinks = + relation('generatePreviousNextLinks'); + + relations.previousFlashActLink = + (query.previousFlashAct + ? relation('linkFlashAct', query.previousFlashAct) + : null); + + relations.nextFlashActLink = + (query.nextFlashAct + ? relation('linkFlashAct', query.nextFlashAct) + : null); + } + + return relations; + }, + + generate(relations, {html, language}) { + const {content: previousNextLinks = []} = + relations.previousNextLinks && + relations.previousNextLinks.slots({ + previousLink: relations.previousFlashActLink, + nextLink: relations.nextFlashActLink, + }); + + const allLinks = [ + ...previousNextLinks, + ].filter(Boolean); + + if (empty(allLinks)) { + return html.blank(); + } + + return `(${language.formatUnitList(allLinks)})`; + }, +}; diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js new file mode 100644 index 00000000..ff5dc049 --- /dev/null +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -0,0 +1,209 @@ +import find from '#find'; +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['linkFlash', 'linkFlashAct', 'linkFlashIndex'], + extraDependencies: ['getColors', 'html', 'language', 'wikiData'], + + // So help me Gog, the flash sidebar is heavily hard-coded. + + sprawl: ({flashActData}) => ({flashActData}), + + query(sprawl, act, flash) { + const findFlashAct = directory => + find.flashAct(directory, sprawl.flashActData, {mode: 'error'}); + + const sideFirstActs = [ + findFlashAct('flash-act:a1'), + findFlashAct('flash-act:a6a1'), + findFlashAct('flash-act:hiveswap'), + findFlashAct('flash-act:cool-and-new-web-comic'), + findFlashAct('flash-act:psycholonials'), + ]; + + const sideNames = [ + `Side 1 (Acts 1-5)`, + `Side 2 (Acts 6-7)`, + `Additional Canon`, + `Fan Adventures`, + `More Flashes & Games`, + ]; + + const sideColors = [ + '#4ac925', + '#3796c6', + '#f2a400', + '#c466ff', + '#32c7fe', + ]; + + const sideFirstActIndexes = + sideFirstActs + .map(act => sprawl.flashActData.indexOf(act)); + + const actSideIndexes = + sprawl.flashActData + .map((act, actIndex) => actIndex) + .map(actIndex => + sideFirstActIndexes + .findIndex((firstActIndex, i) => + i === sideFirstActs.length - 1 || + firstActIndex <= actIndex && + sideFirstActIndexes[i + 1] > actIndex)); + + const sideActs = + sideNames + .map((name, sideIndex) => + stitchArrays({ + act: sprawl.flashActData, + actSideIndex: actSideIndexes, + }).filter(({actSideIndex}) => actSideIndex === sideIndex) + .map(({act}) => act)); + + const currentActFlashes = + act.flashes; + + const currentFlashIndex = + currentActFlashes.indexOf(flash); + + const currentSideIndex = + actSideIndexes[sprawl.flashActData.indexOf(act)]; + + const currentSideActs = + sideActs[currentSideIndex]; + + const currentActIndex = + currentSideActs.indexOf(act); + + const visualNovelActs = [ + findFlashAct('flash-act:friendsim'), + findFlashAct('flash-act:pesterquest'), + findFlashAct('flash-act:psycholonials'), + ]; + + const gameSeriesActs = [ + findFlashAct('flash-act:hiveswap'), + ]; + + const listTerminology = + (visualNovelActs.includes(act) + ? 'volumesInThisGame' + : gameSeriesActs.includes(act) + ? 'gamesInThisSeries' + : act === findFlashAct('flash-act:other-fan-adventures') + ? 'flashesInThisSection' + : currentSideIndex <= 1 + ? 'flashesInThisAct' + : currentSideIndex === 3 + ? 'flashesInThisStory' + : 'entriesInThisSection'); + + return { + sideNames, + sideColors, + sideActs, + + currentSideIndex, + currentSideActs, + currentActIndex, + currentActFlashes, + currentFlashIndex, + + listTerminology, + }; + }, + + relations: (relation, query, sprawl, act, _flash) => ({ + currentActLink: + relation('linkFlashAct', act), + + flashIndexLink: + relation('linkFlashIndex'), + + sideActLinks: + query.sideActs + .map(acts => acts + .map(act => relation('linkFlashAct', act))), + + currentActFlashLinks: + act.flashes + .map(flash => relation('linkFlash', flash)), + }), + + data: (query, sprawl, act, flash) => ({ + isFlashActPage: !flash, + + sideColors: query.sideColors, + sideNames: query.sideNames, + + currentSideIndex: query.currentSideIndex, + currentActIndex: query.currentActIndex, + currentFlashIndex: query.currentFlashIndex, + + listTerminology: query.listTerminology, + }), + + generate(data, relations, {getColors, html, language}) { + const currentActBox = html.tags([ + html.tag('h1', relations.currentActLink), + + html.tag('details', + (data.isFlashActPage + ? {} + : {class: 'current', open: true}), + [ + html.tag('summary', + html.tag('span', {class: 'group-name'}, + language.$('flashSidebar.flashList', data.listTerminology))), + + html.tag('ul', + relations.currentActFlashLinks + .map((flashLink, index) => + html.tag('li', + {class: index === data.currentFlashIndex && 'current'}, + flashLink))), + ]), + ]); + + const sideMapBox = html.tags([ + html.tag('h1', relations.flashIndexLink), + + stitchArrays({ + sideName: data.sideNames, + sideColor: data.sideColors, + actLinks: relations.sideActLinks, + }).map(({sideName, sideColor, actLinks}, sideIndex) => + html.tag('details', { + class: sideIndex === data.currentSideIndex && 'current', + open: data.isFlashActPage && sideIndex === data.currentSideIndex, + style: sideColor && `--primary-color: ${getColors(sideColor).primary}` + }, [ + html.tag('summary', + html.tag('span', {class: 'group-name'}, + sideName)), + + html.tag('ul', + actLinks.map((actLink, actIndex) => + html.tag('li', + {class: + sideIndex === data.currentSideIndex && + actIndex === data.currentActIndex && + 'current'}, + actLink))), + ])), + ]); + + return { + leftSidebarMultiple: + (data.isFlashActPage + ? [ + {content: sideMapBox}, + {content: currentActBox}, + ] + : [ + {content: currentActBox}, + {content: sideMapBox}, + ]), + }; + }, +}; diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js index 66588fdb..ad1dab94 100644 --- a/src/content/dependencies/generateFlashIndexPage.js +++ b/src/content/dependencies/generateFlashIndexPage.js @@ -7,6 +7,7 @@ export default { 'generatePageLayout', 'image', 'linkFlash', + 'linkFlashAct', ], extraDependencies: ['html', 'language', 'wikiData'], @@ -36,9 +37,9 @@ export default { query.flashActs .map(() => relation('generateColorStyleVariables')), - actFirstFlashLinks: + actLinks: query.flashActs - .map(act => relation('linkFlash', act.flashes[0])), + .map(act => relation('linkFlashAct', act)), actCoverGrids: query.flashActs @@ -58,7 +59,7 @@ export default { data: (query) => ({ jumpLinkAnchors: query.jumpActs - .map(act => act.anchor), + .map(act => act.directory), jumpLinkColors: query.jumpActs @@ -70,16 +71,12 @@ export default { actAnchors: query.flashActs - .map(act => act.anchor), + .map(act => act.directory), actColors: query.flashActs .map(act => act.color), - actNames: - query.flashActs - .map(act => act.name), - actCoverGridNames: query.flashActs .map(act => act.flashes @@ -118,10 +115,9 @@ export default { stitchArrays({ colorVariables: relations.actColorVariables, - firstFlashLink: relations.actFirstFlashLinks, + actLink: relations.actLinks, anchor: data.actAnchors, color: data.actColors, - name: data.actNames, coverGrid: relations.actCoverGrids, coverGridImages: relations.actCoverGridImages, @@ -132,8 +128,7 @@ export default { colorVariables, anchor, color, - name, - firstFlashLink, + actLink, coverGrid, coverGridImages, @@ -146,7 +141,7 @@ export default { id: anchor, style: colorVariables.slot('color', color).content, }, - firstFlashLink.slot('content', name)), + actLink), coverGrid.slots({ links: coverGridLinks, diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js index 553d2f54..09c6b37c 100644 --- a/src/content/dependencies/generateFlashInfoPage.js +++ b/src/content/dependencies/generateFlashInfoPage.js @@ -4,13 +4,13 @@ export default { contentDependencies: [ 'generateContentHeading', 'generateContributionList', + 'generateFlashActSidebar', 'generateFlashCoverArtwork', 'generateFlashNavAccent', - 'generateFlashSidebar', 'generatePageLayout', 'generateTrackList', 'linkExternal', - 'linkFlashIndex', + 'linkFlashAct', ], extraDependencies: ['html', 'language'], @@ -41,7 +41,7 @@ export default { relation('generatePageLayout'); relations.sidebar = - relation('generateFlashSidebar', flash); + relation('generateFlashActSidebar', flash.act, flash); if (query.urls) { relations.externalLinks = @@ -59,8 +59,8 @@ export default { const nav = sections.nav = {}; - nav.flashIndexLink = - relation('linkFlashIndex'); + nav.flashActLink = + relation('linkFlashAct', flash.act); nav.flashNavAccent = relation('generateFlashNavAccent', flash); @@ -163,14 +163,11 @@ export default { navLinkStyle: 'hierarchical', navLinks: [ {auto: 'home'}, - {html: sec.nav.flashIndexLink}, + {html: sec.nav.flashActLink.slot('color', false)}, {auto: 'current'}, ], - navBottomRowContent: - sec.nav.flashNavAccent.slots({ - showFlashNavigation: true, - }), + navBottomRowContent: sec.nav.flashNavAccent, ...relations.sidebar, }); diff --git a/src/content/dependencies/generateFlashNavAccent.js b/src/content/dependencies/generateFlashNavAccent.js index 2c8205d3..57196d06 100644 --- a/src/content/dependencies/generateFlashNavAccent.js +++ b/src/content/dependencies/generateFlashNavAccent.js @@ -55,13 +55,8 @@ export default { return relations; }, - slots: { - showFlashNavigation: {type: 'boolean', default: false}, - }, - - generate(relations, slots, {html, language}) { + generate(relations, {html, language}) { const {content: previousNextLinks = []} = - slots.showFlashNavigation && relations.previousNextLinks && relations.previousNextLinks.slots({ previousLink: relations.previousFlashLink, diff --git a/src/content/dependencies/generateFlashSidebar.js b/src/content/dependencies/generateFlashSidebar.js deleted file mode 100644 index ba761922..00000000 --- a/src/content/dependencies/generateFlashSidebar.js +++ /dev/null @@ -1,236 +0,0 @@ -import {stitchArrays} from '#sugar'; - -export default { - contentDependencies: ['linkFlash', 'linkFlashIndex'], - extraDependencies: ['html', 'wikiData'], - - // So help me Gog, the flash sidebar is heavily hard-coded. - - sprawl: ({flashActData}) => ({flashActData}), - - query(sprawl, flash) { - const flashActs = - sprawl.flashActData.slice(); - - const act6 = - flashActs - .findIndex(act => act.name.startsWith('Act 6')); - - const postCanon = - flashActs - .findIndex(act => act.name.includes('Post Canon')); - - const outsideCanon = - postCanon + - flashActs - .slice(postCanon) - .findIndex(act => !act.name.includes('Post Canon')); - - const currentAct = flash.act; - - const actIndex = - flashActs - .indexOf(currentAct); - - const side = - (actIndex < 0 - ? 0 - : actIndex < act6 - ? 1 - : actIndex < outsideCanon - ? 2 - : 3); - - const sideActs = - flashActs - .filter((act, index) => - act.name.startsWith('Act 1') || - act.name.startsWith('Act 6 Act 1') || - act.name.startsWith('Hiveswap') || - index >= outsideCanon); - - const currentSideIndex = - sideActs - .findIndex(act => { - if (act.name.startsWith('Act 1')) { - return side === 1; - } else if (act.name.startsWith('Act 6 Act 1')) { - return side === 2; - } else if (act.name.startsWith('Hiveswap Act 1')) { - return side === 3; - } else { - return act === currentAct; - } - }) - - const sideNames = - sideActs - .map(act => { - if (act.name.startsWith('Act 1')) { - return `Side 1 (Acts 1-5)`; - } else if (act.name.startsWith('Act 6 Act 1')) { - return `Side 2 (Acts 6-7)`; - } else if (act.name.startsWith('Hiveswap Act 1')) { - return `Outside Canon (Misc. Games)`; - } else { - return act.name; - } - }); - - const sideColors = - sideActs - .map(act => { - if (act.name.startsWith('Act 1')) { - return '#4ac925'; - } else if (act.name.startsWith('Act 6 Act 1')) { - return '#1076a2'; - } else if (act.name.startsWith('Hiveswap Act 1')) { - return '#008282'; - } else { - return act.color; - } - }); - - const sideFirstFlashes = - sideActs - .map(act => act.flashes[0]); - - const scopeActs = - flashActs - .filter((act, index) => { - if (index < act6) { - return side === 1; - } else if (index < outsideCanon) { - return side === 2; - } else { - return false; - } - }); - - const currentScopeActIndex = - scopeActs.indexOf(currentAct); - - const scopeActNames = - scopeActs - .map(act => act.name); - - const scopeActFirstFlashes = - scopeActs - .map(act => act.flashes[0]); - - const currentActFlashes = - currentAct.flashes; - - const currentFlashIndex = - currentActFlashes - .indexOf(flash); - - return { - currentSideIndex, - sideNames, - sideColors, - sideFirstFlashes, - - currentScopeActIndex, - scopeActNames, - scopeActFirstFlashes, - - currentActFlashes, - currentFlashIndex, - }; - }, - - relations: (relation, query) => ({ - flashIndexLink: - relation('linkFlashIndex'), - - sideFirstFlashLinks: - query.sideFirstFlashes - .map(flash => relation('linkFlash', flash)), - - scopeActFirstFlashLinks: - query.scopeActFirstFlashes - .map(flash => relation('linkFlash', flash)), - - currentActFlashLinks: - query.currentActFlashes - .map(flash => relation('linkFlash', flash)), - }), - - data: (query) => ({ - currentSideIndex: query.currentSideIndex, - sideColors: query.sideColors, - sideNames: query.sideNames, - - currentScopeActIndex: query.currentScopeActIndex, - scopeActNames: query.scopeActNames, - - currentFlashIndex: query.currentFlashIndex, - }), - - generate(data, relations, {html}) { - const currentActFlashList = - html.tag('ul', - relations.currentActFlashLinks - .map((flashLink, index) => - html.tag('li', - {class: index === data.currentFlashIndex && 'current'}, - flashLink))); - - return { - leftSidebarContent: html.tags([ - html.tag('h1', relations.flashIndexLink), - - html.tag('dl', - stitchArrays({ - sideFirstFlashLink: relations.sideFirstFlashLinks, - sideColor: data.sideColors, - sideName: data.sideNames, - }).map(({sideFirstFlashLink, sideColor, sideName}, index) => [ - // Side acts are displayed whether part of Homestuck proper or - // not, and they're always the same regardless the current flash - // page. Scope acts, if applicable, and the list of flashes - // belonging to the current act, will be inserted after the - // heading of the current side. - html.tag('dt', - {class: [ - 'side', - index === data.currentSideIndex && 'current', - ]}, - sideFirstFlashLink.slots({ - color: sideColor, - content: sideName, - })), - - // Scope acts are only applicable when inside Homestuck proper. - // Hiveswap and all acts beyond are each considered to be its - // own "side". - index === data.currentSideIndex && - data.currentScopeActIndex !== -1 && - stitchArrays({ - scopeActFirstFlashLink: relations.scopeActFirstFlashLinks, - scopeActName: data.scopeActNames, - }).map(({scopeActFirstFlashLink, scopeActName}, index) => [ - html.tag('dt', - {class: index === data.currentScopeActIndex && 'current'}, - scopeActFirstFlashLink.slot('content', scopeActName)), - - // Inside Homestuck proper, the flash list of the current - // act should show after the heading for the relevant - // scope act. - index === data.currentScopeActIndex && - html.tag('dd', currentActFlashList), - ]), - - // Outside of Homestuck proper, the current act is represented - // by a side instead of a scope act, so place its flash list - // after the heading for the relevant side. - index === data.currentSideIndex && - data.currentScopeActIndex === -1 && - html.tag('dd', currentActFlashList), - ])), - - ]), - }; - }, -}; diff --git a/src/content/dependencies/linkFlashAct.js b/src/content/dependencies/linkFlashAct.js new file mode 100644 index 00000000..fbb819ed --- /dev/null +++ b/src/content/dependencies/linkFlashAct.js @@ -0,0 +1,14 @@ +export default { + contentDependencies: ['linkThing'], + extraDependencies: ['html'], + + relations: (relation, flashAct) => + ({link: relation('linkThing', 'localized.flashActGallery', flashAct)}), + + data: (flashAct) => + ({name: flashAct.name}), + + generate: (data, relations, {html}) => + relations.link + .slot('content', new html.Tag(null, null, data.name)), +}; diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js new file mode 100644 index 00000000..b3a54747 --- /dev/null +++ b/src/content/dependencies/listArtTagNetwork.js @@ -0,0 +1 @@ +export default {generate() {}}; diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 8fb1edfa..52e30f88 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -12,6 +12,7 @@ import { import { color, contributionList, + directory, fileExtension, name, referenceList, @@ -117,12 +118,15 @@ export class Flash extends Thing { } export class FlashAct extends Thing { + static [Thing.referenceType] = 'flash-act'; + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose name: name('Unnamed Flash Act'), + directory: directory(), color: color(), - anchor: simpleString(), + jump: simpleString(), jumpColor: { diff --git a/src/data/yaml.js b/src/data/yaml.js index c799be5f..0ecc1f1e 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -434,8 +434,10 @@ export const processFlashDocument = makeProcessDocument(T.Flash, { export const processFlashActDocument = makeProcessDocument(T.FlashAct, { propertyFieldMapping: { name: 'Act', + directory: 'Directory', + color: 'Color', - anchor: 'Anchor', + jump: 'Jump', jumpColor: 'Jump Color', }, diff --git a/src/find.js b/src/find.js index 5a249c28..8c9413b7 100644 --- a/src/find.js +++ b/src/find.js @@ -150,6 +150,10 @@ const find = { referenceTypes: ['flash'], }), + flashAct: findHelper({ + referenceTypes: ['flash-act'], + }), + group: findHelper({ referenceTypes: ['group', 'group-gallery'], }), @@ -190,6 +194,7 @@ export function bindFind(wikiData, opts1) { artist: 'artistData', artTag: 'artTagData', flash: 'flashData', + flashAct: 'flashActData', group: 'groupData', listing: 'listingSpec', newsEntry: 'newsData', diff --git a/src/page/flash-act.js b/src/page/flash-act.js new file mode 100644 index 00000000..e54525ae --- /dev/null +++ b/src/page/flash-act.js @@ -0,0 +1,23 @@ +export const description = `flash act gallery pages`; + +export function condition({wikiData}) { + return wikiData.wikiInfo.enableFlashesAndGames; +} + +export function targets({wikiData}) { + return wikiData.flashActData; +} + +export function pathsForTarget(flashAct) { + return [ + { + type: 'page', + path: ['flashActGallery', flashAct.directory], + + contentFunction: { + name: 'generateFlashActGalleryPage', + args: [flashAct], + }, + }, + ]; +} diff --git a/src/page/index.js b/src/page/index.js index 48e22d2e..21d93c8f 100644 --- a/src/page/index.js +++ b/src/page/index.js @@ -2,6 +2,7 @@ export * as album from './album.js'; export * as artist from './artist.js'; export * as artistAlias from './artist-alias.js'; export * as flash from './flash.js'; +export * as flashAct from './flash-act.js'; export * as group from './group.js'; export * as homepage from './homepage.js'; export * as listing from './listing.js'; diff --git a/src/static/site5.css b/src/static/site5.css index 7b3e3e03..b7f1b669 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -285,6 +285,8 @@ body::before { .sidebar > h3, .sidebar > p { text-align: center; + padding-left: 4px; + padding-right: 4px; } .sidebar h1 { diff --git a/src/strings-default.json b/src/strings-default.json index 904d25d4..7a7fa041 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -323,6 +323,12 @@ "flashIndex.title": "Flashes & Games", "flashPage.title": "{FLASH}", "flashPage.nav.flash": "{FLASH}", + "flashSidebar.flashList.flashesInThisAct": "Flashes in this act", + "flashSidebar.flashList.flashesInThisStory": "Flashes in this story", + "flashSidebar.flashList.flashesInThisSection": "Flashes in this section", + "flashSidebar.flashList.volumesInThisGame": "Volumes in this game", + "flashSidebar.flashList.gamesInThisSeries": "Games in this series", + "flashSidebar.flashList.entriesInThisSection": "Entries in this section", "groupSidebar.title": "Groups", "groupSidebar.groupList.category": "{CATEGORY}", "groupSidebar.groupList.item": "{GROUP}", diff --git a/src/url-spec.js b/src/url-spec.js index 4d103134..2ff0fa5b 100644 --- a/src/url-spec.js +++ b/src/url-spec.js @@ -37,6 +37,8 @@ const urlSpec = { flashIndex: 'flash/', flash: 'flash/<>/', + flashActGallery: 'flash-act/<>/', + groupInfo: 'group/<>/', groupGallery: 'group/<>/gallery/', -- cgit 1.3.0-6-gf8a5 From e842ce93e6405334b6ef475ec1db41e051cfd2b5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 11 Oct 2023 14:49:43 -0300 Subject: data: use flash act directory for better determinism --- src/data/yaml.js | 1 + src/util/wiki-data.js | 15 ++------------- 2 files changed, 3 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index 0ecc1f1e..a2811d43 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1391,6 +1391,7 @@ export function filterDuplicateDirectories(wikiData) { 'albumData', 'artTagData', 'flashData', + 'flashActData', 'groupData', 'newsData', 'trackData', diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index ac652b27..0790ae91 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -610,20 +610,9 @@ export function sortFlashesChronologically(data, { latestFirst = false, getDate, } = {}) { - // Flash acts don't actually have any identifying properties because they - // don't have dedicated pages (yet), so don't have a directory. Make up a - // fake key identifying them so flashes can be grouped together. - const flashActs = new Set(data.map(flash => flash.act)); - const flashActIdentifiers = new Map(); - - let counter = 0; - for (const act of flashActs) { - flashActIdentifiers.set(act, ++counter); - } - // Group flashes by act... - data.sort((a, b) => { - return flashActIdentifiers.get(a.act) - flashActIdentifiers.get(b.act); + sortByDirectory(data, { + getDirectory: flash => flash.act.directory, }); // Sort flashes by position in act... -- cgit 1.3.0-6-gf8a5 From 428e14394209f55215168b9acbe680a982f9beb6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 11 Oct 2023 15:11:42 -0300 Subject: content: transformContent: add [[flash-act]] tag --- src/content/dependencies/transformContent.js | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 9a5ac456..3c2c3521 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -53,6 +53,10 @@ export const replacerSpec = { } }, }, + 'flash-act': { + find: 'flashAct', + link: 'flashAct', + }, group: { find: 'group', link: 'groupInfo', @@ -119,6 +123,7 @@ const linkThingRelationMap = { artist: 'linkArtist', artistGallery: 'linkArtistGallery', flash: 'linkFlash', + flashAct: 'linkFlashAct', groupInfo: 'linkGroup', groupGallery: 'linkGroupGallery', listing: 'linkListing', -- cgit 1.3.0-6-gf8a5 From 4e2dae523e7bf8b49272bd6afcba86a8157af4a1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 18 Oct 2023 14:25:27 -0300 Subject: data: add [Thing.friendlyName] property to some Thing subclasses --- src/data/things/album.js | 2 ++ src/data/things/art-tag.js | 1 + src/data/things/flash.js | 1 + src/data/things/group.js | 2 ++ src/data/things/homepage-layout.js | 6 ++++++ src/data/things/news-entry.js | 1 + src/data/things/static-page.js | 1 + src/data/things/thing.js | 1 + src/data/things/wiki-info.js | 2 ++ 9 files changed, 17 insertions(+) (limited to 'src') diff --git a/src/data/things/album.js b/src/data/things/album.js index f451a7e9..546fda3b 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -181,6 +181,8 @@ export class Album extends Thing { } export class TrackSectionHelper extends Thing { + static [Thing.friendlyName] = `Track Section`; + static [Thing.getPropertyDescriptors] = () => ({ name: name('Unnamed Track Section'), color: color(), diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 1266a4e0..6503beec 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -16,6 +16,7 @@ import Thing from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; + static [Thing.friendlyName] = `Art Tag`; static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ // Update & expose diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 52e30f88..e3ef9f5c 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -119,6 +119,7 @@ export class Flash extends Thing { export class FlashAct extends Thing { static [Thing.referenceType] = 'flash-act'; + static [Thing.friendlyName] = `Flash Act`; static [Thing.getPropertyDescriptors] = () => ({ // Update & expose diff --git a/src/data/things/group.js b/src/data/things/group.js index d5ae03e7..8764a9db 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -83,6 +83,8 @@ export class Group extends Thing { } export class GroupCategory extends Thing { + static [Thing.friendlyName] = `Group Category`; + static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index de9d0e50..bfa971ca 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -26,6 +26,8 @@ import { import Thing from './thing.js'; export class HomepageLayout extends Thing { + static [Thing.friendlyName] = `Homepage Layout`; + static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ // Update & expose @@ -47,6 +49,8 @@ export class HomepageLayout extends Thing { } export class HomepageLayoutRow extends Thing { + static [Thing.friendlyName] = `Homepage Row`; + static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({ // Update & expose @@ -75,6 +79,8 @@ export class HomepageLayoutRow extends Thing { } export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { + static [Thing.friendlyName] = `Homepage Albums Row`; + static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({ ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index ba065c25..36da0299 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -9,6 +9,7 @@ import Thing from './thing.js'; export class NewsEntry extends Thing { static [Thing.referenceType] = 'news-entry'; + static [Thing.friendlyName] = `News Entry`; static [Thing.getPropertyDescriptors] = () => ({ // Update & expose diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index f03e4405..ab9c5f98 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -10,6 +10,7 @@ import Thing from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; + static [Thing.friendlyName] = `Static Page`; static [Thing.getPropertyDescriptors] = () => ({ // Update & expose diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a47f8506..def7e914 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -9,6 +9,7 @@ import CacheableObject from './cacheable-object.js'; export default class Thing extends CacheableObject { static referenceType = Symbol.for('Thing.referenceType'); + static friendlyName = Symbol.for(`Thing.friendlyName`); static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 0460f272..6286a267 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -14,6 +14,8 @@ import { import Thing from './thing.js'; export class WikiInfo extends Thing { + static [Thing.friendlyName] = `Wiki Info`; + static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose -- cgit 1.3.0-6-gf8a5 From 645a127bef38c3a7a2ef1b94d23b25fb7bdc4191 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 18 Oct 2023 14:26:49 -0300 Subject: data, test: wrap property value errors with proper class & cause --- src/data/things/cacheable-object.js | 19 ++++++++++++------- src/data/things/index.js | 6 +++++- 2 files changed, 17 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 4bc3668d..9fda865e 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -179,13 +179,8 @@ export default class CacheableObject { } else if (result !== true) { throw new TypeError(`Validation failed for value ${newValue}`); } - } catch (error) { - error.message = [ - `Property ${colors.green(property)}`, - `(${inspect(this[property])} -> ${inspect(newValue)}):`, - error.message - ].join(' '); - throw error; + } catch (caughtError) { + throw new CacheableObjectPropertyValueError(property, this[property], newValue, caughtError); } } @@ -359,3 +354,13 @@ export default class CacheableObject { return object.#propertyUpdateValues[key] ?? null; } } + +export class CacheableObjectPropertyValueError extends Error { + constructor(property, oldValue, newValue, error) { + super( + `Error setting ${colors.green(property)} (${inspect(oldValue)} -> ${inspect(newValue)})`, + {cause: error}); + + this.property = property; + } +} diff --git a/src/data/things/index.js b/src/data/things/index.js index 77e5fa76..4ea1f007 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -21,7 +21,11 @@ import * as trackClasses from './track.js'; import * as wikiInfoClasses from './wiki-info.js'; export {default as Thing} from './thing.js'; -export {default as CacheableObject} from './cacheable-object.js'; + +export { + default as CacheableObject, + CacheableObjectPropertyValueError, +} from './cacheable-object.js'; const allClassLists = { 'album.js': albumClasses, -- cgit 1.3.0-6-gf8a5 From 167e3ba07b54e6b9b780258fe8c10abd1ad80c2f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 18 Oct 2023 14:30:21 -0300 Subject: yaml: cosmetic code clean-up --- src/data/yaml.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index a2811d43..33ca736d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -59,7 +59,7 @@ export const DATA_STATIC_PAGE_DIRECTORY = 'static-page'; // document and apply the configuration passed to makeProcessDocument in order // to construct a Thing subclass. function makeProcessDocument( - thingClass, + thingConstructor, { // Optional early step for transforming field values before providing them // to the Thing's update() method. This is useful when the input format @@ -110,7 +110,7 @@ function makeProcessDocument( invalidFieldCombinations = [], } ) { - if (!thingClass) { + if (!thingConstructor) { throw new Error(`Missing Thing class`); } @@ -152,7 +152,7 @@ function makeProcessDocument( .filter((field) => !knownFields.includes(field)); if (!empty(unknownFields)) { - throw new makeProcessDocument.UnknownFieldsError(unknownFields); + aggregate.push(new UnknownFieldsError(unknownFields)); } const presentFields = Object.keys(document); @@ -162,18 +162,16 @@ function makeProcessDocument( for (const {message, fields} of invalidFieldCombinations) { const fieldsPresent = presentFields.filter(field => fields.includes(field)); - if (fieldsPresent.length <= 1) { - continue; + if (fieldsPresent.length >= 2) { + fieldCombinationErrors.push( + new FieldCombinationError( + filterProperties(document, fieldsPresent), + message)); } - - fieldCombinationErrors.push( - new makeProcessDocument.FieldCombinationError( - filterProperties(document, fieldsPresent), - message)); } if (!empty(fieldCombinationErrors)) { - throw new makeProcessDocument.FieldCombinationsError(fieldCombinationErrors); + aggregate.push(new FieldCombinationAggregateError(fieldCombinationErrors)); } const fieldValues = {}; @@ -193,9 +191,9 @@ function makeProcessDocument( sourceProperties[property] = value; } - const thing = Reflect.construct(thingClass, []); + const thing = Reflect.construct(thingConstructor, []); - withAggregate({message: `Errors applying ${colors.green(thingClass.name)} properties`}, ({call}) => { + withAggregate({message: `Errors applying ${colors.green(thingConstructor.name)} properties`}, ({call}) => { for (const [property, value] of Object.entries(sourceProperties)) { call(() => (thing[property] = value)); } @@ -212,20 +210,20 @@ function makeProcessDocument( return fn; } -makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends Error { +export class UnknownFieldsError extends Error { constructor(fields) { super(`Unknown fields present: ${fields.join(', ')}`); this.fields = fields; } -}; +} -makeProcessDocument.FieldCombinationsError = class FieldCombinationsError extends AggregateError { +export class FieldCombinationAggregateError extends AggregateError { constructor(errors) { super(errors, `Errors in combinations of fields present`); } -}; +} -makeProcessDocument.FieldCombinationError = class FieldCombinationError extends Error { +export class FieldCombinationError extends Error { constructor(fields, message) { const fieldNames = Object.keys(fields); const combinePart = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; @@ -933,6 +931,10 @@ export const dataSteps = [ { title: `Process homepage layout file`, + + // Kludge: This benefits from the same headerAndEntries style messaging as + // albums and tracks (for example), but that document mode is designed to + // support multiple files, and only one is actually getting processed here. files: [HOMEPAGE_LAYOUT_DATA_FILE], documentMode: documentModes.headerAndEntries, -- cgit 1.3.0-6-gf8a5 From d8d877b63eec2e7c1d1afbca84b7f3cf6d24ca35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 18 Oct 2023 14:32:00 -0300 Subject: yaml: filter and skip properties, not entire documents --- src/data/yaml.js | 260 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 164 insertions(+), 96 deletions(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index 33ca736d..06ef5546 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -10,7 +10,12 @@ import yaml from 'js-yaml'; import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import find, {bindFind} from '#find'; import {traverse} from '#node-utils'; -import T, {CacheableObject, Thing} from '#things'; + +import T, { + CacheableObject, + CacheableObjectPropertyValueError, + Thing, +} from '#things'; import { conditionallySuppressError, @@ -144,6 +149,25 @@ function makeProcessDocument( }; const fn = decorateErrorWithName((document) => { + const nameField = propertyFieldMapping['name']; + const namePart = + (nameField + ? (document[nameField] + ? ` named ${colors.green(`"${document[nameField]}"`)}` + : ` (name field, "${nameField}", not specified)`) + : ``); + + const constructorPart = + (thingConstructor[Thing.friendlyName] + ? colors.green(thingConstructor[Thing.friendlyName]) + : thingConstructor.name + ? colors.green(thingConstructor.name) + : `document`); + + const aggregate = openAggregate({ + message: `Errors processing ${constructorPart}` + namePart, + }); + const documentEntries = Object.entries(document) .filter(([field]) => !ignoredFields.includes(field)); @@ -187,19 +211,31 @@ function makeProcessDocument( const sourceProperties = {}; for (const [field, value] of Object.entries(fieldValues)) { - const property = fieldPropertyMapping[field]; - sourceProperties[property] = value; + if (Object.hasOwn(fieldPropertyMapping, field)) { + const property = fieldPropertyMapping[field]; + sourceProperties[property] = value; + } } const thing = Reflect.construct(thingConstructor, []); - withAggregate({message: `Errors applying ${colors.green(thingConstructor.name)} properties`}, ({call}) => { - for (const [property, value] of Object.entries(sourceProperties)) { - call(() => (thing[property] = value)); + const fieldValueErrors = []; + + // This for loop would like to certify itself as "not into capitalism". + for (const [property, value] of Object.entries(sourceProperties)) { + const field = propertyFieldMapping[property]; + try { + thing[property] = value; + } catch (caughtError) { + fieldValueErrors.push(new FieldValueError(field, property, value, caughtError)); } - }); + } - return thing; + if (!empty(fieldValueErrors)) { + aggregate.push(new FieldValueAggregateError(thingConstructor, fieldValueErrors)); + } + + return {thing, aggregate}; }); Object.assign(fn, { @@ -212,7 +248,7 @@ function makeProcessDocument( export class UnknownFieldsError extends Error { constructor(fields) { - super(`Unknown fields present: ${fields.join(', ')}`); + super(`Unknown fields present: ${fields.map(field => colors.red(field)).join(', ')}`); this.fields = fields; } } @@ -240,6 +276,25 @@ export class FieldCombinationError extends Error { } } +export class FieldValueAggregateError extends AggregateError { + constructor(thingConstructor, errors) { + super(errors, `Errors processing field values for ${colors.green(thingConstructor.name)}`); + } +} + +export class FieldValueError extends Error { + constructor(field, property, value, caughtError) { + const cause = + (caughtError instanceof CacheableObjectPropertyValueError + ? caughtError.cause + : caughtError); + + super( + `Failed to set ${colors.green(`"${field}"`)} field (${colors.green(property)}) to ${inspect(value)}`, + {cause}); + } +} + export const processAlbumDocument = makeProcessDocument(T.Album, { fieldTransformations: { 'Artists': parseContributors, @@ -1023,8 +1078,8 @@ export async function loadAndProcessDataDocuments({dataPath}) { for (const dataStep of dataSteps) { await processDataAggregate.nestAsync( - {message: `Errors during data step: ${dataStep.title}`}, - async ({call, callAsync, map, mapAsync, nest}) => { + {message: `Errors during data step: ${colors.bright(dataStep.title)}`}, + async ({call, callAsync, map, mapAsync, push, nest}) => { const {documentMode} = dataStep; if (!Object.values(documentModes).includes(documentMode)) { @@ -1149,32 +1204,52 @@ export async function loadAndProcessDataDocuments({dataPath}) { return; } - const yamlResult = - documentMode === documentModes.oneDocumentTotal - ? call(yaml.load, readResult) - : call(yaml.loadAll, readResult); + let processResults; - if (!yamlResult) { - return; - } + switch (documentMode) { + case documentModes.oneDocumentTotal: { + const yamlResult = call(yaml.load, readResult); - let processResults; + if (!yamlResult) { + processResults = null; + break; + } + + const {thing, aggregate} = + dataStep.processDocument(yamlResult); + + processResults = thing; + + call(() => aggregate.close()); - if (documentMode === documentModes.oneDocumentTotal) { - nest({message: `Errors processing document`}, ({call}) => { - processResults = call(dataStep.processDocument, yamlResult); - }); - } else { - const {documents, aggregate: aggregate1} = filterBlankDocuments(yamlResult); - call(aggregate1.close); - - const {result, aggregate: aggregate2} = mapAggregate( - documents, - decorateErrorWithIndex(dataStep.processDocument), - {message: `Errors processing documents`}); - call(aggregate2.close); - - processResults = result; + break; + } + + case documentModes.allInOne: { + const yamlResults = call(yaml.loadAll, readResult); + + if (!yamlResults) { + processResults = []; + return; + } + + const {documents, aggregate: filterAggregate} = + filterBlankDocuments(yamlResults); + + call(filterAggregate.close); + + processResults = []; + + map(documents, decorateErrorWithIndex(document => { + const {thing, aggregate} = + dataStep.processDocument(document); + + processResults.push(thing); + aggregate.close(); + }), {message: `Errors processing documents`}); + + break; + } } if (!processResults) return; @@ -1232,81 +1307,74 @@ export async function loadAndProcessDataDocuments({dataPath}) { return {file, documents: filteredDocuments}; }); - let processResults; + const processResults = []; - if (documentMode === documentModes.headerAndEntries) { - nest({message: `Errors processing data files as valid documents`}, ({call, map}) => { - processResults = []; + switch (documentMode) { + case documentModes.headerAndEntries: + map(yamlResults, decorateErrorWithFile(({documents}) => { + const headerDocument = documents[0]; + const entryDocuments = documents.slice(1).filter(Boolean); - yamlResults.forEach(({file, documents}) => { - const [headerDocument, ...entryDocuments] = documents; + if (!headerDocument) + throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`); - if (!headerDocument) { - call(decorateErrorWithFile(() => { - throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`); - }), {file}); - return; - } + // This'll be decorated with the file, and groups together any + // errors from processing the header and entry documents. + const fileAggregate = + openAggregate({message: `Errors processing documents`}); - const header = call( - decorateErrorWithFile(({document}) => - dataStep.processHeaderDocument(document)), - {file, document: headerDocument}); + const {thing: headerObject, aggregate: headerAggregate} = + dataStep.processHeaderDocument(headerDocument); - // Don't continue processing files whose header - // document is invalid - the entire file is excempt - // from data in this case. - if (!header) { - return; + try { + headerAggregate.close() + } catch (caughtError) { + caughtError.message = `(${colors.yellow(`header`)}) ${caughtError.message}`; + fileAggregate.push(caughtError); } - const entries = map( - entryDocuments - .filter(Boolean) - .map((document) => ({file, document})), - decorateErrorWithFile( - decorateErrorWithIndex(({document}) => - dataStep.processEntryDocument(document))), - {message: `Errors processing entry documents`}); - - // Entries may be incomplete (i.e. any errored - // documents won't have a processed output - // represented here) - this is intentional! By - // principle, partial output is preferred over - // erroring an entire file. - processResults.push({header, entries}); - }); - }); - } + const entryObjects = []; - if (documentMode === documentModes.onePerFile) { - nest({message: `Errors processing data files as valid documents`}, ({call}) => { - processResults = []; + for (let index = 0; index < entryDocuments.length; index++) { + const entryDocument = entryDocuments[index]; - yamlResults.forEach(({file, documents}) => { - if (documents.length > 1) { - call(decorateErrorWithFile(() => { - throw new Error(`Only expected one document to be present per file`); - }), {file}); - return; - } else if (empty(documents) || !documents[0]) { - call(decorateErrorWithFile(() => { - throw new Error(`Expected a document, this file is empty`); - }), {file}); - } + const {thing: entryObject, aggregate: entryAggregate} = + dataStep.processEntryDocument(entryDocument); - const result = call( - decorateErrorWithFile(({document}) => - dataStep.processDocument(document)), - {file, document: documents[0]}); + entryObjects.push(entryObject); - if (!result) { - return; + try { + entryAggregate.close(); + } catch (caughtError) { + caughtError.message = `(${colors.yellow(`entry #${index + 1}`)}) ${caughtError.message}`; + fileAggregate.push(caughtError); + } } - processResults.push(result); - }); - }); + processResults.push({ + header: headerObject, + entries: entryObjects, + }); + + fileAggregate.close(); + }), {message: `Errors processing documents in data files`}); + break; + + case documentModes.onePerFile: + map(yamlResults, decorateErrorWithFile(({documents}) => { + if (documents.length > 1) + throw new Error(`Only expected one document to be present per file, got ${documents.length} here`); + + if (empty(documents) || !documents[0]) + throw new Error(`Expected a document, this file is empty`); + + const {thing, aggregate} = + dataStep.processDocument(documents[0]); + + processResults.push(thing); + aggregate.close(); + }), {message: `Errors processing data files as valid documents`}); + break; } const saveResult = call(dataStep.save, processResults); -- cgit 1.3.0-6-gf8a5 From ea1b8196ba240d2cc4c64a9079947028cb536bf8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 18 Oct 2023 14:53:38 -0300 Subject: sugar: filterProperties: preserve original order if specified --- src/util/sugar.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/util/sugar.js b/src/util/sugar.js index 2e724bae..3e39e98f 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -168,7 +168,9 @@ export function setIntersection(set1, set2) { return intersection; } -export function filterProperties(object, properties) { +export function filterProperties(object, properties, { + preserveOriginalOrder = false, +} = {}) { if (typeof object !== 'object' || object === null) { throw new TypeError(`Expected object to be an object, got ${typeAppearance(object)}`); } @@ -179,9 +181,17 @@ export function filterProperties(object, properties) { const filteredObject = {}; - for (const property of properties) { - if (Object.hasOwn(object, property)) { - filteredObject[property] = object[property]; + if (preserveOriginalOrder) { + for (const property of Object.keys(object)) { + if (properties.includes(property)) { + filteredObject[property] = object[property]; + } + } + } else { + for (const property of properties) { + if (Object.hasOwn(object, property)) { + filteredObject[property] = object[property]; + } } } -- cgit 1.3.0-6-gf8a5 From 84d22c117d6deabd53aaee1546e3a99f5d6049c7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Wed, 18 Oct 2023 14:54:38 -0300 Subject: yaml: track skipped fields separately & report summary at bottom --- src/data/yaml.js | 80 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index 06ef5546..f49f48dd 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -171,12 +171,18 @@ function makeProcessDocument( const documentEntries = Object.entries(document) .filter(([field]) => !ignoredFields.includes(field)); + const skippedFields = new Set(); + const unknownFields = documentEntries .map(([field]) => field) .filter((field) => !knownFields.includes(field)); if (!empty(unknownFields)) { aggregate.push(new UnknownFieldsError(unknownFields)); + + for (const field of unknownFields) { + skippedFields.add(field); + } } const presentFields = Object.keys(document); @@ -187,10 +193,17 @@ function makeProcessDocument( const fieldsPresent = presentFields.filter(field => fields.includes(field)); if (fieldsPresent.length >= 2) { - fieldCombinationErrors.push( - new FieldCombinationError( - filterProperties(document, fieldsPresent), - message)); + const filteredDocument = + filterProperties( + document, + fieldsPresent, + {preserveOriginalOrder: true}); + + fieldCombinationErrors.push(new FieldCombinationError(filteredDocument, message)); + + for (const field of Object.keys(filteredDocument)) { + skippedFields.add(field); + } } } @@ -201,6 +214,7 @@ function makeProcessDocument( const fieldValues = {}; for (const [field, value] of documentEntries) { + if (skippedFields.has(field)) continue; if (Object.hasOwn(fieldTransformations, field)) { fieldValues[field] = fieldTransformations[field](value); } else { @@ -211,10 +225,8 @@ function makeProcessDocument( const sourceProperties = {}; for (const [field, value] of Object.entries(fieldValues)) { - if (Object.hasOwn(fieldPropertyMapping, field)) { - const property = fieldPropertyMapping[field]; - sourceProperties[property] = value; - } + const property = fieldPropertyMapping[field]; + sourceProperties[property] = value; } const thing = Reflect.construct(thingConstructor, []); @@ -227,6 +239,7 @@ function makeProcessDocument( try { thing[property] = value; } catch (caughtError) { + skippedFields.add(field); fieldValueErrors.push(new FieldValueError(field, property, value, caughtError)); } } @@ -235,6 +248,15 @@ function makeProcessDocument( aggregate.push(new FieldValueAggregateError(thingConstructor, fieldValueErrors)); } + if (skippedFields.size >= 1) { + aggregate.push( + new SkippedFieldsSummaryError( + filterProperties( + document, + Array.from(skippedFields), + {preserveOriginalOrder: true}))); + } + return {thing, aggregate}; }); @@ -248,30 +270,37 @@ function makeProcessDocument( export class UnknownFieldsError extends Error { constructor(fields) { - super(`Unknown fields present: ${fields.map(field => colors.red(field)).join(', ')}`); + super(`Unknown fields ignored: ${fields.map(field => colors.red(field)).join(', ')}`); this.fields = fields; } } export class FieldCombinationAggregateError extends AggregateError { constructor(errors) { - super(errors, `Errors in combinations of fields present`); + super(errors, `Invalid field combinations - all involved fields ignored`); } } export class FieldCombinationError extends Error { constructor(fields, message) { const fieldNames = Object.keys(fields); - const combinePart = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; - const messagePart = + const mainMessage = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; + + const causeMessage = (typeof message === 'function' - ? `: ${message(fields)}` + ? message(fields) : typeof message === 'string' - ? `: ${message}` - : ``); + ? message + : null); + + super(mainMessage, { + cause: + (causeMessage + ? new Error(causeMessage) + : null), + }); - super(combinePart + messagePart); this.fields = fields; } } @@ -295,6 +324,25 @@ export class FieldValueError extends Error { } } +export class SkippedFieldsSummaryError extends Error { + constructor(filteredDocument) { + const entries = Object.entries(filteredDocument); + + const lines = + entries.map(([field, value]) => + ` - ${field}: ` + + inspect(value) + .split('\n') + .map((line, index) => index === 0 ? line : ` ${line}`) + .join('\n')); + + super( + colors.bright(colors.yellow(`Altogether, skipped ${entries.length === 1 ? `1 field` : `${entries.length} fields`}:\n`)) + + lines.join('\n') + '\n' + + colors.bright(colors.yellow(`See above errors for details.`))); + } +} + export const processAlbumDocument = makeProcessDocument(T.Album, { fieldTransformations: { 'Artists': parseContributors, -- cgit 1.3.0-6-gf8a5 From 1511c0b0fd35d4f368e72288d48958cb3eff778f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Sun, 22 Oct 2023 14:44:42 -0300 Subject: data: fix bad dependency for Artist.albumsAsCommentator --- src/data/things/artist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/artist.js b/src/data/things/artist.js index ff9f8aee..ea19d2ba 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -100,7 +100,7 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: [this, 'albumData'], + dependencies: ['this', 'albumData'], compute: ({this: artist, albumData}) => albumData?.filter(({commentatorArtists}) => -- cgit 1.3.0-6-gf8a5 From a42078aed0805209ecb4724ea55a35e3909541dc Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 23 Oct 2023 11:56:13 -0300 Subject: css: inset long-content pages a little lighter in thin layout --- src/static/site5.css | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/static/site5.css b/src/static/site5.css index b7f1b669..6076228b 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -689,10 +689,14 @@ p code { margin-bottom: 0; } +main.long-content { + --long-content-padding-ratio: 0.12; +} + main.long-content .main-content-container, main.long-content > h1 { - padding-left: 12%; - padding-right: 12%; + padding-left: calc(var(--long-content-padding-ratio) * 100%); + padding-right: calc(var(--long-content-padding-ratio) * 100%); } dl dt { @@ -1311,8 +1315,8 @@ main.long-content .content-sticky-heading-container { main.long-content .content-sticky-heading-container .content-sticky-heading-row, main.long-content .content-sticky-heading-container .content-sticky-subheading-row { - padding-left: calc(0.12 * (100% - 2 * var(--content-padding)) + var(--content-padding)); - padding-right: calc(0.12 * (100% - 2 * var(--content-padding)) + var(--content-padding)); + padding-left: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); + padding-right: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); } .content-sticky-heading-row { @@ -1744,4 +1748,8 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content #header > div:not(:first-child) { margin-top: 0.5em; } + + main.long-content { + --long-content-padding-ratio: 0.04; + } } -- cgit 1.3.0-6-gf8a5 From 63075c650bf990407e9eefe3e9f135b2425a2ded Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 23 Oct 2023 13:48:47 -0300 Subject: content, css: linkTemplate: new linkless slot --- src/content/dependencies/linkTemplate.js | 35 ++++++++++++++++++-------------- src/content/dependencies/linkThing.js | 2 ++ src/static/site5.css | 8 ++++++++ 3 files changed, 30 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index 7206e960..d9af726c 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -15,6 +15,7 @@ export default { href: {type: 'string'}, path: {validate: v => v.validateArrayItems(v.isString)}, hash: {type: 'string'}, + linkless: {type: 'boolean', default: false}, tooltip: {type: 'string'}, attributes: {validate: v => v.isAttributes}, @@ -33,25 +34,29 @@ export default { let style; let title; - if (slots.href) { - href = encodeURI(slots.href); - } else if (!empty(slots.path)) { - href = to(...slots.path); + if (slots.linkless) { + href = null; } else { - href = ''; - } + if (slots.href) { + href = encodeURI(slots.href); + } else if (!empty(slots.path)) { + href = to(...slots.path); + } else { + href = ''; + } - if (appendIndexHTML) { - if ( - /^(?!https?:\/\/).+\/$/.test(href) && - href.endsWith('/') - ) { - href += 'index.html'; + if (appendIndexHTML) { + if ( + /^(?!https?:\/\/).+\/$/.test(href) && + href.endsWith('/') + ) { + href += 'index.html'; + } } - } - if (slots.hash) { - href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash; + if (slots.hash) { + href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash; + } } if (slots.color) { diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js index e661ca7c..b20b132b 100644 --- a/src/content/dependencies/linkThing.js +++ b/src/content/dependencies/linkThing.js @@ -36,6 +36,7 @@ export default { }, anchor: {type: 'boolean', default: false}, + linkless: {type: 'boolean', default: false}, attributes: {validate: v => v.isAttributes}, hash: {type: 'string'}, @@ -78,6 +79,7 @@ export default { attributes: slots.attributes, hash: slots.hash, + linkless: slots.linkless, }); }, } diff --git a/src/static/site5.css b/src/static/site5.css index 6076228b..d50d4623 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -439,6 +439,14 @@ a.current { font-weight: 800; } +a:not([href]) { + cursor: default; +} + +a:not([href]):hover { + text-decoration: none; +} + .nav-main-links > span > span { white-space: nowrap; } -- cgit 1.3.0-6-gf8a5 From 22b1823dd82bf4fd2063d121c743d02e452fe7f3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 23 Oct 2023 13:49:20 -0300 Subject: content: generateAlbimSidebarTrackSection: handle commentary-less tracks --- .../dependencies/generateAlbumCommentaryPage.js | 1 + .../generateAlbumSidebarTrackSection.js | 24 +++++++++++++++++++--- src/static/site5.css | 4 ++++ 3 files changed, 26 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 2982b037..3ad1549e 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -207,6 +207,7 @@ export default { section.slots({ anchor: true, open: true, + mode: 'commentary', })), ], }); diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index 00e9b621..d3cd37f0 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -37,12 +37,21 @@ export default { trackSection.tracks .map(track => track.directory); + data.tracksAreMissingCommentary = + trackSection.tracks + .map(track => !track.commentary); + return data; }, slots: { anchor: {type: 'boolean'}, open: {type: 'boolean'}, + + mode: { + validate: v => v.is('info', 'commentary'), + default: 'info', + }, }, generate(data, relations, slots, {getColors, html, language}) { @@ -62,14 +71,23 @@ export default { relations.trackLinks.map((trackLink, index) => html.tag('li', { - class: + class: [ data.includesCurrentTrack && index === data.currentTrackIndex && - 'current', + 'current', + + slots.mode === 'commentary' && + data.tracksAreMissingCommentary[index] && + 'no-commentary', + ], }, language.$('albumSidebar.trackList.item', { track: - (slots.anchor + (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index] + ? trackLink.slots({ + linkless: true, + }) + : slots.anchor ? trackLink.slots({ anchor: true, hash: data.trackDirectories[index], diff --git a/src/static/site5.css b/src/static/site5.css index d50d4623..0eb7dcda 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -794,6 +794,10 @@ li > ul { display: none; } +html[data-url-key="localized.albumCommentary"] li.no-commentary { + opacity: 0.7; +} + /* Images */ .image-container { -- cgit 1.3.0-6-gf8a5 From d62e174c5b7d71c38270ef93da7ad4b640c4d72b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Mon, 23 Oct 2023 23:43:44 -0300 Subject: client: don't have album sidebar break on tracks w/o href --- src/static/client2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/static/client2.js b/src/static/client2.js index 4f4a7153..758d91a6 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -1241,7 +1241,7 @@ function getAlbumCommentarySidebarReferences() { info.sidebarTrackDirectories = info.sidebarTrackLinks - .map(el => el.getAttribute('href').slice(1)); + .map(el => el.getAttribute('href')?.slice(1) ?? null); info.sidebarTrackSections = Array.from(info.sidebar.getElementsByTagName('details')); -- cgit 1.3.0-6-gf8a5 From 66d529179fc1896141876988dbe2a037f58b393b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 24 Oct 2023 09:25:12 -0300 Subject: yaml: remove cruft, support blank list items --- src/data/yaml.js | 58 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index f49f48dd..bf63f05d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -213,13 +213,36 @@ function makeProcessDocument( const fieldValues = {}; - for (const [field, value] of documentEntries) { + for (const [field, documentValue] of documentEntries) { if (skippedFields.has(field)) continue; - if (Object.hasOwn(fieldTransformations, field)) { - fieldValues[field] = fieldTransformations[field](value); - } else { - fieldValues[field] = value; + + // This variable would like to certify itself as "not into capitalism". + let propertyValue = + (Object.hasOwn(fieldTransformations, field) + ? fieldTransformations[field](documentValue) + : documentValue); + + // Completely blank items in a YAML list are read as null. + // They're handy to have around when filling out a document and shouldn't + // be considered an error (or data at all). + if (Array.isArray(propertyValue)) { + const wasEmpty = empty(propertyValue); + + propertyValue = + propertyValue.filter(item => item !== null); + + const isEmpty = empty(propertyValue); + + // Don't set arrays which are empty as a result of the above filter. + // Arrays which were originally empty, i.e. `Field: []`, are still + // valid data, but if it's just an array not containing any filled out + // items, it should be treated as a placeholder and skipped over. + if (isEmpty && !wasEmpty) { + propertyValue = null; + } } + + fieldValues[field] = propertyValue; } const sourceProperties = {}; @@ -233,7 +256,6 @@ function makeProcessDocument( const fieldValueErrors = []; - // This for loop would like to certify itself as "not into capitalism". for (const [property, value] of Object.entries(sourceProperties)) { const field = propertyFieldMapping[property]; try { @@ -700,33 +722,17 @@ export function parseContributors(contributors) { return contributors; } - if (contributors.length === 1 && contributors[0].startsWith('<i>')) { - const arr = []; - arr.textContent = contributors[0]; - return arr; - } - contributors = contributors.map((contrib) => { - // 8asically, the format is "Who (What)", or just "Who". 8e sure to - // keep in mind that "what" doesn't necessarily have a value! + if (typeof contrib !== 'string') return contrib; + const match = contrib.match(/^(.*?)( \((.*)\))?$/); - if (!match) { - return contrib; - } + if (!match) return contrib; + const who = match[1]; const what = match[3] || null; return {who, what}; }); - const badContributor = contributors.find((val) => typeof val === 'string'); - if (badContributor) { - throw new Error(`Incorrectly formatted contribution: "${badContributor}".`); - } - - if (contributors.length === 1 && contributors[0].who === 'none') { - return null; - } - return contributors; } -- cgit 1.3.0-6-gf8a5 From f461941ac3d39b307ac32e21f9ff41b47fba638b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 24 Oct 2023 10:47:40 -0300 Subject: data, yaml: new flash act field List Termonology / listTerminology --- src/data/things/flash.js | 1 + src/data/yaml.js | 1 + 2 files changed, 2 insertions(+) (limited to 'src') diff --git a/src/data/things/flash.js b/src/data/things/flash.js index e3ef9f5c..511ff19c 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -127,6 +127,7 @@ export class FlashAct extends Thing { name: name('Unnamed Flash Act'), directory: directory(), color: color(), + listTerminology: simpleString(), jump: simpleString(), diff --git a/src/data/yaml.js b/src/data/yaml.js index bf63f05d..f12c4c31 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -560,6 +560,7 @@ export const processFlashActDocument = makeProcessDocument(T.FlashAct, { directory: 'Directory', color: 'Color', + listTerminology: 'List Terminology', jump: 'Jump', jumpColor: 'Jump Color', -- cgit 1.3.0-6-gf8a5 From f64118417ea45178cfa50c181c9c102e5c8ff1b5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 24 Oct 2023 10:48:10 -0300 Subject: content: generateFlashActSidebar: use flash act listTermonology --- .../dependencies/generateFlashActSidebar.js | 31 ++++++---------------- src/strings-default.json | 4 --- 2 files changed, 8 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js index ff5dc049..f8ad33db 100644 --- a/src/content/dependencies/generateFlashActSidebar.js +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -75,27 +75,9 @@ export default { const currentActIndex = currentSideActs.indexOf(act); - const visualNovelActs = [ - findFlashAct('flash-act:friendsim'), - findFlashAct('flash-act:pesterquest'), - findFlashAct('flash-act:psycholonials'), - ]; - - const gameSeriesActs = [ - findFlashAct('flash-act:hiveswap'), - ]; - - const listTerminology = - (visualNovelActs.includes(act) - ? 'volumesInThisGame' - : gameSeriesActs.includes(act) - ? 'gamesInThisSeries' - : act === findFlashAct('flash-act:other-fan-adventures') - ? 'flashesInThisSection' - : currentSideIndex <= 1 + const fallbackListTerminology = + (currentSideIndex <= 1 ? 'flashesInThisAct' - : currentSideIndex === 3 - ? 'flashesInThisStory' : 'entriesInThisSection'); return { @@ -109,7 +91,7 @@ export default { currentActFlashes, currentFlashIndex, - listTerminology, + fallbackListTerminology, }; }, @@ -140,7 +122,8 @@ export default { currentActIndex: query.currentActIndex, currentFlashIndex: query.currentFlashIndex, - listTerminology: query.listTerminology, + customListTerminology: act.listTerminology, + fallbackListTerminology: query.fallbackListTerminology, }), generate(data, relations, {getColors, html, language}) { @@ -154,7 +137,9 @@ export default { [ html.tag('summary', html.tag('span', {class: 'group-name'}, - language.$('flashSidebar.flashList', data.listTerminology))), + (data.customListTerminology + ? language.sanitize(data.customListTerminology) + : language.$('flashSidebar.flashList', data.fallbackListTerminology)))), html.tag('ul', relations.currentActFlashLinks diff --git a/src/strings-default.json b/src/strings-default.json index 7a7fa041..b6471bdf 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -324,10 +324,6 @@ "flashPage.title": "{FLASH}", "flashPage.nav.flash": "{FLASH}", "flashSidebar.flashList.flashesInThisAct": "Flashes in this act", - "flashSidebar.flashList.flashesInThisStory": "Flashes in this story", - "flashSidebar.flashList.flashesInThisSection": "Flashes in this section", - "flashSidebar.flashList.volumesInThisGame": "Volumes in this game", - "flashSidebar.flashList.gamesInThisSeries": "Games in this series", "flashSidebar.flashList.entriesInThisSection": "Entries in this section", "groupSidebar.title": "Groups", "groupSidebar.groupList.category": "{CATEGORY}", -- cgit 1.3.0-6-gf8a5 From 6c0c1525a0e924896a2a593fc05633e442a80413 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 24 Oct 2023 10:49:05 -0300 Subject: yaml: check artists for duplicate directories --- src/data/yaml.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index f12c4c31..16303a64 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1515,6 +1515,7 @@ export function filterDuplicateDirectories(wikiData) { const deduplicateSpec = [ 'albumData', 'artTagData', + 'artistData', 'flashData', 'flashActData', 'groupData', -- cgit 1.3.0-6-gf8a5 From 0df06e2f5d7bb3aeaccfd6b2187eec6e8f40755f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 24 Oct 2023 11:33:12 -0300 Subject: content: generateFlashActSidebar: rearrange some sides --- src/content/dependencies/generateFlashActSidebar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js index f8ad33db..bd6063c9 100644 --- a/src/content/dependencies/generateFlashActSidebar.js +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -18,7 +18,7 @@ export default { findFlashAct('flash-act:a6a1'), findFlashAct('flash-act:hiveswap'), findFlashAct('flash-act:cool-and-new-web-comic'), - findFlashAct('flash-act:psycholonials'), + findFlashAct('flash-act:sunday-night-strifin'), ]; const sideNames = [ @@ -26,7 +26,7 @@ export default { `Side 2 (Acts 6-7)`, `Additional Canon`, `Fan Adventures`, - `More Flashes & Games`, + `Fan Games & More`, ]; const sideColors = [ -- cgit 1.3.0-6-gf8a5 From a80dcfd176c41cef1995f5349a1464d4746badbd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Tue, 24 Oct 2023 11:33:25 -0300 Subject: data, yaml: new flash Color / color field --- src/data/things/flash.js | 28 +++++++++++++++++----------- src/data/yaml.js | 1 + 2 files changed, 18 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 511ff19c..71174931 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,6 +9,10 @@ import { oneOf, } from '#validators'; +import { + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + import { color, contributionList, @@ -58,6 +62,19 @@ export class Flash extends Thing { }, }, + color: [ + exposeUpdateValueOrContinue({ + validate: input.value(isColor), + }), + + { + flags: {expose: true}, + dependencies: ['this', 'flashActData'], + compute: ({this: flash, flashActData}) => + flashActData.find((act) => act.flashes.includes(flash))?.color ?? null, + }, + ], + date: simpleDate(), coverArtFileExtension: fileExtension('jpg'), @@ -90,17 +107,6 @@ export class Flash extends Thing { flashActData.find((act) => act.flashes.includes(flash)) ?? null, }, }, - - color: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'flashActData'], - - compute: ({this: flash, flashActData}) => - flashActData.find((act) => act.flashes.includes(flash))?.color ?? null, - }, - }, }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/yaml.js b/src/data/yaml.js index 16303a64..f7856cb7 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -544,6 +544,7 @@ export const processFlashDocument = makeProcessDocument(T.Flash, { name: 'Flash', directory: 'Directory', page: 'Page', + color: 'Color', urls: 'URLs', date: 'Date', -- cgit 1.3.0-6-gf8a5 From c7e21005beb8807216aac6ed3ae54029575007a1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 26 Oct 2023 18:03:24 -0300 Subject: data: Track.withAlbum: bulkily match documented early exit behavior --- src/data/composite/things/track/withAlbum.js | 45 +++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 34845ab0..0e85cee9 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -4,10 +4,9 @@ // exit instead. import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; import {is} from '#validators'; -import {raiseOutputWithoutDependency} from '#composite/control-flow'; - export default templateCompositeFrom({ annotation: `withAlbum`, @@ -21,13 +20,20 @@ export default templateCompositeFrom({ outputs: ['#album'], steps: () => [ - raiseOutputWithoutDependency({ - dependency: 'albumData', - mode: input.value('empty'), - output: input.value({ - ['#album']: null, - }), - }), + { + dependencies: [input('notFoundMode'), 'albumData'], + compute: (continuation, { + [input('notFoundMode')]: notFoundMode, + ['albumData']: albumData, + }) => + (albumData === null + ? continuation.exit(null) + : empty(albumData) + ? (notFoundMode === 'exit' + ? continuation.exit(null) + : continuation.raiseOutput({'#album': null})) + : continuation()), + }, { dependencies: [input.myself(), 'albumData'], @@ -37,21 +43,20 @@ export default templateCompositeFrom({ }) => continuation({ ['#album']: - albumData.find(album => album.tracks.includes(track)), + albumData.find(album => album.tracks.includes(track)) + ?? null, }), }, - raiseOutputWithoutDependency({ - dependency: '#album', - output: input.value({ - ['#album']: null, - }), - }), - { - dependencies: ['#album'], - compute: (continuation, {'#album': album}) => - continuation.raiseOutput({'#album': album}), + dependencies: [input('notFoundMode'), '#album'], + compute: (continuation, { + [input('notFoundMode')]: notFoundMode, + ['#album']: album, + }) => + ((album === null && notFoundMode === 'exit') + ? continuation.exit(null) + : continuation.raiseOutput({'#album': album})), }, ], }); -- cgit 1.3.0-6-gf8a5 From ecac8276182436897dffff02aaf1a7d268738cba Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 26 Oct 2023 18:47:08 -0300 Subject: data: update withPropertyFromList --- src/data/composite/data/withPropertyFromList.js | 100 +++++++++++++++--------- 1 file changed, 63 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js index 3ce05fdf..1983ebbc 100644 --- a/src/data/composite/data/withPropertyFromList.js +++ b/src/data/composite/data/withPropertyFromList.js @@ -16,41 +16,67 @@ // - withUnflattenedList // -import {empty} from '#sugar'; - -// todo: OUHHH THIS ONE'S NOT UPDATED YET LOL -export default function({ - list, - property, - into = null, -}) { - into ??= - (list.startsWith('#') - ? `${list}.${property}` - : `#${list}.${property}`); - - return { - annotation: `withPropertyFromList`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {property}, - - compute(continuation, {list, '#options': {property}}) { - if (list === undefined || empty(list)) { - return continuation({into: []}); - } - - return continuation({ - into: - list.map(item => - (item === null || item === undefined - ? null - : item[property] ?? null)), - }); - }, - }, - }; +import {input, templateCompositeFrom} from '#composite'; + +function getOutputName({list, property, prefix}) { + if (!property) return `#values`; + if (prefix) return `${prefix}.${property}`; + if (list) return `${list}.${property}`; + return `#list.${property}`; } + +export default templateCompositeFrom({ + annotation: `withPropertyFromList`, + + inputs: { + list: input({type: 'array'}), + property: input({type: 'string'}), + prefix: input.staticValue({type: 'string', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + [input.staticValue('property')]: property, + [input.staticValue('prefix')]: prefix, + }) => + [getOutputName({list, property, prefix})], + + steps: () => [ + { + dependencies: [input('list'), input('property')], + compute: (continuation, { + [input('list')]: list, + [input('property')]: property, + }) => continuation({ + ['#values']: + list.map(item => item[property] ?? null), + }), + }, + + { + dependencies: [ + input.staticDependency('list'), + input.staticValue('property'), + input.staticValue('prefix'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: list, + [input.staticValue('property')]: property, + [input.staticValue('prefix')]: prefix, + }) => continuation({ + ['#outputName']: + getOutputName({list, property, prefix}), + }), + }, + + { + dependencies: ['#values', '#outputName'], + compute: (continuation, { + ['#values']: values, + ['#outputName']: outputName, + }) => + continuation.raiseOutput({[outputName]: values}), + }, + ], +}); -- cgit 1.3.0-6-gf8a5 From 5f740046562c85ab4e00063037ddf3af5a545279 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 26 Oct 2023 19:05:39 -0300 Subject: data, test: withResultOfAvailabilityCheck: index mode --- src/data/composite/control-flow/inputAvailabilityCheckMode.js | 2 +- src/data/composite/control-flow/withResultOfAvailabilityCheck.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/composite/control-flow/inputAvailabilityCheckMode.js b/src/data/composite/control-flow/inputAvailabilityCheckMode.js index d74a1149..8008fdeb 100644 --- a/src/data/composite/control-flow/inputAvailabilityCheckMode.js +++ b/src/data/composite/control-flow/inputAvailabilityCheckMode.js @@ -3,7 +3,7 @@ import {is} from '#validators'; export default function inputAvailabilityCheckMode() { return input({ - validate: is('null', 'empty', 'falsy'), + validate: is('null', 'empty', 'falsy', 'index'), defaultValue: 'null', }); } diff --git a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js index bcbd0b37..a6942014 100644 --- a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -10,6 +10,7 @@ // * 'falsy': Check that the value isn't false when treated as a boolean // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! +// * 'index': Check that the value is a number, and is at least zero. // // See also: // - exitWithoutDependency @@ -57,6 +58,10 @@ export default templateCompositeFrom({ case 'falsy': availability = !!value && (!Array.isArray(value) || !empty(value)); break; + + case 'index': + availability = typeof value === 'number' && value >= 0; + break; } return continuation({'#availability': availability}); -- cgit 1.3.0-6-gf8a5 From bb646f28853399a43d52a056c86d04f6a4343932 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 26 Oct 2023 19:09:22 -0300 Subject: data: Track.withAlbum: refactor for clarity Utilizes availability checks instead of manual null comparisons and empty() calls, extracts track lists using withPropertyFromList, operates on index instead of unique album object where possible (including found / not found check). --- src/data/composite/things/track/withAlbum.js | 98 ++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 0e85cee9..9c974cd1 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -4,9 +4,12 @@ // exit instead. import {input, templateCompositeFrom} from '#composite'; -import {empty} from '#sugar'; import {is} from '#validators'; +import {exitWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; + export default templateCompositeFrom({ annotation: `withAlbum`, @@ -20,43 +23,84 @@ export default templateCompositeFrom({ outputs: ['#album'], steps: () => [ + // null albumData is always an early exit. + + exitWithoutDependency({ + dependency: 'albumData', + mode: input.value('null'), + }), + + // empty albumData conditionally exits early or outputs null. + + withResultOfAvailabilityCheck({ + from: 'albumData', + mode: input.value('empty'), + }).outputs({ + '#availability': '#albumDataAvailability', + }), + { - dependencies: [input('notFoundMode'), 'albumData'], - compute: (continuation, { + dependencies: [input('notFoundMode'), '#albumDataAvailability'], + compute(continuation, { [input('notFoundMode')]: notFoundMode, - ['albumData']: albumData, - }) => - (albumData === null - ? continuation.exit(null) - : empty(albumData) - ? (notFoundMode === 'exit' - ? continuation.exit(null) - : continuation.raiseOutput({'#album': null})) - : continuation()), + ['#albumDataAvailability']: albumDataIsAvailable, + }) { + if (albumDataIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#album': null}); + } + }, }, + withPropertyFromList({ + list: 'albumData', + property: input.value('tracks'), + }), + { - dependencies: [input.myself(), 'albumData'], + dependencies: [input.myself(), '#albumData.tracks'], compute: (continuation, { [input.myself()]: track, - ['albumData']: albumData, - }) => - continuation({ - ['#album']: - albumData.find(album => album.tracks.includes(track)) - ?? null, - }), + ['#albumData.tracks']: trackLists, + }) => continuation({ + ['#albumIndex']: + trackLists.findIndex(tracks => tracks.includes(track)), + }), }, + // album not found conditionally exits or outputs null. + + withResultOfAvailabilityCheck({ + from: '#albumIndex', + mode: input.value('index'), + }).outputs({ + '#availability': '#albumAvailability', + }), + { - dependencies: [input('notFoundMode'), '#album'], - compute: (continuation, { + dependencies: [input('notFoundMode'), '#albumAvailability'], + compute(continuation, { [input('notFoundMode')]: notFoundMode, - ['#album']: album, - }) => - ((album === null && notFoundMode === 'exit') - ? continuation.exit(null) - : continuation.raiseOutput({'#album': album})), + ['#albumAvailability']: albumIsAvailable, + }) { + if (albumIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#album': null}); + } + }, + }, + + { + dependencies: ['albumData', '#albumIndex'], + compute: (continuation, { + ['albumData']: albumData, + ['#albumIndex']: albumIndex, + }) => continuation.raiseOutput({ + ['#album']: + albumData[albumIndex], + }), }, ], }); -- cgit 1.3.0-6-gf8a5 From 855dcdbc17831809cfb3c800d378c62186702740 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 26 Oct 2023 19:30:40 -0300 Subject: data: Flash.withFlashAct --- src/data/composite/things/flash/index.js | 1 + src/data/composite/things/flash/withFlashAct.js | 108 ++++++++++++++++++++++++ src/data/composite/things/track/withAlbum.js | 2 + 3 files changed, 111 insertions(+) create mode 100644 src/data/composite/things/flash/index.js create mode 100644 src/data/composite/things/flash/withFlashAct.js (limited to 'src') diff --git a/src/data/composite/things/flash/index.js b/src/data/composite/things/flash/index.js new file mode 100644 index 00000000..63ac13da --- /dev/null +++ b/src/data/composite/things/flash/index.js @@ -0,0 +1 @@ +export {default as withFlashAct} from './withFlashAct.js'; diff --git a/src/data/composite/things/flash/withFlashAct.js b/src/data/composite/things/flash/withFlashAct.js new file mode 100644 index 00000000..ada2dcfe --- /dev/null +++ b/src/data/composite/things/flash/withFlashAct.js @@ -0,0 +1,108 @@ +// Gets the flash's act. This will early exit if flashActData is missing. +// By default, if there's no flash whose list of flashes includes this flash, +// the output dependency will be null; set {notFoundMode: 'exit'} to early +// exit instead. +// +// This step models with Flash.withAlbum. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {exitWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withFlashAct`, + + inputs: { + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#flashAct'], + + steps: () => [ + // null flashActData is always an early exit. + + exitWithoutDependency({ + dependency: 'flashActData', + mode: input.value('null'), + }), + + // empty flashActData conditionally exits early or outputs null. + + withResultOfAvailabilityCheck({ + from: 'flashActData', + mode: input.value('empty'), + }).outputs({ + '#availability': '#flashActDataAvailability', + }), + + { + dependencies: [input('notFoundMode'), '#flashActDataAvailability'], + compute(continuation, { + [input('notFoundMode')]: notFoundMode, + ['#flashActDataAvailability']: flashActDataIsAvailable, + }) { + if (flashActDataIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#flashAct': null}); + } + }, + }, + + withPropertyFromList({ + list: 'flashActData', + property: input.value('flashes'), + }), + + { + dependencies: [input.myself(), '#flashActData.flashes'], + compute: (continuation, { + [input.myself()]: track, + ['#flashActData.flashes']: flashLists, + }) => continuation({ + ['#flashActIndex']: + flashLists.findIndex(flashes => flashes.includes(track)), + }), + }, + + // album not found conditionally exits or outputs null. + + withResultOfAvailabilityCheck({ + from: '#flashActIndex', + mode: input.value('index'), + }).outputs({ + '#availability': '#flashActAvailability', + }), + + { + dependencies: [input('notFoundMode'), '#flashActAvailability'], + compute(continuation, { + [input('notFoundMode')]: notFoundMode, + ['#flashActAvailability']: flashActIsAvailable, + }) { + if (flashActIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#flashAct': null}); + } + }, + }, + + { + dependencies: ['flashActData', '#flashActIndex'], + compute: (continuation, { + ['flashActData']: flashActData, + ['#flashActIndex']: flashActIndex, + }) => continuation.raiseOutput({ + ['#flashAct']: + flashActData[flashActIndex], + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 9c974cd1..cbd16dcd 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -2,6 +2,8 @@ // By default, if there's no album whose list of tracks includes this track, // the output dependency will be null; set {notFoundMode: 'exit'} to early // exit instead. +// +// This step models with Flash.withFlashAct. import {input, templateCompositeFrom} from '#composite'; import {is} from '#validators'; -- cgit 1.3.0-6-gf8a5 From 991386372f418b165a192a3ad36fb9b6cecc4d76 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Thu, 26 Oct 2023 19:32:14 -0300 Subject: data: Flash.color: replace erroneous syntax w/ composite definition --- src/data/things/flash.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 71174931..e2afcef4 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,9 +9,9 @@ import { oneOf, } from '#validators'; -import { - exposeUpdateValueOrContinue, -} from '#composite/control-flow'; +import {exposeDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; import { color, @@ -26,6 +26,8 @@ import { wikiData, } from '#composite/wiki-properties'; +import {withFlashAct} from '#composite/things/flash'; + import Thing from './thing.js'; export class Flash extends Thing { @@ -67,12 +69,14 @@ export class Flash extends Thing { validate: input.value(isColor), }), - { - flags: {expose: true}, - dependencies: ['this', 'flashActData'], - compute: ({this: flash, flashActData}) => - flashActData.find((act) => act.flashes.includes(flash))?.color ?? null, - }, + withFlashAct(), + + withPropertyFromObject({ + object: '#flashAct', + property: input.value('color'), + }), + + exposeDependency({dependency: '#flashAct.color'}), ], date: simpleDate(), -- cgit 1.3.0-6-gf8a5 From 940b2cbf8b68eb0b446cca0feeb507840c486394 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" <qznebula@protonmail.com> Date: Fri, 27 Oct 2023 18:50:32 -0300 Subject: upd8: cleanup & new --show-step-summary CLI option --- src/file-size-preloader.js | 3 + src/upd8.js | 540 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 437 insertions(+), 106 deletions(-) (limited to 'src') diff --git a/src/file-size-preloader.js b/src/file-size-preloader.js index 38e60e67..4eadde7b 100644 --- a/src/file-size-preloader.js +++ b/src/file-size-preloader.js @@ -29,6 +29,8 @@ export default class FileSizePreloader { #loadingPromise = null; #resolveLoadingPromise = null; + hadErrored = false; + loadPaths(...paths) { this.#paths.push(...paths.filter((p) => !this.#paths.includes(p))); return this.#startLoadingPaths(); @@ -67,6 +69,7 @@ export default class FileSizePreloader { // Oops! Discard that path, and don't increment the index before // moving on, since the next path will now be in its place. this.#paths.splice(this.#loadedPathIndex + 1, 1); + this.hasErrored = true; logWarn`Failed to process file size for ${path}: ${error.message}`; return this.#loadNextPath(); } diff --git a/src/upd8.js b/src/upd8.js index 9cd2c509..27445a8e 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -50,6 +50,7 @@ import {sortByName} from '#wiki-data'; import { colors, decorateTime, + fileIssue, logWarn, logInfo, logError, @@ -94,9 +95,64 @@ const BUILD_TIME = new Date(); const DEFAULT_STRINGS_FILE = 'strings-default.json'; +const STATUS_NOT_STARTED = `not started`; +const STATUS_NOT_APPLICABLE = `not applicable`; +const STATUS_STARTED_NOT_DONE = `started but not yet done`; +const STATUS_DONE_CLEAN = `done without warnings`; +const STATUS_FATAL_ERROR = `fatal error`; +const STATUS_HAS_WARNINGS = `has warnings`; + +const defaultStepStatus = {status: STATUS_NOT_STARTED, annotation: null}; + +// Defined globally for quick access outside the main() function's contents. +// This will be initialized and mutated over the course of main(). +let stepStatusSummary; +let showStepStatusSummary = false; + async function main() { Error.stackTraceLimit = Infinity; + stepStatusSummary = { + loadThumbnailCache: + {...defaultStepStatus, name: `load thumbnail cache file`}, + + generateThumbnails: + {...defaultStepStatus, name: `generate thumbnails`}, + + loadDataFiles: + {...defaultStepStatus, name: `load and process data files`}, + + linkWikiDataArrays: + {...defaultStepStatus, name: `link wiki data arrays`}, + + filterDuplicateDirectories: + {...defaultStepStatus, name: `filter duplicate directories`}, + + filterReferenceErrors: + {...defaultStepStatus, name: `filter reference errors`}, + + sortWikiDataArrays: + {...defaultStepStatus, name: `sort wiki data arrays`}, + + precacheData: + {...defaultStepStatus, name: `precache data`}, + + loadInternalDefaultLanguage: + {...defaultStepStatus, name: `load internal default language`}, + + loadLanguageFiles: + {...defaultStepStatus, name: `load custom language files`}, + + initializeDefaultLanguage: + {...defaultStepStatus, name: `initialize default language`}, + + preloadFileSizes: + {...defaultStepStatus, name: `preload file sizes`}, + + performBuild: + {...defaultStepStatus, name: `perform selected build mode`}, + }; + const defaultQueueSize = 500; const buildModeFlagOptions = ( @@ -121,7 +177,7 @@ async function main() { } else if (selectedBuildModeFlags.length > 1) { logError`Building multiple modes (${selectedBuildModeFlags.join(', ')}) at once not supported.`; logError`Please specify a maximum of one build mode.`; - return; + return false; } else { selectedBuildModeFlag = selectedBuildModeFlags[0]; usingDefaultBuildMode = false; @@ -221,6 +277,11 @@ async function main() { type: 'flag', }, + 'show-step-summary': { + help: `Show a summary of all the top-level build steps once hsmusic exits. This is mostly useful for progammer debugging!`, + 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', @@ -361,7 +422,7 @@ async function main() { })`, buildOptions); } - return; + return true; } const dataPath = cliOptions['data-path'] || process.env.HSMUSIC_DATA; @@ -373,6 +434,8 @@ async function main() { const clearThumbsFlag = cliOptions['clear-thumbs'] ?? false; const noBuild = cliOptions['no-build'] ?? false; + showStepStatusSummary = cliOptions['show-step-summary'] ?? false; + const replFlag = cliOptions['repl'] ?? false; const disableReplHistory = cliOptions['no-repl-history'] ?? false; @@ -388,19 +451,16 @@ async function main() { const magickThreads = +(cliOptions['magick-threads'] ?? defaultMagickThreads); - { - let errored = false; - const error = (cond, msg) => { - if (cond) { - console.error(`\x1b[31;1m${msg}\x1b[0m`); - errored = true; - } - }; - error(!dataPath, `Expected --data-path option or HSMUSIC_DATA to be set`); - error(!mediaPath, `Expected --media-path option or HSMUSIC_MEDIA to be set`); - if (errored) { - return; - } + if (!dataPath) { + logError`${`Expected --data-path option or HSMUSIC_DATA to be set`}`; + } + + if (!mediaPath) { + logError`${`Expected --media-path option or HSMUSIC_MEDIA to be set`}`; + } + + if (!dataPath || !mediaPath) { + return false; } if (replFlag) { @@ -423,7 +483,7 @@ async function main() { if (skipThumbs && thumbsOnly) { logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`; - return; + return false; } if (clearThumbsFlag) { @@ -434,23 +494,38 @@ async function main() { logInfo`And don't forget to remove ${'--skip-thumbs'} too, eh?`; } } - return; + return true; } let thumbsCache; if (skipThumbs) { + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_NOT_APPLICABLE, + annotation: `provided --skip-thumbs`, + }); + + stepStatusSummary.loadThumbnailCache.status = STATUS_STARTED_NOT_DONE; + const thumbsCachePath = path.join(mediaPath, thumbsCacheFile); + try { thumbsCache = JSON.parse(await readFile(thumbsCachePath)); logInfo`Thumbnail cache file successfully read.`; + stepStatusSummary.loadThumbnailCache.status = STATUS_DONE_CLEAN; } catch (error) { if (error.code === 'ENOENT') { logError`The thumbnail cache doesn't exist, and it's necessary to build` logError`the website. Please run once without ${'--skip-thumbs'} - after` logError`that you'll be good to go and don't need to process thumbnails` logError`again!`; - return; + + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_FATAL_ERROR, + annotation: `cache does not exist`, + }); + + return false; } else { logError`Malformed or unreadable thumbnail cache file: ${error}`; logError`Path: ${thumbsCachePath}`; @@ -460,21 +535,50 @@ async function main() { logError`you're welcome to message in the HSMusic Discord and we'll try`; logError`to help you out with troubleshooting!`; logError`${'https://hsmusic.wiki/discord/'}`; - return; + + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_FATAL_ERROR, + annotation: `cache malformed or unreadable`, + }); + + return false; } } logInfo`Skipping thumbnail generation.`; } else { + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_NOT_APPLICABLE, + annotation: `using cache from thumbnail generation`, + }); + + stepStatusSummary.generateThumbnails.status = STATUS_STARTED_NOT_DONE; + logInfo`Begin thumbnail generation... -----+`; + const result = await genThumbs(mediaPath, { queueSize, magickThreads, quiet: !thumbsOnly, }); + logInfo`Done thumbnail generation! --------+`; - if (!result.success) return; - if (thumbsOnly) return; + + if (!result.success) { + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_FATAL_ERROR, + annotation: `view log for details`, + }); + + return false; + } + + stepStatusSummary.generateThumbnails.status = STATUS_DONE_CLEAN; + + if (thumbsOnly) { + return true; + } + thumbsCache = result.cache; } @@ -490,8 +594,26 @@ async function main() { CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true; } - const {aggregate: processDataAggregate, result: wikiDataResult} = - await loadAndProcessDataDocuments({dataPath}); + stepStatusSummary.loadDataFiles.status = STATUS_STARTED_NOT_DONE; + + let processDataAggregate, wikiDataResult; + + try { + ({aggregate: processDataAggregate, result: wikiDataResult} = + await loadAndProcessDataDocuments({dataPath})); + } catch (error) { + console.error(error); + + logError`There was a JavaScript error loading data files.`; + fileIssue(); + + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_FATAL_ERROR, + annotation: `javascript error - view log for details`, + }); + + return false; + } Object.assign(wikiData, wikiDataResult); @@ -536,84 +658,105 @@ async function main() { logWarn`still build - but all errored data will be skipped.`; logWarn`(Resolve errors for more complete output!)`; errorless = false; - } - if (errorless) { - logInfo`All data processed without any errors - nice!`; - logInfo`(This means all source files will be fully accounted for during page generation.)`; + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_HAS_WARNINGS, + annotation: `view log for details`, + }); } - } - if (!wikiData.wikiInfo) { - logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`; - return; - } + if (!wikiData.wikiInfo) { + logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`; - let duplicateDirectoriesErrored = false; + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_FATAL_ERROR, + annotation: `wiki info object not available`, + }); - function filterAndShowDuplicateDirectories() { - const aggregate = filterDuplicateDirectories(wikiData); - let errorless = true; - try { - aggregate.close(); - } catch (aggregate) { - niceShowAggregate(aggregate); - logWarn`The above duplicate directories were detected while reviewing data files.`; - logWarn`Each thing listed above will been totally excempt from this build of the site!`; - logWarn`Specify unique 'Directory' fields in data entries to resolve these.`; - logWarn`${`Note:`} This will probably result in reference errors below.`; - logWarn`${`. . .`} You should fix duplicate directories first!`; - logWarn`(Resolve errors for more complete output!)`; - duplicateDirectoriesErrored = true; - errorless = false; - } - if (errorless) { - logInfo`No duplicate directories found - nice!`; + return false; } - } - function filterAndShowReferenceErrors() { - const aggregate = filterReferenceErrors(wikiData); - let errorless = true; - try { - aggregate.close(); - } catch (error) { - niceShowAggregate(error); - logWarn`The above errors were detected while validating references in data files.`; - logWarn`If the remaining valid data is complete enough, the wiki will still build -`; - logWarn`but all errored references will be skipped.`; - if (duplicateDirectoriesErrored) { - logWarn`${`Note:`} Duplicate directories were found as well. Review those first,`; - logWarn`${`. . .`} as they may have caused some of the errors detected above.`; - } - logWarn`(Resolve errors for more complete output!)`; - errorless = false; - } if (errorless) { - logInfo`All references validated without any errors - nice!`; - logInfo`(This means all references between things, such as leitmotif references`; - logInfo` and artist credits, will be fully accounted for during page generation.)`; + logInfo`All data files processed without any errors - nice!`; + stepStatusSummary.loadDataFiles.status = STATUS_DONE_CLEAN; } } // Link data arrays so that all essential references between objects are // complete, so properties (like dates!) are inherited where that's // appropriate. + + stepStatusSummary.linkWikiDataArrays.status = STATUS_STARTED_NOT_DONE; + linkWikiDataArrays(wikiData); + stepStatusSummary.linkWikiDataArrays.status = STATUS_DONE_CLEAN; + // Filter out any things with duplicate directories throughout the data, // warning about them too. - filterAndShowDuplicateDirectories(); + + stepStatusSummary.filterDuplicateDirectories.status = STATUS_STARTED_NOT_DONE; + + const filterDuplicateDirectoriesAggregate = + filterDuplicateDirectories(wikiData); + + try { + filterDuplicateDirectoriesAggregate.close(); + logInfo`No duplicate directories found - nice!`; + stepStatusSummary.filterDuplicateDirectories.status = STATUS_DONE_CLEAN; + } catch (aggregate) { + niceShowAggregate(aggregate); + + logWarn`The above duplicate directories were detected while reviewing data files.`; + logWarn`Since it's impossible to automatically determine which one's directory is`; + logWarn`correct, the build can't continue. Specify unique 'Directory' fields in`; + logWarn`some or all of these data entries to resolve the errors.`; + + Object.assign(stepStatusSummary.filterDuplicateDirectories, { + status: STATUS_FATAL_ERROR, + annotation: `duplicate directories found`, + }); + + return false; + } // Filter out any reference errors throughout the data, warning about them // too. - filterAndShowReferenceErrors(); + + stepStatusSummary.filterReferenceErrors.status = STATUS_STARTED_NOT_DONE; + + const filterReferenceErrorsAggregate = filterReferenceErrors(wikiData); + + try { + filterReferenceErrorsAggregate.close(); + logInfo`All references validated without any errors - nice!`; + stepStatusSummary.filterReferenceErrors.status = STATUS_DONE_CLEAN; + } catch (error) { + niceShowAggregate(error); + + logWarn`The above errors were detected while validating references in data files.`; + logWarn`The wiki will still build, but these connections between data objects`; + logWarn`will be completely skipped. Resolve the errors for more complete output.`; + + Object.assign(stepStatusSummary.filterReferenceErrors, { + status: STATUS_HAS_WARNINGS, + annotation: `view log for details`, + }); + } // Sort data arrays so that they're all in order! This may use properties // which are only available after the initial linking. + + stepStatusSummary.sortWikiDataArrays.status = STATUS_STARTED_NOT_DONE; + sortWikiDataArrays(wikiData); + stepStatusSummary.sortWikiDataArrays.status = STATUS_DONE_CLEAN; + if (precacheData) { + stepStatusSummary.precacheData.status = STATUS_STARTED_NOT_DONE; + + // TODO: Aggregate errors here, instead of just throwing. progressCallAll('Caching all data values', Object.entries(wikiData) .filter(([key]) => key !== 'listingSpec' && @@ -624,32 +767,96 @@ async function main() { [key, value]) .flatMap(([_key, things]) => things) .map(thing => () => CacheableObject.cacheAllExposedProperties(thing))); + + stepStatusSummary.precacheData.status = STATUS_DONE_CLEAN; + } else { + Object.assign(stepStatusSummary.precacheData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-data not provided`, + }); } if (noBuild) { + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_NOT_APPLICABLE, + annotation: `--no-build provided`, + }); + displayCompositeCacheAnalysis(); - if (precacheData) return; + + if (precacheData) { + return true; + } } - const internalDefaultLanguage = await processLanguageFile( - path.join(__dirname, DEFAULT_STRINGS_FILE)); + let internalDefaultLanguage; + + try { + internalDefaultLanguage = + await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); + + stepStatusSummary.loadInternalDefaultLanguage.status = STATUS_DONE_CLEAN; + } catch (error) { + console.error(error); + + logError`There was an error reading the internal language file.`; + fileIssue(); + + Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { + status: STATUS_FATAL_ERROR, + annotation: `see log for details`, + }); + + return false; + } let languages; + if (langPath) { + stepStatusSummary.loadLanguageFiles.status = STATUS_STARTED_NOT_DONE; + const languageDataFiles = await traverse(langPath, { filterFile: name => path.extname(name) === '.json', pathStyle: 'device', }); - const results = await progressPromiseAll(`Reading & processing language files.`, - languageDataFiles.map((file) => processLanguageFile(file))); + let results; + + // TODO: Aggregate errors (with Promise.allSettled). + try { + results = + await progressPromiseAll(`Reading & processing language files.`, + languageDataFiles.map((file) => processLanguageFile(file))); + } catch (error) { + console.error(error); + + logError`Failed to load language files. Please investigate these, or don't provide`; + logError`--lang-path (or HSMUSIC_LANG) and build again.`; - languages = Object.fromEntries( - results.map((language) => [language.code, language])); + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_FATAL_ERROR, + annotation: `see log for details`, + }); + + return false; + } + + languages = + Object.fromEntries( + results.map((language) => [language.code, language])); + + stepStatusSummary.loadLanguageFiles.status = STATUS_DONE_CLEAN; } else { languages = {}; + + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_NOT_APPLICABLE, + annotation: `--lang-path and HSMUSIC_LANG not provided`, + }); } + stepStatusSummary.initializeDefaultLanguage.status = STATUS_STARTED_NOT_DONE; + const customDefaultLanguage = languages[wikiData.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code]; let finalDefaultLanguage; @@ -658,17 +865,34 @@ async function main() { logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; finalDefaultLanguage = customDefaultLanguage; + + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_DONE_CLEAN, + annotation: `using wiki-specified custom default language`, + }); } else if (wikiData.wikiInfo.defaultLanguage) { logError`Wiki info file specified default language is ${wikiData.wikiInfo.defaultLanguage}, but no such language file exists!`; if (langPath) { logError`Check if an appropriate file exists in ${langPath}?`; } else { - logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`; + logError`Be sure to specify ${'--lang-path'} or ${'HSMUSIC_LANG'} with the path to language files.`; } - return; + + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_FATAL_ERROR, + annotation: `wiki specifies default language whose file is not available`, + }); + + return false; } else { languages[internalDefaultLanguage.code] = internalDefaultLanguage; finalDefaultLanguage = internalDefaultLanguage; + stepStatusSummary.initializeDefaultLanguage.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_DONE_CLEAN, + annotation: `no custom default language specified`, + }); } for (const language of Object.values(languages)) { @@ -749,6 +973,8 @@ async function main() { const getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths); const getSizeOfImagePath = getSizeOfMediaFileHelper(imageFilePaths); + stepStatusSummary.preloadFileSizes.status = STATUS_STARTED_NOT_DONE; + logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`; fileSizePreloader.loadPaths(...additionalFilePaths.map((path) => path.device)); @@ -759,10 +985,22 @@ async function main() { fileSizePreloader.loadPaths(...imageFilePaths.map((path) => path.device)); await fileSizePreloader.waitUntilDoneLoading(); - logInfo`Done preloading filesizes!`; + if (fileSizePreloader.hasErrored) { + logWarn`Some media files couldn't be read for preloading filesizes.`; + logWarn`This means the wiki won't display file sizes for these files.`; + logWarn`Investigate missing or unreadable files to get that fixed!`; + + Object.assign(stepStatusSummary.preloadFileSizes, { + status: STATUS_HAS_WARNINGS, + annotation: `see log for details`, + }); + } else { + logInfo`Done preloading filesizes without any errors - nice!`; + stepStatusSummary.preloadFileSizes.status = STATUS_DONE_CLEAN; + } if (noBuild) { - return; + return true; } const developersComment = @@ -795,27 +1033,58 @@ async function main() { .map(line => ` ` + line) .join('\n') + `\n-->`; - return selectedBuildMode.go({ - cliOptions, - dataPath, - mediaPath, - queueSize, - srcRootPath: __dirname, - - defaultLanguage: finalDefaultLanguage, - languages, - missingImagePaths, - thumbsCache, - urls, - urlSpec, - wikiData, - - cachebust: '?' + CACHEBUST, - developersComment, - getSizeOfAdditionalFile, - getSizeOfImagePath, - niceShowAggregate, - }); + stepStatusSummary.performBuild.status = STATUS_STARTED_NOT_DONE; + + let buildModeResult; + + try { + buildModeResult = await selectedBuildMode.go({ + cliOptions, + dataPath, + mediaPath, + queueSize, + srcRootPath: __dirname, + + defaultLanguage: finalDefaultLanguage, + languages, + missingImagePaths, + thumbsCache, + urls, + urlSpec, + wikiData, + + cachebust: '?' + CACHEBUST, + developersComment, + getSizeOfAdditionalFile, + getSizeOfImagePath, + niceShowAggregate, + }); + } catch (error) { + console.error(error); + + logError`There was a JavaScript error performing the build.`; + fileIssue(); + + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_FATAL_ERROR, + message: `javascript error - view log for details`, + }); + + return false; + } + + if (buildModeResult !== true) { + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_HAS_WARNINGS, + message: `may not have completed - view log for details`, + }); + + return false; + } + + stepStatusSummary.performBuild.status = STATUS_DONE_CLEAN; + + return true; } // TODO: isMain detection isn't consistent across platforms here @@ -834,6 +1103,65 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus } } + if (showStepStatusSummary) { + console.error(colors.bright(`Step summary:`)); + + const longestNameLength = + Math.max(... + Object.values(stepStatusSummary) + .map(({name}) => name.length)); + + const anyStepsNotClean = + Object.values(stepStatusSummary) + .some(({status}) => + status === STATUS_HAS_WARNINGS || + status === STATUS_FATAL_ERROR || + status === STATUS_STARTED_NOT_DONE); + + for (const {name, status, annotation} of Object.values(stepStatusSummary)) { + let message = `${(name + ': ').padEnd(longestNameLength + 4, '.')} ${status}`; + if (annotation) { + message += ` (${annotation})`; + } + + switch (status) { + case STATUS_DONE_CLEAN: + console.error(colors.green(message)); + break; + + case STATUS_NOT_STARTED: + case STATUS_NOT_APPLICABLE: + console.error(colors.dim(message)); + break; + + case STATUS_HAS_WARNINGS: + case STATUS_STARTED_NOT_DONE: + console.error(colors.yellow(message)); + break; + + case STATUS_FATAL_ERROR: + console.error(colors.red(message)); + break; + + default: + console.error(message); + break; + } + } + + if (result === true) { + if (anyStepsNotClean) { + console.error(colors.bright(`Final output is true, but some steps aren't clean.`)); + process.exit(1); + return; + } else { + console.error(colors.bright(`Final output is true and all steps are clean.`)); + } + } else { + console.error(colors.bright(`Final output is not true (${result}).`)); + } + } + if (result !== true) { process.exit(1); return; -- cgit 1.3.0-6-gf8a5