From 8ab00d99fa2f14ac983f0693552b26e4050a939c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Mar 2023 15:00:59 -0300 Subject: data steps: content function evaluation essentials Also some more actual content in generateAlbumInfoPageContent, which is in fact fully working as-is(!!). --- src/content-function.js | 221 +++++++++++++++++++++ .../generateAdditionalFilesShortcut.js | 27 +++ .../dependencies/generateAlbumInfoPageContent.js | 96 +++++---- .../dependencies/linkAlbumAdditionalFile.js | 16 +- src/content/dependencies/linkAlbumCommentary.js | 8 + src/content/dependencies/linkAlbumGallery.js | 8 + src/content/dependencies/linkArtist.js | 11 +- src/content/dependencies/linkTemplate.js | 53 +++++ src/content/dependencies/linkThing.js | 51 +++++ 9 files changed, 433 insertions(+), 58 deletions(-) create mode 100644 src/content/dependencies/generateAdditionalFilesShortcut.js create mode 100644 src/content/dependencies/linkAlbumCommentary.js create mode 100644 src/content/dependencies/linkAlbumGallery.js create mode 100644 src/content/dependencies/linkTemplate.js create mode 100644 src/content/dependencies/linkThing.js diff --git a/src/content-function.js b/src/content-function.js index 891a348f..dbac691b 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -200,3 +200,224 @@ export function fulfillDependencies({ return newFulfilledDependencies; } + +export function getRelationsTree(dependencies, contentFunctionName, ...args) { + const relationIdentifier = Symbol('Relation'); + + function recursive(contentFunctionName, ...args) { + const contentFunction = dependencies[contentFunctionName]; + if (!contentFunctionName) { + throw new Error(`Couldn't find dependency ${contentFunctionName}`); + } + + if (!contentFunction?.relations) { + return null; + } + + const relationSlots = {}; + + const relationSymbolMessage = (() => { + let num = 1; + return name => `#${num++} ${name}`; + })(); + + const relationFunction = (name, ...args) => { + const relationSymbol = Symbol(relationSymbolMessage(name)); + relationSlots[relationSymbol] = {name, args}; + return {[relationIdentifier]: relationSymbol}; + }; + + const relationsLayout = contentFunction.relations(relationFunction, ...args); + + const relationsTree = Object.fromEntries( + Object.getOwnPropertySymbols(relationSlots) + .map(symbol => [symbol, relationSlots[symbol]]) + .map(([symbol, {name, args}]) => [ + symbol, + recursive(name, ...args), + ])); + + return { + layout: relationsLayout, + slots: relationSlots, + tree: relationsTree, + }; + } + + const relationsTree = recursive(contentFunctionName, ...args); + + return { + root: { + name: contentFunctionName, + args, + relations: relationsTree?.layout, + }, + + relationIdentifier, + relationsTree, + }; +} + +export function flattenRelationsTree({ + root, + relationIdentifier, + relationsTree, +}) { + const flatRelationSlots = {}; + + function recursive({layout, slots, tree}) { + for (const slot of Object.getOwnPropertySymbols(slots)) { + if (tree[slot]) { + recursive(tree[slot]); + } + + flatRelationSlots[slot] = { + name: slots[slot].name, + args: slots[slot].args, + relations: tree[slot]?.layout ?? null, + }; + } + } + + recursive(relationsTree); + + return { + root, + relationIdentifier, + flatRelationSlots, + }; +} + +export function fillRelationsLayoutFromSlotResults(relationIdentifier, results, layout) { + function recursive(object) { + if (typeof object !== 'object' || object === null) { + return object; + } + + if (Array.isArray(object)) { + return object.map(recursive); + } + + if (relationIdentifier in object) { + return results[object[relationIdentifier]]; + } + + if (object.constructor !== Object) { + throw new Error(`Expected primitive, array, relation, or normal {key: value} style Object`); + } + + return Object.fromEntries( + Object.entries(object) + .map(([key, value]) => [key, recursive(value)])); + } + + return recursive(layout); +} + +function getNeededContentDependencyNames(contentDependencies, name) { + const set = new Set(); + + function recursive(name) { + const contentFunction = contentDependencies[name]; + for (const dependencyName of contentFunction?.contentDependencies ?? []) { + recursive(dependencyName); + } + set.add(name); + } + + recursive(name); + + return set; +} + +export function quickEvaluate({ + contentDependencies: allContentDependencies, + extraDependencies: allExtraDependencies, + + name, + args, +}) { + const treeInfo = getRelationsTree(allContentDependencies, name, ...args); + const flatTreeInfo = flattenRelationsTree(treeInfo); + const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo; + + const neededContentDependencyNames = + getNeededContentDependencyNames(allContentDependencies, name); + + // Content functions aren't recursive, so by following the set above + // sequentually, we will always provide fulfilled content functions as the + // dependencies for later content functions. + const fulfilledContentDependencies = {}; + for (const name of neededContentDependencyNames) { + const unfulfilledContentFunction = allContentDependencies[name]; + if (!unfulfilledContentFunction) continue; + + const {contentDependencies, extraDependencies} = unfulfilledContentFunction; + + if (empty(contentDependencies) && empty(extraDependencies)) { + fulfilledContentDependencies[name] = unfulfilledContentFunction; + continue; + } + + const fulfillments = {}; + + for (const dependencyName of contentDependencies ?? []) { + if (dependencyName in fulfilledContentDependencies) { + fulfillments[dependencyName] = + fulfilledContentDependencies[dependencyName]; + } + } + + for (const dependencyName of extraDependencies ?? []) { + if (dependencyName in allExtraDependencies) { + fulfillments[dependencyName] = + allExtraDependencies[dependencyName]; + } + } + + fulfilledContentDependencies[name] = + unfulfilledContentFunction.fulfill(fulfillments); + } + + // There might still be unfulfilled content functions if dependencies weren't + // provided as part of allContentDependencies or allExtraDependencies. + // Catch and report these early, together in an aggregate error. + const unfulfilledErrors = []; + for (const name of neededContentDependencyNames) { + const contentFunction = fulfilledContentDependencies[name]; + if (!contentFunction) continue; + if (!contentFunction.fulfilled) { + try { + contentFunction(); + } catch (error) { + error.message = `(${name}) ${error.message}`; + unfulfilledErrors.push(error); + } + } + } + + if (!empty(unfulfilledErrors)) { + throw new AggregateError(unfulfilledErrors, `Content functions unfulfilled`); + } + + const slotResults = {}; + + function runContentFunction({name, args, relations}) { + const contentFunction = fulfilledContentDependencies[name]; + const filledRelations = + fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, relations); + + const generateArgs = [ + contentFunction.data?.(...args), + filledRelations, + ].filter(Boolean); + + return contentFunction(...generateArgs); + } + + for (const slot of Object.getOwnPropertySymbols(flatRelationSlots)) { + slotResults[slot] = runContentFunction(flatRelationSlots[slot]); + } + + return runContentFunction(root); +} diff --git a/src/content/dependencies/generateAdditionalFilesShortcut.js b/src/content/dependencies/generateAdditionalFilesShortcut.js new file mode 100644 index 00000000..dd097e28 --- /dev/null +++ b/src/content/dependencies/generateAdditionalFilesShortcut.js @@ -0,0 +1,27 @@ +export default { + extraDependencies: [ + 'html', + 'language', + ], + + data(additionalFiles) { + return { + titles: additionalFiles.map(fileGroup => fileGroup.title), + }; + }, + + generate(data, { + html, + language, + }) { + return language.$('releaseInfo.additionalFiles.shortcut', { + anchorLink: + html.tag('a', + {href: '#additional-files'}, + language.$('releaseInfo.additionalFiles.shortcut.anchorLink')), + + titles: + language.formatUnitList(data.titles), + }); + }, +} diff --git a/src/content/dependencies/generateAlbumInfoPageContent.js b/src/content/dependencies/generateAlbumInfoPageContent.js index 013ab3f4..236e550e 100644 --- a/src/content/dependencies/generateAlbumInfoPageContent.js +++ b/src/content/dependencies/generateAlbumInfoPageContent.js @@ -2,9 +2,12 @@ import {accumulateSum, empty} from '../../util/sugar.js'; export default { contentDependencies: [ + 'generateAdditionalFilesShortcut', 'generateAlbumAdditionalFilesList', 'generateContributionLinks', 'generateContentHeading', + 'linkAlbumCommentary', + 'linkAlbumGallery', ], extraDependencies: [ @@ -18,9 +21,9 @@ export default { const contributionLinksRelation = contribs => relation('generateContributionLinks', contribs, { - showContrib: true, + showContribution: true, showIcons: true, - }) + }); relations.artistLinks = contributionLinksRelation(album.artistContribs); @@ -37,7 +40,20 @@ export default { const contentHeadingRelation = () => relation('generateContentHeading'); + if (album.tracks.some(t => t.hasUniqueCoverArt)) { + relations.galleryLink = + relation('linkAlbumGallery', album); + } + + if (album.commentary || album.tracks.some(t => t.commentary)) { + relations.commentaryLink = + relation('linkAlbumCommentary', album); + } + if (!empty(album.additionalFiles)) { + relations.additionalFilesShortcut = + relation('generateAdditionalFilesShortcut', album.additionalFiles); + relations.additionalFilesHeading = contentHeadingRelation(); @@ -84,29 +100,29 @@ export default { content.main = { headingMode: 'sticky', - content: [ + content: html.tag(null, [ html.tag('p', { [html.onlyIfContent]: true, [html.joinChildren]: '
', }, [ - !empty(relations.artistLinks) && + relations.artistLinks && language.$('releaseInfo.by', { artists: relations.artistLinks, }), - !empty(relations.coverArtistLinks) && + relations.coverArtistLinks && language.$('releaseInfo.coverArtBy', { artists: relations.coverArtistLinks, }), - !empty(relations.wallpaperArtistLinks) && + relations.wallpaperArtistLinks && language.$('releaseInfo.wallpaperArtBy', { artists: relations.wallpaperArtistLinks, }), - !empty(relations.bannerArtistLinks) && + relations.bannerArtistLinks && language.$('releaseInfo.bannerArtBy', { artists: relations.bannerArtistLinks, }), @@ -130,31 +146,30 @@ export default { }), ]), - /* - html.tag('p', - { - [html.onlyIfContent]: true, - [html.joinChildren]: '
', - }, - [ - hasAdditionalFiles && - generateAdditionalFilesShortcut(album.additionalFiles), + html.tag('p', + { + [html.onlyIfContent]: true, + [html.joinChildren]: html.tag('br'), + }, + [ + relations.additionalFilesShortcut, - checkGalleryPage(album) && - language.$('releaseInfo.viewGallery', { - link: link.albumGallery(album, { - text: language.$('releaseInfo.viewGallery.link'), - }), - }), + relations.galleryLink && + language.$('releaseInfo.viewGallery', { + link: + relations.galleryLink + .slot('text', language.$('releaseInfo.viewGallery.link')), + }), - checkCommentaryPage(album) && - language.$('releaseInfo.viewCommentary', { - link: link.albumCommentary(album, { - text: language.$('releaseInfo.viewCommentary.link'), - }), - }), - ]), + relations.commentaryLink && + language.$('releaseInfo.viewCommentary', { + link: + relations.commentaryLink + .slot('text', language.$('releaseInfo.viewCommentary.link')), + }), + ]), + /* !empty(album.urls) && html.tag('p', language.$('releaseInfo.listenOn', { @@ -204,25 +219,6 @@ export default { ), }) ]), - - ...html.fragment( - hasAdditionalFiles && [ - generateContentHeading({ - id: 'additional-files', - title: language.$('releaseInfo.additionalFiles.heading', { - additionalFiles: language.countAdditionalFiles(numAdditionalFiles, { - unit: true, - }), - }), - }), - - generateAlbumAdditionalFilesList(album, album.additionalFiles, { - generateAdditionalFilesList, - getSizeOfAdditionalFile, - link, - urls, - }), - ]), */ relations.additionalFilesList && [ @@ -240,12 +236,12 @@ export default { data.artistCommentary && [ relations.artistCommentaryHeading .slot('id', 'artist-commentary') - .slot('title', language.$('releaseDate.artistCommentary')), + .slot('title', language.$('releaseInfo.artistCommentary')), html.tag('blockquote', transformMultiline(data.artistCommentary)), ], - ] + ]), }; return content; diff --git a/src/content/dependencies/linkAlbumAdditionalFile.js b/src/content/dependencies/linkAlbumAdditionalFile.js index 6c47edca..d1cca914 100644 --- a/src/content/dependencies/linkAlbumAdditionalFile.js +++ b/src/content/dependencies/linkAlbumAdditionalFile.js @@ -1,4 +1,14 @@ export default { + contentDependencies: [ + 'linkTemplate', + ], + + relations(relation) { + return { + linkTemplate: relation('linkTemplate'), + }; + }, + data(album, file) { return { albumDirectory: album.directory, @@ -6,7 +16,9 @@ export default { }; }, - generate(data) { - return `(stub album additional file link: ${data.albumDirectory}/${data.file})`; + generate(data, relations) { + return relations.linkTemplate + .slot('path', ['media.albumAdditionalFile', data.albumDirectory, data.file]) + .slot('content', data.file); }, }; diff --git a/src/content/dependencies/linkAlbumCommentary.js b/src/content/dependencies/linkAlbumCommentary.js new file mode 100644 index 00000000..ab519fd6 --- /dev/null +++ b/src/content/dependencies/linkAlbumCommentary.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, album) => + ({link: relation('linkThing', 'localized.albumCommentary', album)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkAlbumGallery.js b/src/content/dependencies/linkAlbumGallery.js new file mode 100644 index 00000000..e3f30a29 --- /dev/null +++ b/src/content/dependencies/linkAlbumGallery.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, album) => + ({link: relation('linkThing', 'localized.albumGallery', album)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkArtist.js b/src/content/dependencies/linkArtist.js index 396eca41..718ee6fa 100644 --- a/src/content/dependencies/linkArtist.js +++ b/src/content/dependencies/linkArtist.js @@ -1,9 +1,8 @@ export default { - data(artist) { - return {directory: artist.directory}; - }, + contentDependencies: ['linkThing'], - generate(data) { - return `(stub artist link: "${data.directory}")`; - }, + relations: (relation, artist) => + ({link: relation('linkThing', 'localized.artist', artist)}), + + generate: (relations) => relations.link, }; diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js new file mode 100644 index 00000000..94b90652 --- /dev/null +++ b/src/content/dependencies/linkTemplate.js @@ -0,0 +1,53 @@ +import {empty} from '../../util/sugar.js'; + +export default { + extraDependencies: [ + 'appendIndexHTML', + 'getColors', + 'html', + 'to', + ], + + generate({ + appendIndexHTML, + getColors, + html, + to, + }) { + return html.template(slot => + slot('color', ([color]) => + slot('hash', ([hash]) => + slot('href', ([href]) => + slot('path', ([...path]) => { + let style; + + if (!href && !empty(path)) { + href = to(...path); + } + + if (appendIndexHTML) { + if (/^(?!https?:\/\/).+\/$/.test(href)) { + href += 'index.html'; + } + } + + if (hash) { + href += (hash.startsWith('#') ? '' : '#') + hash; + } + + if (color) { + const {primary, dim} = getColors(color); + style = `--primary-color: ${primary}; --dim-color: ${dim}`; + } + + return slot('attributes', ([attributes]) => + html.tag('a', + { + ...attributes ?? {}, + href, + style, + }, + slot('content'))); + }))))); + }, +} diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js new file mode 100644 index 00000000..ebff6761 --- /dev/null +++ b/src/content/dependencies/linkThing.js @@ -0,0 +1,51 @@ +import {empty} from '../../util/sugar.js'; + +export default { + contentDependencies: [ + 'linkTemplate', + ], + + extraDependencies: [ + 'html', + ], + + relations(relation) { + return { + linkTemplate: relation('linkTemplate'), + }; + }, + + data(pathKey, thing) { + return { + pathKey, + + color: thing.color, + directory: thing.directory, + + name: thing.name, + nameShort: thing.nameShort, + }; + }, + + generate(data, relations, {html}) { + const path = [data.pathKey, data.directory]; + + return html.template(slot => + slot('content', ([...content]) => + slot('preferShortName', ([preferShortName]) => { + if (empty(content)) { + content = + (preferShortName + ? data.nameShort ?? data.name + : data.name); + } + + return relations.linkTemplate + .slot('path', path) + .slot('color', slot('color', data.color)) + .slot('attributes', slot('attributes', {})) + .slot('hash', slot('hash')) + .slot('content', content); + }))); + }, +} -- cgit 1.3.0-6-gf8a5