From da4eda535893f1a26b095e5890658099e89d9457 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Mar 2023 10:27:40 -0400 Subject: data-steps: initial commit --- src/misc-templates.js | 61 +++++++++++------ src/page/album.js | 184 +++++++++++++++++++++++++++++++++++--------------- src/page/index.js | 70 +++++++++++++++++++ src/util/link.js | 168 ++++++++++++++++++++++++++++++--------------- src/util/sugar.js | 9 +++ 5 files changed, 362 insertions(+), 130 deletions(-) (limited to 'src') diff --git a/src/misc-templates.js b/src/misc-templates.js index 867193c..0d749d1 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -18,6 +18,8 @@ import { sortChronologically, } from './util/wiki-data.js'; +import u_link from './util/link.js'; + const BANDCAMP_DOMAINS = ['bc.s3m.us', 'music.solatrux.com']; const MASTODON_DOMAINS = ['types.pl']; @@ -78,45 +80,60 @@ function unbound_generateAdditionalFilesList(additionalFiles, { // Artist strings -function unbound_getArtistString(artists, { +unbound_generateContributionLinks.data = (contributions, { + showContribution = false, + showIcons = false, +}) => { + return { + showContribution, + showIcons, + + contributionData: + contributions.map(({who, what}) => ({ + artistLinkData: u_link.artist.data(who), + + hasContributionPart: !!(showContribution && what), + hasExternalPart: !!(showIcons && !empty(who.urls)), + + artistUrls: who.urls, + contribution: showContribution && what, + })), + }; +}; + +function unbound_generateContributionLinks(data, { html, + iconifyURL, language, link, - - iconifyURL, - - showIcons = false, - showContrib = false, }) { return language.formatConjunctionList( - artists.map(({who, what}) => { - const {urls} = who; - - const hasContribPart = !!(showContrib && what); - const hasExternalPart = !!(showIcons && !empty(urls)); - - const artistLink = link.artist(who); + data.contributionData.map(({ + artistLinkData, + hasContributionPart, + hasExternalPart, + artistUrls, + contribution, + }) => { + const artistLink = link.artist(artistLinkData); const externalLinks = hasExternalPart && html.tag('span', - { - [html.noEdgeWhitespace]: true, - class: 'icons' - }, + {[html.noEdgeWhitespace]: true, class: 'icons'}, language.formatUnitList( - urls.map(url => iconifyURL(url, {language})))); + artistUrls.map(url => iconifyURL(url, {language})))); return ( - (hasContribPart + (hasContributionPart ? (hasExternalPart ? language.$('misc.artistLink.withContribution.withExternalLinks', { artist: artistLink, - contrib: what, + contrib: contribution, links: externalLinks, }) : language.$('misc.artistLink.withContribution', { artist: artistLink, - contrib: what, + contrib: contribution, })) : (hasExternalPart ? language.$('misc.artistLink.withExternalLinks', { @@ -1040,7 +1057,7 @@ export { unbound_generateAdditionalFilesList as generateAdditionalFilesList, unbound_generateAdditionalFilesShortcut as generateAdditionalFilesShortcut, - unbound_getArtistString as getArtistString, + unbound_generateContributionLinks as generateContributionLinks, unbound_generateChronologyLinks as generateChronologyLinks, diff --git a/src/page/album.js b/src/page/album.js index 9ee57c0..a266b91 100644 --- a/src/page/album.js +++ b/src/page/album.js @@ -12,55 +12,148 @@ import { getTotalDuration, } from '../util/wiki-data.js'; +import { + generateContributionLinks as u_generateContributionLinks, +} from '../misc-templates.js'; + +import u_link from '../util/link.js'; + export const description = `per-album info & track artwork gallery pages`; export function targets({wikiData}) { return wikiData.albumData; } -export function write(album, {wikiData}) { - const unbound_trackToListItem = (track, { - getArtistString, - getLinkThemeString, - html, - language, - link, - }) => { - const itemOpts = { - duration: language.formatDuration(track.duration ?? 0), - track: link.track(track), - }; +export const dataSteps = { + computePathsForTarget(data, album) { + data.hasGalleryPage = album.tracks.some(t => t.hasUniqueCoverArt); + data.hasCommentaryPage = !!album.commentary || album.tracks.some(t => t.commentary);; - return html.tag('li', - {style: getLinkThemeString(track.color)}, - compareArrays( - track.artistContribs.map((c) => c.who), - album.artistContribs.map((c) => c.who), - {checkOrder: false} - ) - ? language.$('trackList.item.withDuration', itemOpts) - : language.$('trackList.item.withDuration.withArtists', { - ...itemOpts, - by: html.tag('span', - {class: 'by'}, - language.$('trackList.item.withArtists.by', { - artists: getArtistString(track.artistContribs), - })), - })); + return [ + { + type: 'page', + path: ['album', album.directory], + }, + + data.hasGalleryPage && { + type: 'page', + path: ['albumGallery', album.directory], + }, + + data.hasCommentaryPage && { + type: 'page', + path: ['albumCommentary', album.directory], + }, + + { + type: 'data', + path: ['album', album.directory], + }, + ]; + }, + + computeDataCommonAcrossMixedWrites(data, album) { + data.albumDuration = getTotalDuration(album.tracks); + }, + + computeDataCommonAcrossPageWrites(data, album) { + data.listTag = getAlbumListTag(album); + }, + + computeDataForPageWrite: { + album(data, album, _pathArgs) { + data.hasAdditionalFiles = !empty(album.additionalFiles); + data.numAdditionalFiles = album.additionalFiles.flatMap((g) => g.files).length; + + data.displayTrackSections = + album.trackSections && + (album.trackSections.length > 1 || + !album.trackSections[0]?.isDefaultTrackSection); + }, + }, + + computeContentForPageWrite: { + album(data, { + absoluteTo, + fancifyURL, + generateAdditionalFilesShortcut, + generateAdditionalFilesList, + generateChronologyLinks, + generateContributionLinks, + generateContentHeading, + generateNavigationLinks, + getAlbumCover, + getAlbumStylesheet, + getLinkThemeString, + getSizeOfAdditionalFile, + getThemeString, + html, + link, + language, + transformMultiline, + urls, + }) { + const generateTrackListItem = bindOpts(u_generateTrackListItem, { + generateContributionLinks, + getLinkThemeString, + html, + language, + link, + }); + + void generateTrackListItem; + }, + }, +}; + +function u_generateTrackListItem(data, { + generateContributionLinks, + getLinkThemeString, + html, + language, + link, +}) { + const stringOpts = { + duration: language.formatDuration(data.duration), + track: link.track(data.linkData), }; - const hasAdditionalFiles = !empty(album.additionalFiles); - const numAdditionalFiles = album.additionalFiles.flatMap((g) => g.files).length; + return html.tag('li', + {style: getLinkThemeString(data.color)}, + (!data.showArtists + ? language.$('trackList.item.withDuration', stringOpts) + : language.$('trackList.item.withDuration.withArtists', { + ...stringOpts, + by: + html.tag('span', {class: 'by'}, + language.$('trackList.item.withArtists.by', { + artists: generateContributionLinks(data.contributionLinksData), + })), + }))); +} - const albumDuration = getTotalDuration(album.tracks); +u_generateTrackListItem.data = track => { + return { + color: track.color, + duration: track.duration ?? 0, + linkData: u_link.track.data(track), - const displayTrackSections = - album.trackSections && - (album.trackSections.length > 1 || - !album.trackSections[0]?.isDefaultTrackSection); + showArtists: + !compareArrays( + track.artistContribs.map((c) => c.who), + track.album.artistContribs.map((c) => c.who), + {checkOrder: false}), - const listTag = getAlbumListTag(album); + contributionLinksData: + u_generateContributionLinks.data(track.artistContribs, { + showContribution: false, + showIcons: false, + }), + }; +}; +/* +export function write(album, {wikiData}) { const getSocialEmbedDescription = ({ getArtistString: _getArtistString, language, @@ -127,24 +220,6 @@ export function write(album, {wikiData}) { type: 'page', path: ['album', album.directory], page: ({ - absoluteTo, - fancifyURL, - generateAdditionalFilesShortcut, - generateAdditionalFilesList, - generateChronologyLinks, - generateContentHeading, - generateNavigationLinks, - getAlbumCover, - getAlbumStylesheet, - getArtistString, - getLinkThemeString, - getSizeOfAdditionalFile, - getThemeString, - html, - link, - language, - transformMultiline, - urls, }) => { const trackToListItem = bindOpts(unbound_trackToListItem, { getArtistString, @@ -867,3 +942,4 @@ export function generateAlbumAdditionalFilesList(album, additionalFiles, { link.albumAdditionalFile({album, file}), }); } +*/ diff --git a/src/page/index.js b/src/page/index.js index f580cbe..fc0c646 100644 --- a/src/page/index.js +++ b/src/page/index.js @@ -17,6 +17,76 @@ // Usually this will simply mean returning the appropriate thingData array, // but it may also apply filter/map/etc if useful. // +// dataSteps {...} +// Object with key-to-functions matching a variety of steps described next. +// In general, the use of dataSteps is to separate data computations from +// actual page content generation, making explicit what data is carried +// from one step to the next, and letting the build/serve mode have a +// standardized guideline for deciding when to compute data at each step. +// +// Important notes on the usage of dataSteps: +// +// - Every dataStep function is provided a `data` object which stores +// values passed through to that step. To save data for a coming step, +// just mutate this object (set a key and value on it). +// +// - Some dataStep functions return values, but not all do. Some are just +// for computing data used by following steps. +// +// - Do not set any data properties to live wiki objects or arrays/objects +// including live wiki objects. All data passed between each step should +// be fully serializable in JSON or otherwise plain-text format. +// +// **NB: DATA WRITES ARE CURRENTLY DISABLED. All steps exclusively applicable +// to data writes will currently be skipped.** +// +// dataSteps.computePathsForTargets(data, target) +// Compute paths at which pages or files will be generated for the given +// target wiki object, returning {type, path} pairs. Data applied here, +// such as flags indicating which pages have content, will automatically +// be passed onto all further steps. +// +// dataSteps.computeDataCommonAcrossMixedWrites(data, target) +// Compute data which is useful in a mixed list of any path writes. +// This function should only be used when data is pertinent to more than +// one kind of write, ex. a variable which is useful for page writes but +// also exposed through a data write. Data applied here is passed onto +// all further steps. +// +// dataSteps.computeDataCommonAcrossPageWrites(data, target) +// Compute data which is useful across more than one page write. +// Use this function when data is pertinent to more than one page write, +// but isn't relevant outside of page writes. Data applied here is passed +// onto further steps for page writes. +// +// dataSteps.computeDataCommonAcrossDataWrites(data, target) +// Analagous to computeDataAcrossPages; for data writes. +// +// dataSteps.computeDataForPageWrite.[pathKey](data, target, pathArgs) +// Compute data which is useful for a single given page write. +// Note that dataSteps.computeDataForPage is an object; its keys are the +// possible path keys from computePathsForTargets() for page writes. +// Data applied here is passed onto the final write call for this page. +// +// dataSteps.computeDataForDataWrite.[pathKey](data, target, pathArgs) +// Analogous to computeDataForPageWrite; for data writes. +// +// dataSteps.computeContentForPageWrite.[pathKey](data, utils) +// Use data prepared in previous steps to compute and return the actual +// content for a given page write. The target wiki object is no longer +// accessible at this step, so all required data must be computed ahead. +// +// - The returned page object will be destructured for +// usage in generateDocumentHTML(), `src/write/page-template.js`. +// +// - The utils object is a set of bound functions handy for any page +// content. It is described in `src/write/bind-utilities.js`. +// +// dataSteps.compteContentForDataWrite.[pathKey](data, utils) +// Analogous to computeContentForDataWrite; for data writes. +// NB: When data writes are enabled, the utils object will be uniquely +// defined separate from what's provided to page writes. +// // write(thing, {wikiData}) // Provides descriptors for any page and data writes associated with the // given thing (which will be a value from the targets() array). This diff --git a/src/util/link.js b/src/util/link.js index 6210634..00abc69 100644 --- a/src/util/link.js +++ b/src/util/link.js @@ -24,23 +24,29 @@ export function unbound_getLinkThemeString(color, { const appendIndexHTMLRegex = /^(?!https?:\/\/).+\/$/; -const linkHelper = - (hrefFn, { - color = true, - attr = null, - } = {}) => - (thing, { +function linkHelper({ + path: pathOption, + + expectThing = true, + color: colorOption = true, + + attr: attrOption = null, + data: dataOption = null, + text: textOption = null, +}) { + const generateLink = (data, { getLinkThemeString, to, text = '', attributes = null, class: className = '', - color: color2 = true, + color = true, hash = '', preferShortName = false, }) => { - let href = hrefFn(thing, {to}); + const path = (expectThing ? pathOption(data) : pathOption()); + let href = to(...path); if (link.globalOptions.appendIndexHTML) { if (appendIndexHTMLRegex.test(href)) { @@ -52,41 +58,100 @@ const linkHelper = href += (hash.startsWith('#') ? '' : '#') + hash; } - return html.tag( - 'a', + return html.tag('a', { - ...(attr ? attr(thing) : {}), + ...(attrOption ? attrOption(data) : {}), ...(attributes ? attributes : {}), href, style: - typeof color2 === 'string' - ? getLinkThemeString(color2) - : color2 && color - ? getLinkThemeString(thing.color) + typeof color === 'string' + ? getLinkThemeString(color) + : color && colorOption + ? getLinkThemeString(data.color) : '', class: className, }, + (text || - (preferShortName - ? thing.nameShort ?? thing.name - : thing.name)) - ); + (textOption + ? textOption(data) + : (preferShortName + ? data.nameShort ?? data.name + : data.name)))); }; -const linkDirectory = (key, {expose = null, attr = null, ...conf} = {}) => - linkHelper((thing, {to}) => to('localized.' + key, thing.directory), { - attr: (thing) => ({ - ...(attr ? attr(thing) : {}), - ...(expose ? {[expose]: thing.directory} : {}), + generateLink.data = thing => { + if (!expectThing) { + throw new Error(`This kind of link doesn't need any data serialized`); + } + + const data = (dataOption ? dataOption(thing) : {}); + + if (colorOption) { + data.color = thing.color; + } + + if (!textOption) { + data.name = thing.name; + data.nameShort = thing.nameShort ?? thing.name; + } + + return data; + }; + + return generateLink; +} + +function linkDirectory(key, { + exposeDirectory = null, + prependLocalized = true, + + data = null, + attr = null, + ...conf +}) { + return linkHelper({ + data: thing => ({ + ...(data ? data(thing) : {}), + directory: thing.directory, }), + + path: data => + (prependLocalized + ? ['localized.' + key, data.directory] + : [key, data.directory]), + + attr: (data) => ({ + ...(attr ? attr(data) : {}), + ...(exposeDirectory ? {[exposeDirectory]: data.directory} : {}), + }), + + ...conf, + }); +} + +function linkIndex(key, conf) { + return linkHelper({ + path: () => [key], + + expectThing: false, ...conf, }); +} -const linkPathname = (key, conf) => - linkHelper(({directory: pathname}, {to}) => to(key, pathname), conf); +function linkAdditionalFile(key, conf) { + return linkHelper({ + data: ({file, album}) => ({ + directory: album.directory, + file, + }), -const linkIndex = (key, conf) => - linkHelper((_, {to}) => to('localized.' + key), conf); + path: data => ['media.albumAdditionalFile', data.directory, data.file], + + color: false, + ...conf, + }); +} // Mapping of Thing constructor classes to the key for a link.x() function. // These represent a sensible "default" link, i.e. to the primary page for @@ -114,6 +179,7 @@ const link = { }, album: linkDirectory('album'), + albumAdditionalFile: linkAdditionalFile('albumAdditionalFile'), albumGallery: linkDirectory('albumGallery'), albumCommentary: linkDirectory('albumCommentary'), artist: linkDirectory('artist', {color: false}), @@ -130,32 +196,26 @@ const link = { newsEntry: linkDirectory('newsEntry', {color: false}), staticPage: linkDirectory('staticPage', {color: false}), tag: linkDirectory('tag'), - track: linkDirectory('track', {expose: 'data-track'}), - - // TODO: This is a bit hacky. Files are just strings (not objects), so we - // have to manually provide the album alongside the file. They also don't - // follow the usual {name: whatever} type shape, so we have to provide that - // ourselves. - _albumAdditionalFileHelper: linkHelper( - (fakeFileObject, {to}) => - to( - 'media.albumAdditionalFile', - fakeFileObject.album.directory, - fakeFileObject.name), - {color: false}), - - albumAdditionalFile: ({file, album}, {to, ...opts}) => - link._albumAdditionalFileHelper( - { - name: file, - album, - }, - {to, ...opts}), - - media: linkPathname('media.path', {color: false}), - root: linkPathname('shared.path', {color: false}), - data: linkPathname('data.path', {color: false}), - site: linkPathname('localized.path', {color: false}), + track: linkDirectory('track', {exposeDirectory: 'data-track'}), + + media: linkDirectory('media.path', { + prependLocalized: false, + color: false, + }), + + root: linkDirectory('shared.path', { + prependLocalized: false, + color: false, + }), + data: linkDirectory('data.path', { + prependLocalized: false, + color: false, + }), + + site: linkDirectory('localized.path', { + prependLocalized: false, + color: false, + }), // This is NOT an arrow functions because it should be callable for other // "this" objects - i.e, if we bind arguments in other functions on the same diff --git a/src/util/sugar.js b/src/util/sugar.js index 0813c1d..c60bddb 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -150,6 +150,15 @@ export function bindOpts(fn, bind) { value: fn.name ? `(options-bound) ${fn.name}` : `(options-bound)`, }); + for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(fn))) { + if (key === 'length') continue; + if (key === 'name') continue; + if (key === 'arguments') continue; + if (key === 'caller') continue; + if (key === 'prototype') continue; + Object.defineProperty(bound, key, descriptor); + } + return bound; } -- cgit 1.3.0-6-gf8a5