diff options
Diffstat (limited to 'src')
96 files changed, 2712 insertions, 1325 deletions
diff --git a/src/aggregate.js b/src/aggregate.js index cb806e89..d5ea2d73 100644 --- a/src/aggregate.js +++ b/src/aggregate.js @@ -604,7 +604,7 @@ export function showAggregate(topError, { headerPart += ` ${colors.dim(tracePart)}`; } - const head1 = level % 2 === 0 ? '\u21aa' : colors.dim('\u21aa'); + const head1 = '\u21aa'; const bar1 = ' '; const causePart = diff --git a/src/cli.js b/src/cli.js index bd4ec685..ec72a625 100644 --- a/src/cli.js +++ b/src/cli.js @@ -376,77 +376,79 @@ decorateTime.displayTime = function () { } }; -export function progressPromiseAll(msgOrMsgFn, array) { +const progressUpdateInterval = 1000 / 60; + +function progressShow(message, total) { + let start = Date.now(), last = 0, done = 0; + + const progress = () => { + const messagePart = + (typeof message === 'function' + ? message() + : message); + + const percent = + Math.round((done / total) * 1000) / 10 + '%'; + + const percentPart = + percent.padEnd('99.9%'.length, ' '); + + return `${messagePart} [${percentPart}]`; + }; + + process.stdout.write(`\r` + progress()); + + return () => { + done++; + + if (done === total) { + process.stdout.write( + `\r\x1b[2m` + progress() + + `\x1b[0;32m Done! ` + + `\x1b[0;2m(${formatDuration(Date.now() - start)}) ` + + `\x1b[0m\n` + ); + } else if (Date.now() - last >= progressUpdateInterval) { + process.stdout.write('\r' + progress()); + last = Date.now(); + } + }; +} + +export function progressPromiseAll(message, array) { if (!array.length) { return Promise.resolve([]); } - const msgFn = - typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn; - - let done = 0, - total = array.length; - process.stdout.write(`\r${msgFn()} [0/${total}]`); - const start = Date.now(); - return Promise.all( - array.map((promise) => - Promise.resolve(promise).then((val) => { - done++; - // const pc = `${done}/${total}`; - const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd( - '99.9%'.length, - ' ' - ); - if (done === total) { - const time = Date.now() - start; - process.stdout.write( - `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n` - ); - } else { - process.stdout.write(`\r${msgFn()} [${pc}] `); - } - return val; - }) - ) - ); + const show = progressShow(message, array.length); + + const next = value => { + show(); + + return value; + }; + + const promises = + array.map(promise => Promise.resolve(promise).then(next)); + + return Promise.all(promises); } -export function progressCallAll(msgOrMsgFn, array) { +export function progressCallAll(message, array) { if (!array.length) { return []; } - const msgFn = - typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn; + const show = progressShow(message, array.length); - const updateInterval = 1000 / 60; - - let done = 0, - total = array.length; - process.stdout.write(`\r${msgFn()} [0/${total}]`); - const start = Date.now(); - const vals = []; - let lastTime = 0; + const values = []; for (const fn of array) { - const val = fn(); - done++; - - if (done === total) { - const pc = '100%'.padEnd('99.9%'.length, ' '); - const time = Date.now() - start; - process.stdout.write( - `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n` - ); - } else if (Date.now() - lastTime >= updateInterval) { - const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd('99.9%'.length, ' '); - process.stdout.write(`\r${msgFn()} [${pc}] `); - lastTime = Date.now(); - } - vals.push(val); + values.push(fn()); + show(); } - return vals; + return values; } export function fileIssue({ @@ -459,6 +461,24 @@ export function fileIssue({ console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`)); } +// Quick'n dirty function to present a duration nicely for command-line use. +export function formatDuration(timeDelta) { + const seconds = timeDelta / 1000; + + if (seconds > 90) { + const modSeconds = Math.floor(seconds % 60); + const minutes = Math.floor(seconds - seconds % 60) / 60; + return `${minutes}m${modSeconds}s`; + } + + if (seconds < 0.1) { + return 'instant'; + } + + const precision = (seconds > 1 ? 3 : 2); + return `${seconds.toPrecision(precision)}s`; +} + export async function logicalCWD() { if (process.env.PWD) { return process.env.PWD; @@ -469,7 +489,7 @@ export async function logicalCWD() { try { await stat('/bin/sh'); - } catch (error) { + } catch { // Not logical, so sad. return process.cwd(); } diff --git a/src/common-util/sugar.js b/src/common-util/sugar.js index 9e344816..e931ad59 100644 --- a/src/common-util/sugar.js +++ b/src/common-util/sugar.js @@ -116,10 +116,14 @@ export function findIndexOrEnd(array, fn) { // returns null (or values in the array are nullish), they'll just be skipped in // the sum. export function accumulateSum(array, fn = x => x) { + if (!Array.isArray(array)) { + return accumulateSum(Array.from(array, fn)); + } + return array.reduce( (accumulator, value, index, array) => accumulator + - fn(value, index, array) ?? 0, + (fn(value, index, array) ?? 0), 0); } @@ -254,11 +258,20 @@ export function compareObjects(obj1, obj2, { // Stolen from jq! Which pro8a8ly stole the concept from other places. Nice. export const withEntries = (obj, fn) => { - const result = fn(Object.entries(obj)); - if (result instanceof Promise) { - return result.then(entries => Object.fromEntries(entries)); + if (obj instanceof Map) { + const result = fn(Array.from(obj.entries())); + if (result instanceof Promise) { + return result.then(entries => new map(entries)); + } else { + return new Map(result); + } } else { - return Object.fromEntries(result); + const result = fn(Object.entries(obj)); + if (result instanceof Promise) { + return result.then(entries => Object.fromEntries(entries)); + } else { + return Object.fromEntries(result); + } } } @@ -302,34 +315,74 @@ export function filterProperties(object, properties, { return filteredObject; } -export function queue(array, max = 50) { - if (max === 0) { - return array.map((fn) => fn()); +export function queue(functionList, queueSize = 50) { + if (queueSize === 0) { + return functionList.map(fn => fn()); } - const begin = []; - let current = 0; - const ret = array.map( - (fn) => - new Promise((resolve, reject) => { - begin.push(() => { - current++; - Promise.resolve(fn()).then((value) => { - current--; - if (current < max && begin.length) { - begin.shift()(); - } - resolve(value); - }, reject); - }); - }) - ); + const promiseList = []; + const resolveList = []; + const rejectList = []; - for (let i = 0; i < max && begin.length; i++) { - begin.shift()(); + for (let i = 0; i < functionList.length; i++) { + const promiseWithResolvers = Promise.withResolvers(); + promiseList.push(promiseWithResolvers.promise); + resolveList.push(promiseWithResolvers.resolve); + rejectList.push(promiseWithResolvers.reject); } - return ret; + let cursor = 0; + let running = 0; + + const next = async () => { + if (running >= queueSize) { + return; + } + + if (cursor === functionList.length) { + return; + } + + const thisFunction = functionList[cursor]; + const thisResolve = resolveList[cursor]; + const thisReject = rejectList[cursor]; + + delete functionList[cursor]; + delete resolveList[cursor]; + delete rejectList[cursor]; + + cursor++; + running++; + + try { + thisResolve(await thisFunction()); + } catch (error) { + thisReject(error); + } finally { + running--; + + // If the cursor is at 1, this is the first promise that resolved, + // so we're now done the "kick start", and can start the remaining + // promises (up to queueSize). + if (cursor === 1) { + // Since only one promise is used for the "kick start", and that one + // has just resolved, we know there's none running at all right now, + // and can start as many as specified in the queueSize right away. + for (let i = 0; i < queueSize; i++) { + next(); + } + } else { + next(); + } + } + }; + + // Only start a single promise, as a "kick start", so that it resolves as + // early as possible (it will resolve before we use CPU to start the rest + // of the promises, up to queueSize). + next(); + + return promiseList; } export function delay(ms) { diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 1e39b47d..3529c4dc 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -5,7 +5,7 @@ export default { 'generateAlbumCommentarySidebar', 'generateAlbumNavAccent', 'generateAlbumSecondaryNav', - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateCommentaryEntry', 'generateContentHeading', 'generateCoverArtwork', @@ -44,8 +44,8 @@ export default { relations.sidebar = relation('generateAlbumCommentarySidebar', album); - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); + relations.albumStyleTags = + relation('generateAlbumStyleTags', album, null); relations.albumLink = relation('linkAlbum', album); @@ -151,7 +151,7 @@ export default { headingMode: 'sticky', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['long-content'], mainContent: [ @@ -266,7 +266,10 @@ export default { }), })), - cover?.slots({mode: 'commentary'}), + cover?.slots({ + mode: 'commentary', + color: true, + }), trackDate && trackDate !== data.date && diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index 2ba3b272..516a7ca8 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -9,7 +9,7 @@ export default { 'generateAlbumGalleryTrackGrid', 'generateAlbumNavAccent', 'generateAlbumSecondaryNav', - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateIntrapageDotSwitcher', 'generatePageLayout', 'linkAlbum', @@ -46,8 +46,8 @@ export default { layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), albumLink: relation('linkAlbum', album), @@ -106,7 +106,7 @@ export default { headingMode: 'static', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['top-index'], mainContent: [ diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index ed19bf75..1664c788 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -11,9 +11,10 @@ export default { 'generateAlbumSecondaryNav', 'generateAlbumSidebar', 'generateAlbumSocialEmbed', - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateAlbumTrackList', 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generatePageLayout', 'linkAlbumCommentary', @@ -26,8 +27,8 @@ export default { layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), socialEmbed: relation('generateAlbumSocialEmbed', album), @@ -55,6 +56,9 @@ export default { contentHeading: relation('generateContentHeading'), + contentContentHeading: + relation('generateContentContentHeading', album), + releaseInfo: relation('generateAlbumReleaseInfo', album), @@ -79,7 +83,7 @@ export default { .map(entry => relation('generateCommentaryEntry', entry)), creditSourceEntries: - album.creditSources + album.creditingSources .map(entry => relation('generateCommentaryEntry', entry)), }), @@ -104,7 +108,7 @@ export default { color: data.color, headingMode: 'sticky', - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, additionalNames: relations.additionalNamesBox, @@ -157,11 +161,11 @@ export default { : html.blank()), !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + language.encapsulate(capsule, 'readCreditingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#crediting-sources'}, language.$(capsule, 'link')), })), ])), @@ -179,6 +183,11 @@ export default { }), ])), + (!html.isBlank(relations.artistCommentaryEntries) || + !html.isBlank(relations.creditSourceEntries)) + && + html.tag('hr', {class: 'main-separator'}), + language.encapsulate('releaseInfo.additionalFiles', capsule => html.tags([ relations.contentHeading.clone() @@ -191,20 +200,20 @@ export default { ])), html.tags([ - relations.contentHeading.clone() + relations.contentContentHeading.clone() .slots({ attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), + string: 'misc.artistCommentary', }), relations.artistCommentaryEntries, ]), html.tags([ - relations.contentHeading.clone() + relations.contentContentHeading.clone() .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), + attributes: {id: 'crediting-sources'}, + string: 'misc.creditingSources', }), relations.creditSourceEntries, diff --git a/src/content/dependencies/generateAlbumReferencedArtworksPage.js b/src/content/dependencies/generateAlbumReferencedArtworksPage.js index 7586393c..52c78dc2 100644 --- a/src/content/dependencies/generateAlbumReferencedArtworksPage.js +++ b/src/content/dependencies/generateAlbumReferencedArtworksPage.js @@ -1,6 +1,6 @@ export default { contentDependencies: [ - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateBackToAlbumLink', 'generateReferencedArtworksPage', 'linkAlbum', @@ -12,8 +12,8 @@ export default { page: relation('generateReferencedArtworksPage', album.coverArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), albumLink: relation('linkAlbum', album), @@ -35,7 +35,7 @@ export default { data.name, }), - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, navLinks: [ {auto: 'home'}, diff --git a/src/content/dependencies/generateAlbumReferencingArtworksPage.js b/src/content/dependencies/generateAlbumReferencingArtworksPage.js index d072d2f6..bc36ae06 100644 --- a/src/content/dependencies/generateAlbumReferencingArtworksPage.js +++ b/src/content/dependencies/generateAlbumReferencingArtworksPage.js @@ -1,6 +1,6 @@ export default { contentDependencies: [ - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateBackToAlbumLink', 'generateReferencingArtworksPage', 'linkAlbum', @@ -12,8 +12,8 @@ export default { page: relation('generateReferencingArtworksPage', album.coverArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), albumLink: relation('linkAlbum', album), @@ -35,7 +35,7 @@ export default { data.name, }), - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, navLinks: [ {auto: 'home'}, diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js index 0abb412c..2a958244 100644 --- a/src/content/dependencies/generateAlbumReleaseInfo.js +++ b/src/content/dependencies/generateAlbumReleaseInfo.js @@ -3,7 +3,7 @@ import {accumulateSum, empty} from '#sugar'; export default { contentDependencies: [ 'generateReleaseInfoContributionsLine', - 'linkExternal', + 'generateReleaseInfoListenLine', ], extraDependencies: ['html', 'language'], @@ -14,15 +14,8 @@ export default { relations.artistContributionsLine = relation('generateReleaseInfoContributionsLine', album.artistContribs); - relations.wallpaperArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.wallpaperArtistContribs); - - relations.bannerArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs); - - relations.externalLinks = - album.urls.map(url => - relation('linkExternal', url)); + relations.listenLine = + relation('generateReleaseInfoListenLine', album); return relations; }, @@ -87,21 +80,16 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, - language.$(capsule, 'listenOn', { - [language.onlyIfOptions]: ['links'], - - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => - link.slot('context', [ - 'album', - (data.numTracks === 0 - ? 'albumNoTracks' - : data.numTracks === 1 - ? 'albumOneTrack' - : 'albumMultipleTracks'), - ]))), + relations.listenLine.slots({ + context: [ + 'album', + + (data.numTracks === 0 + ? 'albumNoTracks' + : data.numTracks === 1 + ? 'albumOneTrack' + : 'albumMultipleTracks'), + ], })), ])), }; diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js deleted file mode 100644 index 6bfcc62e..00000000 --- a/src/content/dependencies/generateAlbumStyleRules.js +++ /dev/null @@ -1,107 +0,0 @@ -import {empty, stitchArrays} from '#sugar'; - -export default { - extraDependencies: ['to'], - - data(album, track) { - const data = {}; - - data.hasWallpaper = !empty(album.wallpaperArtistContribs); - data.hasBanner = !empty(album.bannerArtistContribs); - - if (data.hasWallpaper) { - if (!empty(album.wallpaperParts)) { - data.wallpaperMode = 'parts'; - - data.wallpaperPaths = - album.wallpaperParts.map(part => - (part.asset - ? ['media.albumWallpaperPart', album.directory, part.asset] - : null)); - - data.wallpaperStyles = - album.wallpaperParts.map(part => part.style); - } else { - data.wallpaperMode = 'one'; - data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension]; - data.wallpaperStyle = album.wallpaperStyle; - } - } - - if (data.hasBanner) { - data.hasBannerStyle = !!album.bannerStyle; - data.bannerStyle = album.bannerStyle; - } - - data.albumDirectory = album.directory; - - if (track) { - data.trackDirectory = track.directory; - } - - return data; - }, - - generate(data, {to}) { - const indent = parts => - (parts ?? []) - .filter(Boolean) - .join('\n') - .split('\n') - .map(line => ' '.repeat(4) + line) - .join('\n'); - - const rule = (selector, parts) => - (!empty(parts.filter(Boolean)) - ? [`${selector} {`, indent(parts), `}`] - : []); - - const oneWallpaperRule = - data.wallpaperMode === 'one' && - rule(`body::before`, [ - `background-image: url("${to(...data.wallpaperPath)}");`, - data.wallpaperStyle, - ]); - - const wallpaperPartRules = - data.wallpaperMode === 'parts' && - stitchArrays({ - path: data.wallpaperPaths, - style: data.wallpaperStyles, - }).map(({path, style}, index) => - rule(`.wallpaper-part:nth-child(${index + 1})`, [ - path && `background-image: url("${to(...path)}");`, - style, - ])); - - const nukeBasicWallpaperRule = - data.wallpaperMode === 'parts' && - rule(`body::before`, ['display: none']); - - const wallpaperRules = [ - oneWallpaperRule, - ...wallpaperPartRules || [], - nukeBasicWallpaperRule, - ]; - - const bannerRule = - data.hasBanner && - rule(`#banner img`, [ - data.bannerStyle, - ]); - - const dataRule = - rule(`:root`, [ - data.albumDirectory && - `--album-directory: ${data.albumDirectory};`, - data.trackDirectory && - `--track-directory: ${data.trackDirectory};`, - ]); - - return ( - [...wallpaperRules, bannerRule, dataRule] - .filter(Boolean) - .flat() - .join('\n')); - }, -}; diff --git a/src/content/dependencies/generateAlbumStyleTags.js b/src/content/dependencies/generateAlbumStyleTags.js new file mode 100644 index 00000000..4cdc6581 --- /dev/null +++ b/src/content/dependencies/generateAlbumStyleTags.js @@ -0,0 +1,65 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: ['generateAlbumWallpaperStyleTag', 'generateStyleTag'], + extraDependencies: ['html'], + + relations: (relation, album, _track) => ({ + styleTag: + relation('generateStyleTag'), + + wallpaperStyleTag: + relation('generateAlbumWallpaperStyleTag', album), + }), + + data(album, track) { + const data = {}; + + data.hasBanner = !empty(album.bannerArtistContribs); + + if (data.hasBanner) { + data.hasBannerStyle = !!album.bannerStyle; + data.bannerStyle = album.bannerStyle; + } + + data.albumDirectory = album.directory; + + if (track) { + data.trackDirectory = track.directory; + } + + return data; + }, + + generate: (data, relations, {html}) => + html.tags([ + relations.wallpaperStyleTag, + + relations.styleTag.clone().slots({ + attributes: {class: 'album-banner-style'}, + + rules: [ + data.hasBanner && { + select: '#banner img', + declare: [data.bannerStyle], + }, + ], + }), + + relations.styleTag.clone().slots({ + attributes: {class: 'album-directory-style'}, + + rules: [ + { + select: ':root', + declare: [ + data.albumDirectory && + `--album-directory: ${data.albumDirectory};`, + data.trackDirectory && + `--track-directory: ${data.trackDirectory};`, + ], + }, + ] + }), + ], {[html.joinChildren]: ''}), +}; diff --git a/src/content/dependencies/generateAlbumWallpaperStyleTag.js b/src/content/dependencies/generateAlbumWallpaperStyleTag.js new file mode 100644 index 00000000..47864a1d --- /dev/null +++ b/src/content/dependencies/generateAlbumWallpaperStyleTag.js @@ -0,0 +1,38 @@ +export default { + contentDependencies: ['generateWallpaperStyleTag'], + extraDependencies: ['html'], + + relations: (relation, album) => ({ + wallpaperStyleTag: + (album.hasWallpaperArt + ? relation('generateWallpaperStyleTag') + : null), + }), + + data: (album) => ({ + singleWallpaperPath: + ['media.albumWallpaper', album.directory, album.wallpaperFileExtension], + + singleWallpaperStyle: + album.wallpaperStyle, + + wallpaperPartPaths: + album.wallpaperParts.map(part => + (part.asset + ? ['media.albumWallpaperPart', album.directory, part.asset] + : null)), + + wallpaperPartStyles: + album.wallpaperParts.map(part => part.style), + }), + + generate: (data, relations, {html}) => + (relations.wallpaperStyleTag + ? relations.wallpaperStyleTag.slots({ + singleWallpaperPath: data.singleWallpaperPath, + singleWallpaperStyle: data.singleWallpaperStyle, + wallpaperPartPaths: data.wallpaperPartPaths, + wallpaperPartStyles: data.wallpaperPartStyles, + }) + : html.blank()), +}; diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js index 3e0cd1d2..e1fa7a0b 100644 --- a/src/content/dependencies/generateArtistGroupContributionsInfo.js +++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js @@ -1,83 +1,90 @@ -import {empty, filterProperties, stitchArrays, unique} from '#sugar'; +import {accumulateSum, empty, stitchArrays, withEntries} from '#sugar'; export default { contentDependencies: ['linkGroup'], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({groupCategoryData}) { - return { - groupOrder: groupCategoryData.flatMap(category => category.groups), - } - }, + sprawl: ({groupCategoryData}) => ({ + groupOrder: + groupCategoryData.flatMap(category => category.groups), + }), - query(sprawl, tracksAndAlbums) { - const filteredAlbums = tracksAndAlbums.filter(thing => !thing.album); - const filteredTracks = tracksAndAlbums.filter(thing => thing.album); + query(sprawl, contributions) { + const allGroupsUnordered = + new Set(contributions.flatMap(contrib => contrib.groups)); - const allAlbums = unique([ - ...filteredAlbums, - ...filteredTracks.map(track => track.album), - ]); + const allGroupsOrdered = + sprawl.groupOrder.filter(group => allGroupsUnordered.has(group)); - const allGroupsUnordered = new Set(Array.from(allAlbums).flatMap(album => album.groups)); - const allGroupsOrdered = sprawl.groupOrder.filter(group => allGroupsUnordered.has(group)); + const groupToThingsCountedForContributions = + new Map(allGroupsOrdered.map(group => [group, new Set])); - const mapTemplate = allGroupsOrdered.map(group => [group, 0]); - const groupToCountMap = new Map(mapTemplate); - const groupToDurationMap = new Map(mapTemplate); - const groupToDurationCountMap = new Map(mapTemplate); + const groupToThingsCountedForDuration = + new Map(allGroupsOrdered.map(group => [group, new Set])); - for (const album of filteredAlbums) { - for (const group of album.groups) { - groupToCountMap.set(group, groupToCountMap.get(group) + 1); - } - } + for (const contrib of contributions) { + for (const group of contrib.groups) { + if (contrib.countInContributionTotals) { + groupToThingsCountedForContributions.get(group).add(contrib.thing); + } - for (const track of filteredTracks) { - for (const group of track.album.groups) { - groupToCountMap.set(group, groupToCountMap.get(group) + 1); - if (track.duration && track.mainReleaseTrack === null) { - groupToDurationMap.set(group, groupToDurationMap.get(group) + track.duration); - groupToDurationCountMap.set(group, groupToDurationCountMap.get(group) + 1); + if (contrib.countInDurationTotals) { + groupToThingsCountedForDuration.get(group).add(contrib.thing); } } } + const groupToTotalContributions = + withEntries( + groupToThingsCountedForContributions, + entries => entries.map( + ([group, things]) => + ([group, things.size]))); + + const groupToTotalDuration = + withEntries( + groupToThingsCountedForDuration, + entries => entries.map( + ([group, things]) => + ([group, accumulateSum(things, thing => thing.duration)]))) + const groupsSortedByCount = allGroupsOrdered - .slice() - .sort((a, b) => groupToCountMap.get(b) - groupToCountMap.get(a)); + .filter(group => groupToTotalContributions.get(group) > 0) + .sort((a, b) => + (groupToTotalContributions.get(b) + - groupToTotalContributions.get(a))); - // The filter here ensures all displayed groups have at least some duration - // when sorting by duration. const groupsSortedByDuration = allGroupsOrdered - .filter(group => groupToDurationMap.get(group) > 0) - .sort((a, b) => groupToDurationMap.get(b) - groupToDurationMap.get(a)); + .filter(group => groupToTotalDuration.get(group) > 0) + .sort((a, b) => + (groupToTotalDuration.get(b) + - groupToTotalDuration.get(a))); const groupCountsSortedByCount = groupsSortedByCount - .map(group => groupToCountMap.get(group)); + .map(group => groupToTotalContributions.get(group)); const groupDurationsSortedByCount = groupsSortedByCount - .map(group => groupToDurationMap.get(group)); + .map(group => groupToTotalDuration.get(group)); const groupDurationsApproximateSortedByCount = groupsSortedByCount - .map(group => groupToDurationCountMap.get(group) > 1); + .map(group => groupToThingsCountedForDuration.get(group).size > 1); const groupCountsSortedByDuration = groupsSortedByDuration - .map(group => groupToCountMap.get(group)); + .map(group => groupToTotalContributions.get(group)); const groupDurationsSortedByDuration = groupsSortedByDuration - .map(group => groupToDurationMap.get(group)); + .map(group => groupToTotalDuration.get(group)); const groupDurationsApproximateSortedByDuration = groupsSortedByDuration - .map(group => groupToDurationCountMap.get(group) > 1); + .map(group => groupToThingsCountedForDuration.get(group).size > 1); return { groupsSortedByCount, @@ -93,29 +100,35 @@ export default { }; }, - relations(relation, query) { - return { - groupLinksSortedByCount: - query.groupsSortedByCount - .map(group => relation('linkGroup', group)), + relations: (relation, query) => ({ + groupLinksSortedByCount: + query.groupsSortedByCount + .map(group => relation('linkGroup', group)), - groupLinksSortedByDuration: - query.groupsSortedByDuration - .map(group => relation('linkGroup', group)), - }; - }, + groupLinksSortedByDuration: + query.groupsSortedByDuration + .map(group => relation('linkGroup', group)), + }), - data(query) { - return filterProperties(query, [ - 'groupCountsSortedByCount', - 'groupDurationsSortedByCount', - 'groupDurationsApproximateSortedByCount', + data: (query) => ({ + groupCountsSortedByCount: + query.groupCountsSortedByCount, - 'groupCountsSortedByDuration', - 'groupDurationsSortedByDuration', - 'groupDurationsApproximateSortedByDuration', - ]); - }, + groupDurationsSortedByCount: + query.groupDurationsSortedByCount, + + groupDurationsApproximateSortedByCount: + query.groupDurationsApproximateSortedByCount, + + groupCountsSortedByDuration: + query.groupCountsSortedByDuration, + + groupDurationsSortedByDuration: + query.groupDurationsSortedByDuration, + + groupDurationsApproximateSortedByDuration: + query.groupDurationsApproximateSortedByDuration, + }), slots: { title: { diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index 3a3cf8b7..1f738de4 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -20,29 +20,17 @@ export default { extraDependencies: ['html', 'language'], query: (artist) => ({ - // Even if an artist has served as both "artist" (compositional) and - // "contributor" (instruments, production, etc) on the same track, that - // track only counts as one unique contribution in the list. - allTracks: - unique( - ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - ]).flat() - .map(({thing}) => thing)), - - // Artworks are different, though. We intentionally duplicate album data - // objects when the artist has contributed some combination of cover art, - // wallpaper, and banner - these each count as a unique contribution. - allArtworkThings: - ([ - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, - artist.trackCoverArtistContributions, - ]).flat() - .filter(({annotation}) => !annotation?.startsWith('edits for wiki')) - .map(({thing}) => thing.thing), + trackContributions: [ + ...artist.trackArtistContributions, + ...artist.trackContributorContributions, + ], + + artworkContributions: [ + ...artist.albumCoverArtistContributions, + ...artist.albumWallpaperArtistContributions, + ...artist.albumBannerArtistContributions, + ...artist.trackCoverArtistContributions, + ], // Banners and wallpapers don't show up in the artist gallery page, only // cover art. @@ -93,7 +81,7 @@ export default { relation('generateArtistInfoPageTracksChunkedList', artist), tracksGroupInfo: - relation('generateArtistGroupContributionsInfo', query.allTracks), + relation('generateArtistGroupContributionsInfo', query.trackContributions), artworksChunkedList: relation('generateArtistInfoPageArtworksChunkedList', artist, false), @@ -102,7 +90,7 @@ export default { relation('generateArtistInfoPageArtworksChunkedList', artist, true), artworksGroupInfo: - relation('generateArtistGroupContributionsInfo', query.allArtworkThings), + relation('generateArtistGroupContributionsInfo', query.artworkContributions), artistGalleryLink: (query.hasGallery @@ -128,7 +116,11 @@ export default { .map(({annotation}) => annotation), totalTrackCount: - query.allTracks.length, + unique( + query.trackContributions + .filter(contrib => contrib.countInContributionTotals) + .map(contrib => contrib.thing)) + .length, totalDuration: artist.totalDuration, diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js index 2f2fe0c5..cb436b0f 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js @@ -1,8 +1,11 @@ +import {empty} from '#sugar'; + export default { contentDependencies: [ 'generateArtistInfoPageChunkItem', 'generateArtistInfoPageOtherArtistLinks', 'linkTrack', + 'transformContent', ], extraDependencies: ['html', 'language'], @@ -29,6 +32,9 @@ export default { otherArtistLinks: relation('generateArtistInfoPageOtherArtistLinks', [contrib]), + + originDetails: + relation('transformContent', contrib.thing.originDetails), }), data: (query, contrib) => ({ @@ -37,6 +43,9 @@ export default { annotation: contrib.annotation, + + label: + contrib.thing.label, }), slots: { @@ -51,9 +60,33 @@ export default { otherArtistLinks: relations.otherArtistLinks, annotation: - (slots.filterEditsForWiki - ? data.annotation?.replace(/^edits for wiki(: )?/, '') - : data.annotation), + language.encapsulate('artistPage.creditList.entry.artwork.accent', workingCapsule => { + const workingOptions = {}; + + const artworkLabel = data.label; + + if (artworkLabel) { + workingCapsule += '.withLabel'; + workingOptions.label = + language.typicallyLowerCase(artworkLabel); + } + + const contribAnnotation = + (slots.filterEditsForWiki + ? data.annotation?.replace(/^edits for wiki(: )?/, '') + : data.annotation); + + if (contribAnnotation) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = contribAnnotation; + } + + if (empty(Object.keys(workingOptions))) { + return html.blank(); + } + + return language.$(workingCapsule, workingOptions); + }), content: language.encapsulate('artistPage.creditList.entry', capsule => @@ -68,5 +101,11 @@ export default { : data.kind === 'banner' ? language.$(capsule, 'bannerArt') : language.$(capsule, 'coverArt')))))), + + originDetails: + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }), }), }; diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js index 7987b642..c80aeab7 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js @@ -33,6 +33,11 @@ export default { type: 'html', mutable: false, }, + + originDetails: { + type: 'html', + mutable: false, + }, }, generate: (relations, slots, {html, language}) => @@ -40,52 +45,59 @@ export default { html.tag('li', slots.rerelease && {class: 'rerelease'}, - language.encapsulate(entryCapsule, workingCapsule => { - const workingOptions = {entry: slots.content}; - - if (!html.isBlank(slots.rereleaseTooltip)) { - workingCapsule += '.rerelease'; - workingOptions.rerelease = - relations.textWithTooltip.slots({ - attributes: {class: 'rerelease'}, - text: language.$(entryCapsule, 'rerelease.term'), - tooltip: slots.rereleaseTooltip, - }); - - return language.$(workingCapsule, workingOptions); - } - - if (!html.isBlank(slots.firstReleaseTooltip)) { - workingCapsule += '.firstRelease'; - workingOptions.firstRelease = - relations.textWithTooltip.slots({ - attributes: {class: 'first-release'}, - text: language.$(entryCapsule, 'firstRelease.term'), - tooltip: slots.firstReleaseTooltip, - }); - - return language.$(workingCapsule, workingOptions); - } - - let anyAccent = false; - - if (!empty(slots.otherArtistLinks)) { - anyAccent = true; - workingCapsule += '.withArtists'; - workingOptions.artists = - language.formatConjunctionList(slots.otherArtistLinks); - } - - if (!html.isBlank(slots.annotation)) { - anyAccent = true; - workingCapsule += '.withAnnotation'; - workingOptions.annotation = slots.annotation; - } - - if (anyAccent) { - return language.$(workingCapsule, workingOptions); - } else { - return slots.content; - } - }))), + html.tags([ + language.encapsulate(entryCapsule, workingCapsule => { + const workingOptions = {entry: slots.content}; + + if (!html.isBlank(slots.rereleaseTooltip)) { + workingCapsule += '.rerelease'; + workingOptions.rerelease = + relations.textWithTooltip.slots({ + attributes: {class: 'rerelease'}, + text: language.$(entryCapsule, 'rerelease.term'), + tooltip: slots.rereleaseTooltip, + }); + + return language.$(workingCapsule, workingOptions); + } + + if (!html.isBlank(slots.firstReleaseTooltip)) { + workingCapsule += '.firstRelease'; + workingOptions.firstRelease = + relations.textWithTooltip.slots({ + attributes: {class: 'first-release'}, + text: language.$(entryCapsule, 'firstRelease.term'), + tooltip: slots.firstReleaseTooltip, + }); + + return language.$(workingCapsule, workingOptions); + } + + let anyAccent = false; + + if (!empty(slots.otherArtistLinks)) { + anyAccent = true; + workingCapsule += '.withArtists'; + workingOptions.artists = + language.formatConjunctionList(slots.otherArtistLinks); + } + + if (!html.isBlank(slots.annotation)) { + anyAccent = true; + workingCapsule += '.withAnnotation'; + workingOptions.annotation = slots.annotation; + } + + if (anyAccent) { + return language.$(workingCapsule, workingOptions); + } else { + return slots.content; + } + }), + + html.tag('span', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + slots.originDetails), + ]))), }; diff --git a/src/content/dependencies/generateColorStyleRules.js b/src/content/dependencies/generateColorStyleRules.js deleted file mode 100644 index c412b8f2..00000000 --- a/src/content/dependencies/generateColorStyleRules.js +++ /dev/null @@ -1,42 +0,0 @@ -export default { - contentDependencies: ['generateColorStyleVariables'], - extraDependencies: ['html'], - - relations: (relation) => ({ - variables: - relation('generateColorStyleVariables'), - }), - - data: (color) => ({ - color: - color ?? null, - }), - - slots: { - color: { - validate: v => v.isColor, - }, - }, - - generate(data, relations, slots) { - const color = data.color ?? slots.color; - - if (!color) { - return ''; - } - - return [ - `:root {`, - ...( - relations.variables - .slots({ - color, - context: 'page-root', - mode: 'property-list', - }) - .content - .map(line => line + ';')), - `}`, - ].join('\n'); - }, -}; diff --git a/src/content/dependencies/generateColorStyleTag.js b/src/content/dependencies/generateColorStyleTag.js new file mode 100644 index 00000000..2b1a21dd --- /dev/null +++ b/src/content/dependencies/generateColorStyleTag.js @@ -0,0 +1,51 @@ +export default { + contentDependencies: ['generateColorStyleVariables', 'generateStyleTag'], + extraDependencies: ['html'], + + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + + variables: + relation('generateColorStyleVariables'), + }), + + data: (color) => ({ + color: + color ?? null, + }), + + slots: { + color: { + validate: v => v.isColor, + }, + }, + + generate(data, relations, slots, {html}) { + const color = + data.color ?? slots.color; + + if (!color) { + return html.blank(); + } + + return relations.styleTag.slots({ + attributes: [ + {class: 'color-style'}, + {'data-color': color}, + ], + + rules: [ + { + select: ':root', + declare: + relations.variables.slots({ + color, + context: 'page-root', + mode: 'declarations', + }).content, + }, + ], + }); + }, +}; diff --git a/src/content/dependencies/generateColorStyleVariables.js b/src/content/dependencies/generateColorStyleVariables.js index 5270dbe4..c872d0b6 100644 --- a/src/content/dependencies/generateColorStyleVariables.js +++ b/src/content/dependencies/generateColorStyleVariables.js @@ -18,7 +18,7 @@ export default { }, mode: { - validate: v => v.is('style', 'property-list'), + validate: v => v.is('style', 'declarations'), default: 'style', }, }, @@ -50,15 +50,15 @@ export default { `--shadow-color: ${shadow}`, ]; - let selectedProperties; + let selectedDeclarations; switch (slots.context) { case 'any-content': - selectedProperties = anyContent; + selectedDeclarations = anyContent; break; case 'image-box': - selectedProperties = [ + selectedDeclarations = [ `--primary-color: ${primary}`, `--dim-color: ${dim}`, `--deep-color: ${deep}`, @@ -67,14 +67,14 @@ export default { break; case 'page-root': - selectedProperties = [ + selectedDeclarations = [ ...anyContent, `--page-primary-color: ${primary}`, ]; break; case 'primary-only': - selectedProperties = [ + selectedDeclarations = [ `--primary-color: ${primary}`, ]; break; @@ -82,10 +82,10 @@ export default { switch (slots.mode) { case 'style': - return selectedProperties.join('; '); + return selectedDeclarations.join('; '); - case 'property-list': - return selectedProperties; + case 'declarations': + return selectedDeclarations.map(declaration => declaration + ';'); } }, }; diff --git a/src/content/dependencies/generateContentContentHeading.js b/src/content/dependencies/generateContentContentHeading.js new file mode 100644 index 00000000..314ef197 --- /dev/null +++ b/src/content/dependencies/generateContentContentHeading.js @@ -0,0 +1,39 @@ +export default { + contentDependencies: ['generateContentHeading'], + extraDependencies: ['html', 'language'], + + relations: (relation, _thing) => ({ + contentHeading: + relation('generateContentHeading'), + }), + + data: (thing) => ({ + name: + thing.name, + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + string: { + type: 'string', + }, + }, + + generate: (data, relations, slots, {html, language}) => + relations.contentHeading.slots({ + attributes: slots.attributes, + + title: + language.$(slots.string, { + thing: + html.tag('i', data.name), + }), + + stickyTitle: + language.$(slots.string, 'sticky'), + }), +}; diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index c1a23bbd..78a6103b 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -1,5 +1,6 @@ export default { contentDependencies: [ + 'generateColorStyleAttribute', 'generateCoverArtworkArtTagDetails', 'generateCoverArtworkArtistDetails', 'generateCoverArtworkOriginDetails', @@ -10,6 +11,9 @@ export default { extraDependencies: ['html'], relations: (relation, artwork) => ({ + colorStyleAttribute: + relation('generateColorStyleAttribute'), + image: relation('image', artwork), @@ -46,7 +50,8 @@ export default { alt: {type: 'string'}, color: { - validate: v => v.isColor, + validate: v => v.anyOf(v.isBoolean, v.isColor), + default: false, }, mode: { @@ -68,10 +73,7 @@ export default { generate(data, relations, slots, {html}) { const {image} = relations; - image.setSlots({ - color: slots.color ?? data.color, - alt: slots.alt, - }); + image.setSlot('alt', slots.alt); const square = (data.dimensions @@ -84,6 +86,22 @@ export default { image.setSlot('dimensions', data.dimensions); } + const attributes = html.attributes(); + + let color = null; + if (typeof slots.color === 'boolean') { + if (slots.color) { + color = data.color; + } + } else if (slots.color) { + color = slots.color; + } + + if (color) { + relations.colorStyleAttribute.setSlot('color', color); + attributes.add(relations.colorStyleAttribute); + } + return html.tags([ data.attachAbove && html.tag('div', {class: 'cover-artwork-joiner'}), @@ -96,6 +114,8 @@ export default { data.attachedArtworkIsMainArtwork && {class: 'attached-artwork-is-main-artwork'}, + attributes, + (slots.mode === 'primary' ? [ relations.image.slots({ diff --git a/src/content/dependencies/generateCoverArtworkOriginDetails.js b/src/content/dependencies/generateCoverArtworkOriginDetails.js index 3908414f..8628179e 100644 --- a/src/content/dependencies/generateCoverArtworkOriginDetails.js +++ b/src/content/dependencies/generateCoverArtworkOriginDetails.js @@ -29,6 +29,9 @@ export default { source: relation('transformContent', artwork.source), + originDetails: + relation('transformContent', artwork.originDetails), + albumLink: (query.artworkThingType === 'album' ? relation('linkAlbum', artwork.thing) @@ -146,12 +149,22 @@ export default { year: relations.datetimestamp, }); + const originDetails = + html.tag('span', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + })); + return [ artworkBy, trackArtFromAlbum, source, label, year, + originDetails, ]; })())), }; diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js index 095e43c4..ee043bfa 100644 --- a/src/content/dependencies/generateFlashInfoPage.js +++ b/src/content/dependencies/generateFlashInfoPage.js @@ -4,6 +4,7 @@ export default { contentDependencies: [ 'generateAdditionalNamesBox', 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generateContributionList', 'generateFlashActSidebar', @@ -53,6 +54,9 @@ export default { contentHeading: relation('generateContentHeading'), + contentContentHeading: + relation('generateContentContentHeading', flash), + flashActLink: relation('linkFlashAct', flash.act), @@ -70,7 +74,7 @@ export default { .map(entry => relation('generateCommentaryEntry', entry)), creditSourceEntries: - flash.commentary + flash.creditingSources .map(entry => relation('generateCommentaryEntry', entry)), }), @@ -133,11 +137,11 @@ export default { })), !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + language.encapsulate(capsule, 'readCreditingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#crediting-sources'}, language.$(capsule, 'link')), })), ])), @@ -168,20 +172,20 @@ export default { ]), html.tags([ - relations.contentHeading.clone() + relations.contentContentHeading.clone() .slots({ attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), + string: 'misc.artistCommentary', }), relations.artistCommentaryEntries, ]), html.tags([ - relations.contentHeading.clone() + relations.contentContentHeading.clone() .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), + attributes: {id: 'crediting-sources'}, + string: 'misc.creditingSources', }), relations.creditSourceEntries, diff --git a/src/content/dependencies/generateLyricsEntry.js b/src/content/dependencies/generateLyricsEntry.js index 02fd3634..0c91ce0c 100644 --- a/src/content/dependencies/generateLyricsEntry.js +++ b/src/content/dependencies/generateLyricsEntry.js @@ -17,6 +17,9 @@ export default { sourceLinks: entry.sourceURLs .map(url => relation('linkExternal', url)), + + originDetails: + relation('transformContent', entry.originDetails), }), data: (entry) => ({ @@ -75,6 +78,14 @@ export default { language.$(capsule, 'squareBracketAnnotations'), ]), + html.tag('p', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + })), + relations.content.slot('mode', 'lyrics'), ])), }; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 89fefb23..0326f415 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -3,12 +3,14 @@ import {atOffset, empty, repeat} from '#sugar'; export default { contentDependencies: [ - 'generateColorStyleRules', + 'generateColorStyleTag', 'generateFooterLocalizationLinks', 'generateImageOverlay', 'generatePageSidebar', 'generateSearchSidebarBox', + 'generateStaticURLStyleTag', 'generateStickyHeadingContainer', + 'generateWikiWallpaperStyleTag', 'transformContent', ], @@ -58,8 +60,14 @@ export default { relation('transformContent', sprawl.footerContent); } - relations.colorStyleRules = - relation('generateColorStyleRules'); + relations.colorStyleTag = + relation('generateColorStyleTag'); + + relations.staticURLStyleTag = + relation('generateStaticURLStyleTag'); + + relations.wikiWallpaperStyleTag = + relation('generateWikiWallpaperStyleTag'); relations.imageOverlay = relation('generateImageOverlay'); @@ -107,9 +115,9 @@ export default { color: {validate: v => v.isColor}, - styleRules: { - validate: v => v.sparseArrayOf(v.isHTML), - default: [], + styleTags: { + type: 'html', + mutable: false, }, mainClasses: { @@ -581,29 +589,33 @@ export default { {id: 'additional-files', string: 'additionalFiles'}, {id: 'commentary', string: 'commentary'}, {id: 'artist-commentary', string: 'artistCommentary'}, - {id: 'credit-sources', string: 'creditSources'}, + {id: 'crediting-sources', string: 'creditingSources'}, + {id: 'referencing-sources', string: 'referencingSources'}, ])), ]); - const styleRulesCSS = - html.resolve(slots.styleRules, {normalize: 'string'}); + const slottedStyleTags = + html.smush(slots.styleTags); - const fallbackBackgroundStyleRule = - (styleRulesCSS.match(/body::before[^}]*background-image:/) - ? '' - : `body::before {\n` + - ` background-image: url("${to('media.path', 'bg.jpg')}");\n` + - `}`); + const slottedWallpaperStyleTag = + slottedStyleTags.content + .find(tag => tag.attributes.has('class', 'wallpaper-style')); - const goshFrigginDarnitStyleRule = - `.image-media-link::after {\n` + - ` mask-image: url("${to('staticMisc.path', 'image.svg')}");\n` + - `}`; + const fallbackWallpaperStyleTag = + (slottedWallpaperStyleTag + ? html.blank() + : relations.wikiWallpaperStyleTag); + + const usingWallpaperStyleTag = + (slottedWallpaperStyleTag + ? slottedWallpaperStyleTag + : html.resolve(fallbackWallpaperStyleTag, {normalize: 'tag'})); const numWallpaperParts = - html.resolve(slots.styleRules, {normalize: 'string'}) - .match(/\.wallpaper-part:nth-child/g) - ?.length ?? 0; + (usingWallpaperStyleTag && + usingWallpaperStyleTag.attributes.has('data-wallpaper-mode', 'parts') + ? parseInt(usingWallpaperStyleTag.attributes.get('data-num-wallpaper-parts')) + : 0); const wallpaperPartsHTML = html.tag('div', {class: 'wallpaper-parts'}, @@ -745,14 +757,14 @@ export default { href: to('staticCSS.path', 'site.css'), }), - html.tag('style', [ - relations.colorStyleRules - .slot('color', slots.color ?? data.wikiColor), + relations.colorStyleTag + .slot('color', slots.color ?? data.wikiColor), - fallbackBackgroundStyleRule, - goshFrigginDarnitStyleRule, - slots.styleRules, - ]), + relations.staticURLStyleTag, + + fallbackWallpaperStyleTag, + + slottedStyleTags, html.tag('script', { src: to('staticLib.path', 'chroma-js/chroma.min.js'), diff --git a/src/content/dependencies/generateReferencedArtworksPage.js b/src/content/dependencies/generateReferencedArtworksPage.js index 154b4762..83451eca 100644 --- a/src/content/dependencies/generateReferencedArtworksPage.js +++ b/src/content/dependencies/generateReferencedArtworksPage.js @@ -47,7 +47,7 @@ export default { }), slots: { - styleRules: {type: 'html', mutable: false}, + styleTags: {type: 'html', mutable: false}, title: {type: 'html', mutable: false}, @@ -62,7 +62,7 @@ export default { subtitle: language.$(pageCapsule, 'subtitle'), color: data.color, - styleRules: slots.styleRules, + styleTags: slots.styleTags, artworkColumnContent: relations.cover.slots({ diff --git a/src/content/dependencies/generateReferencingArtworksPage.js b/src/content/dependencies/generateReferencingArtworksPage.js index 55977b37..e97b01f8 100644 --- a/src/content/dependencies/generateReferencingArtworksPage.js +++ b/src/content/dependencies/generateReferencingArtworksPage.js @@ -47,7 +47,7 @@ export default { }), slots: { - styleRules: {type: 'html', mutable: false}, + styleTags: {type: 'html', mutable: false}, title: {type: 'html', mutable: false}, @@ -62,7 +62,7 @@ export default { subtitle: language.$(pageCapsule, 'subtitle'), color: data.color, - styleRules: slots.styleRules, + styleTags: slots.styleTags, artworkColumnContent: relations.cover.slots({ diff --git a/src/content/dependencies/generateReleaseInfoListenLine.js b/src/content/dependencies/generateReleaseInfoListenLine.js new file mode 100644 index 00000000..f2a6dd29 --- /dev/null +++ b/src/content/dependencies/generateReleaseInfoListenLine.js @@ -0,0 +1,150 @@ +import {isExternalLinkContext} from '#external-links'; +import {empty, stitchArrays, unique} from '#sugar'; + +function getReleaseContext(urlString, { + _artistURLs, + albumArtistURLs, +}) { + const composerBandcampDomains = + albumArtistURLs + .filter(url => url.hostname.endsWith('.bandcamp.com')) + .map(url => url.hostname); + + const url = new URL(urlString); + + if (url.hostname === 'homestuck.bandcamp.com') { + return 'officialRelease'; + } + + if (composerBandcampDomains.includes(url.hostname)) { + return 'composerRelease'; + } + + return null; +} + +export default { + contentDependencies: ['linkExternal'], + extraDependencies: ['html', 'language'], + + query(thing) { + const query = {}; + + query.album = + (thing.album + ? thing.album + : thing); + + query.artists = + thing.artistContribs + .map(contrib => contrib.artist); + + query.artistGroups = + query.artists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + query.albumArtists = + query.album.artistContribs + .map(contrib => contrib.artist); + + query.albumArtistGroups = + query.albumArtists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + return query; + }, + + relations: (relation, _query, thing) => ({ + links: + thing.urls.map(url => relation('linkExternal', url)), + }), + + data(query, thing) { + const data = {}; + + data.name = thing.name; + + const artistURLs = + unique([ + ...query.artists.flatMap(artist => artist.urls), + ...query.artistGroups.flatMap(group => group.urls), + ]).map(url => new URL(url)); + + const albumArtistURLs = + unique([ + ...query.albumArtists.flatMap(artist => artist.urls), + ...query.albumArtistGroups.flatMap(group => group.urls), + ]).map(url => new URL(url)); + + const boundGetReleaseContext = urlString => + getReleaseContext(urlString, { + artistURLs, + albumArtistURLs, + }); + + let releaseContexts = + thing.urls.map(boundGetReleaseContext); + + const albumReleaseContexts = + query.album.urls.map(boundGetReleaseContext); + + const presentReleaseContexts = + unique(releaseContexts.filter(Boolean)); + + const presentAlbumReleaseContexts = + unique(albumReleaseContexts.filter(Boolean)); + + if ( + presentReleaseContexts.length <= 1 && + presentAlbumReleaseContexts.length <= 1 + ) { + releaseContexts = + thing.urls.map(() => null); + } + + data.releaseContexts = releaseContexts; + + return data; + }, + + slots: { + visibleWithoutLinks: { + type: 'boolean', + default: false, + }, + + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('releaseInfo.listenOn', capsule => + (empty(relations.links) && slots.visibleWithoutLinks + ? language.$(capsule, 'noLinks', { + name: + html.tag('i', data.name), + }) + + : language.$('releaseInfo.listenOn', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatDisjunctionList( + stitchArrays({ + link: relations.links, + releaseContext: data.releaseContexts, + }).map(({link, releaseContext}) => + link.slot('context', [ + ... + (Array.isArray(slots.context) + ? slots.context + : [slots.context]), + + releaseContext, + ]))), + }))), +}; diff --git a/src/content/dependencies/generateStaticPage.js b/src/content/dependencies/generateStaticPage.js index 226152c7..931352b4 100644 --- a/src/content/dependencies/generateStaticPage.js +++ b/src/content/dependencies/generateStaticPage.js @@ -23,17 +23,19 @@ export default { title: data.name, headingMode: 'sticky', - styleRules: - (data.stylesheet - ? [data.stylesheet] - : []), + styleTags: [ + html.tag('style', {class: 'static-page-style'}, + {[html.onlyIfContent]: true}, + data.stylesheet), + ], mainClasses: ['long-content'], mainContent: [ relations.content, - data.script && - html.tag('script', data.script), + html.tag('script', + {[html.onlyIfContent]: true}, + data.script), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateStaticURLStyleTag.js b/src/content/dependencies/generateStaticURLStyleTag.js new file mode 100644 index 00000000..b927e5d6 --- /dev/null +++ b/src/content/dependencies/generateStaticURLStyleTag.js @@ -0,0 +1,23 @@ +export default { + contentDependencies: ['generateStyleTag'], + extraDependencies: ['to'], + + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + }), + + generate: (relations, {to}) => + relations.styleTag.slots({ + attributes: {class: 'static-url-style'}, + + rules: [ + { + select: '.image-media-link::after', + declare: [ + `mask-image: url("${to('staticMisc.path', 'image.svg')}");` + ], + }, + ], + }), +}; diff --git a/src/content/dependencies/generateStyleTag.js b/src/content/dependencies/generateStyleTag.js new file mode 100644 index 00000000..5ed09ae5 --- /dev/null +++ b/src/content/dependencies/generateStyleTag.js @@ -0,0 +1,48 @@ +import {empty} from '#sugar'; + +const indent = text => + text + .split('\n') + .map(line => ' '.repeat(4) + line) + .join('\n'); + +export default { + extraDependencies: ['html'], + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + rules: { + validate: v => + v.looseArrayOf( + v.validateProperties({ + select: v.isString, + declare: v.looseArrayOf(v.isString), + })), + }, + }, + + generate: (slots, {html}) => + html.tag('style', slots.attributes, + {[html.onlyIfContent]: true}, + + slots.rules + .filter(Boolean) + + .map(rule => ({ + select: rule.select, + declare: rule.declare.filter(Boolean), + })) + + .filter(rule => !empty(rule.declare)) + + .map(rule => + `${rule.select} {\n` + + indent(rule.declare.join('\n')) + '\n' + + `}`) + + .join('\n\n')), +}; diff --git a/src/content/dependencies/generateTrackArtistCommentarySection.js b/src/content/dependencies/generateTrackArtistCommentarySection.js index e3041d3a..c7e7f0f8 100644 --- a/src/content/dependencies/generateTrackArtistCommentarySection.js +++ b/src/content/dependencies/generateTrackArtistCommentarySection.js @@ -2,8 +2,8 @@ import {empty, stitchArrays} from '#sugar'; export default { contentDependencies: [ + 'generateContentContentHeading', 'generateCommentaryEntry', - 'generateContentHeading', 'linkAlbum', 'linkTrack', ], @@ -18,8 +18,8 @@ export default { }), relations: (relation, query, track) => ({ - contentHeading: - relation('generateContentHeading'), + contentContentHeading: + relation('generateContentContentHeading', track), mainReleaseTrackLink: (track.isSecondaryRelease @@ -78,54 +78,44 @@ export default { generate: (data, relations, {html, language}) => language.encapsulate('misc.artistCommentary', capsule => html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), - }), + relations.contentContentHeading.slots({ + attributes: {id: 'artist-commentary'}, + string: 'misc.artistCommentary', + }), + + relations.artistCommentaryEntries, data.isSecondaryRelease && - html.tags([ - html.tag('p', {class: ['drop', 'commentary-drop']}, - {[html.onlyIfSiblings]: true}, - - language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => { - const workingOptions = {}; - - workingOptions.album = - relations.mainReleaseTrackLink.slots({ - content: - data.mainReleaseAlbumName, - - color: - data.mainReleaseAlbumColor, - }); - - if (data.name !== data.mainReleaseName) { - workingCapsule += '.namedDifferently'; - workingOptions.name = - html.tag('i', data.mainReleaseName); - } - - return language.$(workingCapsule, workingOptions); - })), - - relations.mainReleaseArtistCommentaryEntries, - ]), - - html.tags([ - data.isSecondaryRelease && - !html.isBlank(relations.mainReleaseArtistCommentaryEntries) && - html.tag('p', {class: ['drop', 'commentary-drop']}, - {[html.onlyIfSiblings]: true}, - - language.$(capsule, 'info.releaseSpecific', { - album: - relations.thisReleaseAlbumLink, - })), - - relations.artistCommentaryEntries, - ]), + html.tag('div', {class: 'inherited-commentary-section'}, + {[html.onlyIfContent]: true}, + + [ + html.tag('p', {class: ['drop', 'commentary-drop']}, + {[html.onlyIfSiblings]: true}, + + language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => { + const workingOptions = {}; + + workingOptions.album = + relations.mainReleaseTrackLink.slots({ + content: + data.mainReleaseAlbumName, + + color: + data.mainReleaseAlbumColor, + }); + + if (data.name !== data.mainReleaseName) { + workingCapsule += '.namedDifferently'; + workingOptions.name = + html.tag('i', data.mainReleaseName); + } + + return language.$(workingCapsule, workingOptions); + })), + + relations.mainReleaseArtistCommentaryEntries, + ]), html.tag('p', {class: ['drop', 'commentary-drop']}, {[html.onlyIfContent]: true}, diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index ab6ea1cb..6c16ce27 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -5,8 +5,9 @@ export default { 'generateAlbumNavAccent', 'generateAlbumSecondaryNav', 'generateAlbumSidebar', - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generateContributionList', 'generateLyricsSection', @@ -38,8 +39,8 @@ export default { layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), socialEmbed: relation('generateTrackSocialEmbed', track), @@ -65,6 +66,9 @@ export default { contentHeading: relation('generateContentHeading'), + contentContentHeading: + relation('generateContentContentHeading', track), + releaseInfo: relation('generateTrackReleaseInfo', track), @@ -106,8 +110,12 @@ export default { artistCommentarySection: relation('generateTrackArtistCommentarySection', track), - creditSourceEntries: - track.creditSources + creditingSourceEntries: + track.creditingSources + .map(entry => relation('generateCommentaryEntry', entry)), + + referencingSourceEntries: + track.referencingSources .map(entry => relation('generateCommentaryEntry', entry)), }), @@ -132,7 +140,7 @@ export default { additionalNames: relations.additionalNamesBox, color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, artworkColumnContent: relations.artworkColumn, @@ -181,12 +189,21 @@ export default { language.$(capsule, 'link')), })), - !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + !html.isBlank(relations.creditingSourceEntries) && + language.encapsulate(capsule, 'readCreditingSources', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#crediting-sources'}, + language.$(capsule, 'link')), + })), + + !html.isBlank(relations.referencingSourceEntries) && + language.encapsulate(capsule, 'readReferencingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#referencing-sources'}, language.$(capsule, 'link')), })), ])), @@ -338,13 +355,23 @@ export default { relations.artistCommentarySection, html.tags([ - relations.contentHeading.clone() + relations.contentContentHeading.clone() + .slots({ + attributes: {id: 'crediting-sources'}, + string: 'misc.creditingSources', + }), + + relations.creditingSourceEntries, + ]), + + html.tags([ + relations.contentContentHeading.clone() .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), + attributes: {id: 'referencing-sources'}, + string: 'misc.referencingSources', }), - relations.creditSourceEntries, + relations.referencingSourceEntries, ]), ], diff --git a/src/content/dependencies/generateTrackReferencedArtworksPage.js b/src/content/dependencies/generateTrackReferencedArtworksPage.js index 93438c5b..7073409e 100644 --- a/src/content/dependencies/generateTrackReferencedArtworksPage.js +++ b/src/content/dependencies/generateTrackReferencedArtworksPage.js @@ -1,6 +1,6 @@ export default { contentDependencies: [ - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateBackToTrackLink', 'generateReferencedArtworksPage', 'generateTrackNavLinks', @@ -12,8 +12,8 @@ export default { page: relation('generateReferencedArtworksPage', track.trackArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), navLinks: relation('generateTrackNavLinks', track), @@ -35,7 +35,7 @@ export default { data.name, }), - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, navLinks: html.resolve( diff --git a/src/content/dependencies/generateTrackReferencingArtworksPage.js b/src/content/dependencies/generateTrackReferencingArtworksPage.js index e9818bad..a45144c8 100644 --- a/src/content/dependencies/generateTrackReferencingArtworksPage.js +++ b/src/content/dependencies/generateTrackReferencingArtworksPage.js @@ -1,6 +1,6 @@ export default { contentDependencies: [ - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateBackToTrackLink', 'generateReferencingArtworksPage', 'generateTrackNavLinks', @@ -12,8 +12,8 @@ export default { page: relation('generateReferencingArtworksPage', track.trackArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), navLinks: relation('generateTrackNavLinks', track), @@ -35,7 +35,7 @@ export default { data.name, }), - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, navLinks: html.resolve( diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js index 54e462c7..3298dcc4 100644 --- a/src/content/dependencies/generateTrackReleaseInfo.js +++ b/src/content/dependencies/generateTrackReleaseInfo.js @@ -1,9 +1,7 @@ -import {empty} from '#sugar'; - export default { contentDependencies: [ 'generateReleaseInfoContributionsLine', - 'linkExternal', + 'generateReleaseInfoListenLine', ], extraDependencies: ['html', 'language'], @@ -11,14 +9,11 @@ export default { relations(relation, track) { const relations = {}; - relations.artistContributionLinks = + relations.artistContributionsLine = relation('generateReleaseInfoContributionsLine', track.artistContribs); - if (!empty(track.urls)) { - relations.externalLinks = - track.urls.map(url => - relation('linkExternal', url)); - } + relations.listenLine = + relation('generateReleaseInfoListenLine', track); return relations; }, @@ -48,7 +43,7 @@ export default { {[html.joinChildren]: html.tag('br')}, [ - relations.artistContributionLinks.slots({ + relations.artistContributionsLine.slots({ stringKey: capsule + '.by', featuringStringKey: capsule + '.by.featuring', chronologyKind: 'track', @@ -66,17 +61,9 @@ export default { ]), html.tag('p', - language.encapsulate(capsule, 'listenOn', capsule => - (relations.externalLinks - ? language.$(capsule, { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => link.slot('context', 'track'))), - }) - : language.$(capsule, 'noLinks', { - name: - html.tag('i', data.name), - })))), + relations.listenLine.slots({ + visibleWithoutLinks: true, + context: ['track'], + })), ])), }; diff --git a/src/content/dependencies/generateWallpaperStyleTag.js b/src/content/dependencies/generateWallpaperStyleTag.js new file mode 100644 index 00000000..bf094300 --- /dev/null +++ b/src/content/dependencies/generateWallpaperStyleTag.js @@ -0,0 +1,80 @@ +import {empty, stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['generateStyleTag'], + extraDependencies: ['html', 'to'], + + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + }), + + slots: { + singleWallpaperPath: { + validate: v => v.strictArrayOf(v.isString), + }, + + singleWallpaperStyle: { + validate: v => v.isString, + }, + + wallpaperPartPaths: { + validate: v => + v.strictArrayOf(v.optional(v.strictArrayOf(v.isString))), + }, + + wallpaperPartStyles: { + validate: v => + v.strictArrayOf(v.optional(v.isString)), + }, + }, + + generate(relations, slots, {html, to}) { + const attributes = html.attributes(); + const rules = []; + + attributes.add('class', 'wallpaper-style'); + + if (empty(slots.wallpaperPartPaths)) { + attributes.set('data-wallpaper-mode', 'one'); + + rules.push({ + select: 'body::before', + declare: [ + `background-image: url("${to(...slots.singleWallpaperPath)}");`, + slots.singleWallpaperStyle, + ], + }); + } else { + attributes.set('data-wallpaper-mode', 'parts'); + attributes.set('data-num-wallpaper-parts', slots.wallpaperPartPaths.length); + + stitchArrays({ + path: slots.wallpaperPartPaths, + style: slots.wallpaperPartStyles, + }).forEach(({path, style}, index) => { + rules.push({ + select: `.wallpaper-part:nth-child(${index + 1})`, + declare: [ + path && `background-image: url("${to(...path)}");`, + style, + ], + }); + }); + + rules.push({ + select: 'body::before', + declare: [ + 'display: none;', + ], + }); + } + + relations.styleTag.setSlots({ + attributes, + rules, + }); + + return relations.styleTag; + }, +}; diff --git a/src/content/dependencies/generateWikiWallpaperStyleTag.js b/src/content/dependencies/generateWikiWallpaperStyleTag.js new file mode 100644 index 00000000..12d27304 --- /dev/null +++ b/src/content/dependencies/generateWikiWallpaperStyleTag.js @@ -0,0 +1,38 @@ +export default { + contentDependencies: ['generateWallpaperStyleTag'], + extraDependencies: ['wikiData'], + + sprawl: ({wikiInfo}) => ({wikiInfo}), + + relations: (relation) => ({ + wallpaperStyleTag: + relation('generateWallpaperStyleTag'), + }), + + data: ({wikiInfo}) => ({ + singleWallpaperPath: [ + 'media.path', + 'bg.' + wikiInfo.wikiWallpaperFileExtension, + ], + + singleWallpaperStyle: + wikiInfo.wikiWallpaperStyle, + + wallpaperPartPaths: + wikiInfo.wikiWallpaperParts.map(part => + (part.asset + ? ['media.path', part.asset] + : null)), + + wallpaperPartStyles: + wikiInfo.wikiWallpaperParts.map(part => part.style), + }), + + generate: (data, relations) => + relations.wallpaperStyleTag.slots({ + singleWallpaperPath: data.singleWallpaperPath, + singleWallpaperStyle: data.singleWallpaperStyle, + wallpaperPartPaths: data.wallpaperPartPaths, + wallpaperPartStyles: data.wallpaperPartStyles, + }), +}; diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index a5009804..25d7324f 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -11,6 +11,11 @@ import {colors, logWarn} from '#cli'; import contentFunction, {ContentFunctionSpecError} from '#content-function'; import {annotateFunction} from '#sugar'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const codeSrcPath = path.resolve(__dirname, '..'); +const codeRootPath = path.resolve(codeSrcPath, '..'); + function cachebust(filePath) { if (filePath in cachebust.cache) { cachebust.cache[filePath] += 1; @@ -42,7 +47,9 @@ export function watchContentDependencies({ close, }); - const eslint = new ESLint(); + const eslint = new ESLint({ + cwd: codeRootPath, + }); const metaPath = fileURLToPath(import.meta.url); const metaDirname = path.dirname(metaPath); diff --git a/src/content/dependencies/linkExternal.js b/src/content/dependencies/linkExternal.js index c132baaf..45c08a08 100644 --- a/src/content/dependencies/linkExternal.js +++ b/src/content/dependencies/linkExternal.js @@ -55,7 +55,7 @@ export default { try { new URL(data.url); urlIsValid = true; - } catch (error) { + } catch { urlIsValid = false; } diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js index 41944959..99f19764 100644 --- a/src/content/dependencies/listArtistsByContributions.js +++ b/src/content/dependencies/listArtistsByContributions.js @@ -1,13 +1,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; - -import { - accumulateSum, - empty, - filterByCount, - filterMultipleArrays, - stitchArrays, - unique, -} from '#sugar'; +import {empty, filterByCount, filterMultipleArrays, stitchArrays} + from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], @@ -41,37 +34,46 @@ export default { query[countsKey] = counts; }; + const countContributions = (artist, keys) => { + const contribs = + keys + .flatMap(key => artist[key]) + .filter(contrib => contrib.countInContributionTotals); + + const things = + new Set(contribs.map(contrib => contrib.thing)); + + return things.size; + }; + queryContributionInfo( 'artistsByTrackContributions', 'countsByTrackContributions', artist => - (unique( - ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - ]).flat() - .map(({thing}) => thing) - )).length); + countContributions(artist, [ + 'trackArtistContributions', + 'trackContributorContributions', + ])); queryContributionInfo( 'artistsByArtworkContributions', 'countsByArtworkContributions', artist => - accumulateSum( - [ - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, - artist.trackCoverArtistContributions, - ], - contribs => contribs.length)); + countContributions(artist, [ + 'albumCoverArtistContributions', + 'albumWallpaperArtistContributions', + 'albumBannerArtistContributions', + 'trackCoverArtistContributions', + ])); if (sprawl.enableFlashesAndGames) { queryContributionInfo( 'artistsByFlashContributions', 'countsByFlashContributions', artist => - artist.flashContributorContributions.length); + countContributions(artist, [ + 'flashContributorContributions', + ])); } return query; diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index fcdc3aa4..e9a75744 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -1,5 +1,6 @@ import {basename} from 'node:path'; +import {logWarn} from '#cli'; import {bindFind} from '#find'; import {replacerSpec, parseContentNodes} from '#replacer'; @@ -62,20 +63,30 @@ export default { Object.values(replacerSpec) .map(description => description.link) .filter(Boolean)), + 'image', 'generateTextWithTooltip', 'generateTooltip', 'linkExternal', ], - extraDependencies: ['html', 'language', 'to', 'wikiData'], + extraDependencies: [ + 'html', + 'language', + 'niceShowAggregate', + 'to', + 'wikiData', + ], sprawl(wikiData, content) { - const find = bindFind(wikiData); + const find = bindFind(wikiData, {mode: 'quiet'}); - const parsedNodes = parseContentNodes(content ?? ''); + const {result: parsedNodes, error} = + parseContentNodes(content ?? '', {errorMode: 'return'}); return { + error, + nodes: parsedNodes .map(node => { if (node.type !== 'tag') { @@ -189,6 +200,9 @@ export default { return { content, + error: + sprawl.error, + nodes: sprawl.nodes .map(node => { @@ -301,7 +315,12 @@ export default { }, }, - generate(data, relations, slots, {html, language, to}) { + generate(data, relations, slots, {html, language, niceShowAggregate, to}) { + if (data.error) { + logWarn`Error in content text.`; + niceShowAggregate(data.error); + } + let imageIndex = 0; let internalLinkIndex = 0; let externalLinkIndex = 0; @@ -360,9 +379,8 @@ export default { height && {height}, style && {style}, - align === 'center' && - !link && - {class: 'align-center'}, + align && !link && + {class: 'align-' + align}, pixelate && {class: 'pixelate'}); @@ -373,8 +391,8 @@ export default { {href: link}, {target: '_blank'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, {title: language.encapsulate('misc.external.opensInNewTab', capsule => @@ -424,8 +442,8 @@ export default { inline: false, data: html.tag('div', {class: 'content-image-container'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, image), }; @@ -437,22 +455,31 @@ export default { ? to('media.path', node.src.slice('media/'.length)) : node.src); - const {width, height, align, pixelate} = node; + const {width, height, align, inline, pixelate} = node; - const content = - html.tag('div', {class: 'content-video-container'}, - align === 'center' && - {class: 'align-center'}, + const video = + html.tag('video', + src && {src}, + width && {width}, + height && {height}, - html.tag('video', - src && {src}, - width && {width}, - height && {height}, + {controls: true}, - {controls: true}, + align && inline && + {class: 'align-' + align}, + + pixelate && + {class: 'pixelate'}); + + const content = + (inline + ? video + : html.tag('div', {class: 'content-video-container'}, + align && + {class: 'align-' + align}, + + video)); - pixelate && - {class: 'pixelate'})); return { type: 'processed-video', @@ -466,15 +493,14 @@ export default { ? to('media.path', node.src.slice('media/'.length)) : node.src); - const {align, inline} = node; + const {align, inline, nameless} = node; const audio = html.tag('audio', src && {src}, - align === 'center' && - inline && - {class: 'align-center'}, + align && inline && + {class: 'align-' + align}, {controls: true}); @@ -482,13 +508,14 @@ export default { (inline ? audio : html.tag('div', {class: 'content-audio-container'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, [ - html.tag('a', {class: 'filename'}, - src && {href: src}, - language.sanitize(basename(node.src))), + !nameless && + html.tag('a', {class: 'filename'}, + src && {href: src}, + language.sanitize(basename(node.src))), audio, ])); @@ -537,7 +564,7 @@ export default { try { link.getSlotDescription('preferShortName'); hasPreferShortNameSlot = true; - } catch (error) { + } catch { hasPreferShortNameSlot = false; } @@ -550,7 +577,7 @@ export default { try { link.getSlotDescription('tooltipStyle'); hasTooltipStyleSlot = true; - } catch (error) { + } catch { hasTooltipStyleSlot = false; } diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js index a089e325..651a61cf 100644 --- a/src/data/cacheable-object.js +++ b/src/data/cacheable-object.js @@ -243,13 +243,13 @@ export class CacheableObjectPropertyValueError extends Error { try { inspectOldValue = inspect(oldValue); - } catch (error) { + } catch { inspectOldValue = colors.red(`(couldn't inspect)`); } try { inspectNewValue = inspect(newValue); - } catch (error) { + } catch { inspectNewValue = colors.red(`(couldn't inspect)`); } diff --git a/src/data/checks.js b/src/data/checks.js index 075f929f..3fcb6d3b 100644 --- a/src/data/checks.js +++ b/src/data/checks.js @@ -185,16 +185,20 @@ export function filterReferenceErrors(wikiData, { artTags: '_artTag', referencedArtworks: '_artwork', commentary: '_content', - creditSources: '_content', + creditingSources: '_content', }], ['artTagData', { directDescendantArtTags: 'artTag', }], + ['artworkData', { + referencedArtworks: '_artwork', + }], + ['flashData', { commentary: '_content', - creditSources: '_content', + creditingSources: '_content', }], ['groupCategoryData', { @@ -235,7 +239,8 @@ export function filterReferenceErrors(wikiData, { referencedArtworks: '_artwork', mainReleaseTrack: '_trackMainReleasesOnly', commentary: '_content', - creditSources: '_content', + creditingSources: '_content', + referencingSources: '_content', lyrics: '_content', }], @@ -380,7 +385,7 @@ export function filterReferenceErrors(wikiData, { break; } - const suppress = fn => conditionallySuppressError(error => { + const suppress = fn => conditionallySuppressError(_error => { // We're not suppressing any errors at the moment. // An old suppression is kept below for reference. @@ -552,6 +557,11 @@ export function reportContentTextErrors(wikiData, { description: 'description', }; + const artworkShape = { + source: 'artwork source', + originDetails: 'artwork origin details', + }; + const commentaryShape = { body: 'commentary body', artistText: 'commentary artist text', @@ -568,6 +578,8 @@ export function reportContentTextErrors(wikiData, { ['albumData', { additionalFiles: additionalFileShape, commentary: commentaryShape, + creditingSources: commentaryShape, + coverArtworks: artworkShape, }], ['artTagData', { @@ -580,6 +592,8 @@ export function reportContentTextErrors(wikiData, { ['flashData', { commentary: commentaryShape, + creditingSources: commentaryShape, + coverArtwork: artworkShape, }], ['flashActData', { @@ -609,10 +623,12 @@ export function reportContentTextErrors(wikiData, { ['trackData', { additionalFiles: additionalFileShape, commentary: commentaryShape, - creditSources: commentaryShape, + creditingSources: commentaryShape, + referencingSources: commentaryShape, lyrics: lyricsShape, midiProjectFiles: additionalFileShape, sheetMusicFiles: additionalFileShape, + trackArtworks: artworkShape, }], ['wikiInfo', { @@ -682,7 +698,7 @@ export function reportContentTextErrors(wikiData, { } else if (node.type === 'external-link') { try { new URL(node.data.href); - } catch (error) { + } catch { yield { index, length, message: @@ -753,6 +769,31 @@ export function reportContentTextErrors(wikiData, { const topMessage = `Content text errors` + fieldPropertyMessage; + const checkShapeEntries = (entry, callProcessContentOpts) => { + for (const [key, annotation] of Object.entries(shape)) { + const value = entry[key]; + + // TODO: This should be an undefined/null check, like above, + // but it's not, because sometimes the stuff we're checking + // here isn't actually coded as a Thing - so the properties + // might really be undefined instead of null. Terrifying and + // awful. And most of all, citation needed. + if (!value) continue; + + callProcessContent({ + ...callProcessContentOpts, + + // TODO: `nest` isn't provided by `callProcessContentOpts` + //`but `push` is - this is to match the old code, but + // what's the deal here? + nest, + + value, + message: `Error in ${colors.green(annotation)}`, + }); + } + }; + if (shape === '_content') { callProcessContent({ nest, @@ -760,26 +801,18 @@ export function reportContentTextErrors(wikiData, { value, message: topMessage, }); - } else { + } else if (Array.isArray(value)) { nest({message: topMessage}, ({push}) => { for (const [index, entry] of value.entries()) { - for (const [key, annotation] of Object.entries(shape)) { - const value = entry[key]; - - // TODO: Should this check undefined/null similar to above? - if (!value) continue; - - callProcessContent({ - nest, - push, - value, - message: `Error in ${colors.green(annotation)}`, - annotateError: error => - annotateErrorWithIndex(error, index), - }); - } + checkShapeEntries(entry, { + push, + annotateError: error => + annotateErrorWithIndex(error, index), + }); } }); + } else { + checkShapeEntries(value, {push}); } } }); diff --git a/src/data/composite/data/withLengthOfList.js b/src/data/composite/data/withLengthOfList.js index efc3ecae..e67aa887 100644 --- a/src/data/composite/data/withLengthOfList.js +++ b/src/data/composite/data/withLengthOfList.js @@ -1,5 +1,4 @@ import {input, templateCompositeFrom} from '#composite'; -import {stitchArrays} from '#sugar'; function getOutputName({ [input.staticDependency('list')]: list, diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js index dfc6864f..de1d37c3 100644 --- a/src/data/composite/things/album/index.js +++ b/src/data/composite/things/album/index.js @@ -1,2 +1,2 @@ -export {default as withHasCoverArt} from './withHasCoverArt.js'; +export {default as withCoverArtDate} from './withCoverArtDate.js'; export {default as withTracks} from './withTracks.js'; diff --git a/src/data/composite/wiki-data/withCoverArtDate.js b/src/data/composite/things/album/withCoverArtDate.js index a114d5ff..978f566a 100644 --- a/src/data/composite/wiki-data/withCoverArtDate.js +++ b/src/data/composite/things/album/withCoverArtDate.js @@ -2,8 +2,7 @@ import {input, templateCompositeFrom} from '#composite'; import {isDate} from '#validators'; import {raiseOutputWithoutDependency} from '#composite/control-flow'; - -import withResolvedContribs from './withResolvedContribs.js'; +import {withHasArtwork} from '#composite/wiki-data'; export default templateCompositeFrom({ annotation: `withCoverArtDate`, @@ -19,14 +18,14 @@ export default templateCompositeFrom({ outputs: ['#coverArtDate'], steps: () => [ - withResolvedContribs({ - from: 'coverArtistContribs', - date: input.value(null), + withHasArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', }), raiseOutputWithoutDependency({ - dependency: '#resolvedContribs', - mode: input.value('empty'), + dependency: '#hasArtwork', + mode: input.value('falsy'), output: input.value({'#coverArtDate': null}), }), diff --git a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js index 36abb3fe..e9425c95 100644 --- a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js +++ b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js @@ -1,7 +1,6 @@ import {input, templateCompositeFrom} from '#composite'; import {raiseOutputWithoutDependency} from '#composite/control-flow'; -import {withPropertyFromObject} from '#composite/data'; import {withRecontextualizedContributionList} from '#composite/wiki-data'; import withPropertyFromAttachedArtwork from './withPropertyFromAttachedArtwork.js'; diff --git a/src/data/composite/things/content/withAnnotationParts.js b/src/data/composite/things/content/withAnnotationParts.js index 5eb8e3d5..6311b57a 100644 --- a/src/data/composite/things/content/withAnnotationParts.js +++ b/src/data/composite/things/content/withAnnotationParts.js @@ -1,5 +1,4 @@ import {input, templateCompositeFrom} from '#composite'; -import {parseContentNodes} from '#replacer'; import {transposeArrays} from '#sugar'; import {is} from '#validators'; diff --git a/src/data/composite/things/content/withSourceText.js b/src/data/composite/things/content/withSourceText.js index d310e8ea..292306b7 100644 --- a/src/data/composite/things/content/withSourceText.js +++ b/src/data/composite/things/content/withSourceText.js @@ -1,5 +1,4 @@ import {input, templateCompositeFrom} from '#composite'; -import {parseContentNodes} from '#replacer'; import {raiseOutputWithoutDependency} from '#composite/control-flow'; diff --git a/src/data/composite/things/content/withSourceURLs.js b/src/data/composite/things/content/withSourceURLs.js index f1e8dbc0..f85ff9ea 100644 --- a/src/data/composite/things/content/withSourceURLs.js +++ b/src/data/composite/things/content/withSourceURLs.js @@ -1,5 +1,4 @@ import {input, templateCompositeFrom} from '#composite'; -import {parseContentNodes} from '#replacer'; import {raiseOutputWithoutDependency} from '#composite/control-flow'; import {withFilteredList, withMappedList} from '#composite/data'; diff --git a/src/data/composite/things/contribution/thingPropertyMatches.js b/src/data/composite/things/contribution/thingPropertyMatches.js index 1e9019b8..a678c3f5 100644 --- a/src/data/composite/things/contribution/thingPropertyMatches.js +++ b/src/data/composite/things/contribution/thingPropertyMatches.js @@ -1,7 +1,6 @@ import {input, templateCompositeFrom} from '#composite'; import {exitWithoutDependency} from '#composite/control-flow'; -import {withPropertyFromObject} from '#composite/data'; export default templateCompositeFrom({ annotation: `thingPropertyMatches`, diff --git a/src/data/composite/things/track-section/withContinueCountingFrom.js b/src/data/composite/things/track-section/withContinueCountingFrom.js index e034b7a5..0ca52b6c 100644 --- a/src/data/composite/things/track-section/withContinueCountingFrom.js +++ b/src/data/composite/things/track-section/withContinueCountingFrom.js @@ -1,4 +1,4 @@ -import {input, templateCompositeFrom} from '#composite'; +import {templateCompositeFrom} from '#composite'; import withStartCountingFrom from './withStartCountingFrom.js'; diff --git a/src/data/composite/things/track/withAllReleases.js b/src/data/composite/things/track/withAllReleases.js index b93bf753..891db102 100644 --- a/src/data/composite/things/track/withAllReleases.js +++ b/src/data/composite/things/track/withAllReleases.js @@ -8,7 +8,6 @@ import {input, templateCompositeFrom} from '#composite'; import {sortByDate} from '#sort'; -import {exitWithoutDependency} from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; import withMainRelease from './withMainRelease.js'; diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index 60faeaf4..87edf21e 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -9,7 +9,6 @@ import {isBoolean} from '#validators'; import {withPropertyFromObject} from '#composite/data'; import {withResolvedReference} from '#composite/wiki-data'; -import {soupyFind} from '#composite/wiki-properties'; import { exitWithoutDependency, diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js index 0639742f..bb3e8983 100644 --- a/src/data/composite/things/track/withOtherReleases.js +++ b/src/data/composite/things/track/withOtherReleases.js @@ -3,9 +3,6 @@ import {input, templateCompositeFrom} from '#composite'; -import {exitWithoutDependency} from '#composite/control-flow'; -import {withPropertyFromObject} from '#composite/data'; - import withAllReleases from './withAllReleases.js'; export default templateCompositeFrom({ diff --git a/src/data/composite/wiki-data/exitWithoutArtwork.js b/src/data/composite/wiki-data/exitWithoutArtwork.js new file mode 100644 index 00000000..8e799fda --- /dev/null +++ b/src/data/composite/wiki-data/exitWithoutArtwork.js @@ -0,0 +1,45 @@ +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList, isThing, strictArrayOf} from '#validators'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import withHasArtwork from './withHasArtwork.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutArtwork`, + + inputs: { + contribs: input({ + validate: isContributionList, + defaultValue: null, + }), + + artwork: input({ + validate: isThing, + defaultValue: null, + }), + + artworks: input({ + validate: strictArrayOf(isThing), + defaultValue: null, + }), + + value: input({ + defaultValue: null, + }), + }, + + steps: () => [ + withHasArtwork({ + contribs: input('contribs'), + artwork: input('artwork'), + artworks: input('artworks'), + }), + + exitWithoutDependency({ + dependency: '#hasArtwork', + mode: input.value('falsy'), + value: input('value'), + }), + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js index 38afc2ac..3206575b 100644 --- a/src/data/composite/wiki-data/index.js +++ b/src/data/composite/wiki-data/index.js @@ -5,6 +5,7 @@ // export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as exitWithoutArtwork} from './exitWithoutArtwork.js'; export {default as gobbleSoupyFind} from './gobbleSoupyFind.js'; export {default as gobbleSoupyReverse} from './gobbleSoupyReverse.js'; export {default as inputNotFoundMode} from './inputNotFoundMode.js'; @@ -16,8 +17,8 @@ export {default as withClonedThings} from './withClonedThings.js'; export {default as withConstitutedArtwork} from './withConstitutedArtwork.js'; export {default as withContentNodes} from './withContentNodes.js'; export {default as withContributionListSums} from './withContributionListSums.js'; -export {default as withCoverArtDate} from './withCoverArtDate.js'; export {default as withDirectory} from './withDirectory.js'; +export {default as withHasArtwork} from './withHasArtwork.js'; export {default as withRecontextualizedContributionList} from './withRecontextualizedContributionList.js'; export {default as withRedatedContributionList} from './withRedatedContributionList.js'; export {default as withResolvedAnnotatedReferenceList} from './withResolvedAnnotatedReferenceList.js'; diff --git a/src/data/composite/wiki-data/withConstitutedArtwork.js b/src/data/composite/wiki-data/withConstitutedArtwork.js index 6187d55b..28d719e2 100644 --- a/src/data/composite/wiki-data/withConstitutedArtwork.js +++ b/src/data/composite/wiki-data/withConstitutedArtwork.js @@ -1,6 +1,5 @@ import {input, templateCompositeFrom} from '#composite'; import thingConstructors from '#things'; -import {isContributionList} from '#validators'; export default templateCompositeFrom({ annotation: `withConstitutedArtwork`, diff --git a/src/data/composite/things/album/withHasCoverArt.js b/src/data/composite/wiki-data/withHasArtwork.js index fd3f2894..9c22f439 100644 --- a/src/data/composite/things/album/withHasCoverArt.js +++ b/src/data/composite/wiki-data/withHasArtwork.js @@ -1,7 +1,5 @@ -// TODO: This shouldn't be coded as an Album-specific thing, -// or even really to do with cover artworks in particular, either. - import {input, templateCompositeFrom} from '#composite'; +import {isContributionList, isThing, strictArrayOf} from '#validators'; import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck} from '#composite/control-flow'; @@ -9,13 +7,30 @@ import {fillMissingListItems, withFlattenedList, withPropertyFromList} from '#composite/data'; export default templateCompositeFrom({ - annotation: 'withHasCoverArt', + annotation: 'withHasArtwork', + + inputs: { + contribs: input({ + validate: isContributionList, + defaultValue: null, + }), + + artwork: input({ + validate: isThing, + defaultValue: null, + }), + + artworks: input({ + validate: strictArrayOf(isThing), + defaultValue: null, + }), + }, - outputs: ['#hasCoverArt'], + outputs: ['#hasArtwork'], steps: () => [ withResultOfAvailabilityCheck({ - from: 'coverArtistContribs', + from: input('contribs'), mode: input.value('empty'), }), @@ -26,19 +41,37 @@ export default templateCompositeFrom({ }) => (availability ? continuation.raiseOutput({ - ['#hasCoverArt']: true, + ['#hasArtwork']: true, }) : continuation()), }, + { + dependencies: [input('artwork'), input('artworks')], + compute: (continuation, { + [input('artwork')]: artwork, + [input('artworks')]: artworks, + }) => + continuation({ + ['#artworks']: + (artwork && artworks + ? [artwork, ...artworks] + : artwork + ? [artwork] + : artworks + ? artworks + : []), + }), + }, + raiseOutputWithoutDependency({ - dependency: 'coverArtworks', + dependency: '#artworks', mode: input.value('empty'), - output: input.value({'#hasCoverArt': false}), + output: input.value({'#hasArtwork': false}), }), withPropertyFromList({ - list: 'coverArtworks', + list: '#artworks', property: input.value('artistContribs'), internal: input.value(true), }), @@ -46,19 +79,19 @@ export default templateCompositeFrom({ // Since we're getting the update value for each artwork's artistContribs, // it may not be set at all, and in that case won't be exposing as []. fillMissingListItems({ - list: '#coverArtworks.artistContribs', + list: '#artworks.artistContribs', fill: input.value([]), }), withFlattenedList({ - list: '#coverArtworks.artistContribs', + list: '#artworks.artistContribs', }), withResultOfAvailabilityCheck({ from: '#flattenedList', mode: input.value('empty'), }).outputs({ - '#availability': '#hasCoverArt', + '#availability': '#hasArtwork', }), ], }); diff --git a/src/data/composite/wiki-properties/referencedArtworkList.js b/src/data/composite/wiki-properties/referencedArtworkList.js index 9ba2e393..4f243493 100644 --- a/src/data/composite/wiki-properties/referencedArtworkList.js +++ b/src/data/composite/wiki-properties/referencedArtworkList.js @@ -1,6 +1,5 @@ import {input, templateCompositeFrom} from '#composite'; import find from '#find'; -import {isDate} from '#validators'; import annotatedReferenceList from './annotatedReferenceList.js'; diff --git a/src/data/thing.js b/src/data/thing.js index 66f73de5..f719224d 100644 --- a/src/data/thing.js +++ b/src/data/thing.js @@ -60,7 +60,7 @@ export default class Thing extends CacheableObject { if (this.name) { name = colors.green(`"${this.name}"`); } - } catch (error) { + } catch { name = colors.yellow(`couldn't get name`); } @@ -69,7 +69,7 @@ export default class Thing extends CacheableObject { if (this.directory) { reference = colors.blue(Thing.getReference(this)); } - } catch (error) { + } catch { reference = colors.yellow(`couldn't get reference`); } diff --git a/src/data/things/additional-file.js b/src/data/things/additional-file.js index 2ddc688a..398d0af5 100644 --- a/src/data/things/additional-file.js +++ b/src/data/things/additional-file.js @@ -8,7 +8,7 @@ import {exposeConstant, exposeUpdateValueOrContinue} from '#composite/control-flow'; export class AdditionalFile extends Thing { - static [Thing.getPropertyDescriptors] = ({}) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose thing: thing(), diff --git a/src/data/things/additional-name.js b/src/data/things/additional-name.js index b96fcd50..4c23f291 100644 --- a/src/data/things/additional-name.js +++ b/src/data/things/additional-name.js @@ -1,9 +1,9 @@ import Thing from '#thing'; -import {contentString, simpleString, thing} from '#composite/wiki-properties'; +import {contentString, thing} from '#composite/wiki-properties'; export class AdditionalName extends Thing { - static [Thing.getPropertyDescriptors] = ({}) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose thing: thing(), diff --git a/src/data/things/album.js b/src/data/things/album.js index a4c2f6a5..45cadc12 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -8,7 +8,7 @@ import {colors} from '#cli'; import {input} from '#composite'; import {traverse} from '#node-utils'; import {sortAlbumsTracksChronologically, sortChronologically} from '#sort'; -import {accumulateSum, empty} from '#sugar'; +import {empty} from '#sugar'; import Thing from '#thing'; import {isColor, isDate, isDirectory, isNumber} from '#validators'; @@ -28,8 +28,7 @@ import { import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; - -import {exitWithoutContribs, withDirectory, withCoverArtDate} +import {exitWithoutArtwork, withDirectory, withHasArtwork} from '#composite/wiki-data'; import { @@ -47,7 +46,6 @@ import { name, referencedArtworkList, referenceList, - reverseReferenceList, simpleDate, simpleString, soupyFind, @@ -59,7 +57,7 @@ import { wikiData, } from '#composite/wiki-properties'; -import {withHasCoverArt, withTracks} from '#composite/things/album'; +import {withCoverArtDate, withTracks} from '#composite/things/album'; import {withAlbum, withContinueCountingFrom, withStartCountingFrom} from '#composite/things/track-section'; @@ -74,11 +72,16 @@ export class Album extends Thing { CommentaryEntry, CreditingSourcesEntry, Group, - Track, TrackSection, WikiInfo, }) => ({ - // Update & expose + // > Update & expose - Internal relationships + + trackSections: thingList({ + class: input.value(TrackSection), + }), + + // > Update & expose - Identifying metadata name: name('Unnamed Album'), directory: directory(), @@ -99,20 +102,63 @@ export class Album extends Thing { alwaysReferenceTracksByDirectory: flag(false), suffixTrackDirectories: flag(false), - color: color(), - urls: urls(), + bandcampAlbumIdentifier: simpleString(), + bandcampArtworkIdentifier: simpleString(), additionalNames: thingList({ class: input.value(AdditionalName), }), - bandcampAlbumIdentifier: simpleString(), - bandcampArtworkIdentifier: simpleString(), - date: simpleDate(), - trackArtDate: simpleDate(), dateAddedToWiki: simpleDate(), + // > Update & expose - Credits and contributors + + artistContribs: contributionList({ + date: 'date', + artistProperty: input.value('albumArtistContributions'), + }), + + // > Update & expose - General configuration + + countTracksInArtistTotals: flag(true), + + hasTrackNumbers: flag(true), + isListedOnHomepage: flag(true), + isListedInGalleries: flag(true), + + // > Update & expose - General metadata + + color: color(), + + urls: urls(), + + // > Update & expose - Artworks + + coverArtworks: [ + // This works, lol, because this array describes `expose.transform` for + // the coverArtworks property, and compositions generally access the + // update value, not what's exposed by property access out in the open. + // There's no recursion going on here. + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + value: input.value([]), + }), + + constitutibleArtworkList.fromYAMLFieldSpec + .call(this, 'Cover Artwork'), + ], + + coverArtistContribs: [ + withCoverArtDate(), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumCoverArtistContributions'), + }), + ], + coverArtDate: [ withCoverArtDate({ from: input.updateValue({ @@ -124,52 +170,61 @@ export class Album extends Thing { ], coverArtFileExtension: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), + fileExtension('jpg'), ], - trackCoverArtFileExtension: fileExtension('jpg'), + coverArtDimensions: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), - wallpaperFileExtension: [ - exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), - fileExtension('jpg'), + dimensions(), ], - bannerFileExtension: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - fileExtension('jpg'), - ], + artTags: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + value: input.value([]), + }), - wallpaperStyle: [ - exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), - simpleString(), + referenceList({ + class: input.value(ArtTag), + find: soupyFind.input('artTag'), + }), ], - wallpaperParts: [ - exitWithoutContribs({ - contribs: 'wallpaperArtistContribs', + referencedArtworks: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', value: input.value([]), }), - wallpaperParts(), + referencedArtworkList(), ], - bannerStyle: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - simpleString(), - ], + trackCoverArtistContribs: contributionList({ + // May be null, indicating cover art was added for tracks on the date + // each track specifies, or else the track's own release date. + date: 'trackArtDate', - coverArtDimensions: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), - dimensions(), - ], + // This is the "correct" value, but it gets overwritten - with the same + // value - regardless. + artistProperty: input.value('trackCoverArtistContributions'), + }), - trackDimensions: dimensions(), + trackArtDate: simpleDate(), - bannerDimensions: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - dimensions(), - ], + trackCoverArtFileExtension: fileExtension('jpg'), + + trackDimensions: dimensions(), wallpaperArtwork: [ exitWithoutDependency({ @@ -182,117 +237,113 @@ export class Album extends Thing { .call(this, 'Wallpaper Artwork'), ], - bannerArtwork: [ - exitWithoutDependency({ - dependency: 'bannerArtistContribs', - mode: input.value('empty'), - value: input.value(null), - }), + wallpaperArtistContribs: [ + withCoverArtDate(), - constitutibleArtwork.fromYAMLFieldSpec - .call(this, 'Banner Artwork'), + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumWallpaperArtistContributions'), + }), ], - coverArtworks: [ - withHasCoverArt(), - - exitWithoutDependency({ - dependency: '#hasCoverArt', - mode: input.value('falsy'), - value: input.value([]), + wallpaperFileExtension: [ + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', }), - constitutibleArtworkList.fromYAMLFieldSpec - .call(this, 'Cover Artwork'), + fileExtension('jpg'), ], - hasTrackNumbers: flag(true), - isListedOnHomepage: flag(true), - isListedInGalleries: flag(true), + wallpaperStyle: [ + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', + }), - commentary: thingList({ - class: input.value(CommentaryEntry), - }), + simpleString(), + ], - creditSources: thingList({ - class: input.value(CreditingSourcesEntry), - }), + wallpaperParts: [ + // kinda nonsensical or at least unlikely lol, but y'know + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', + value: input.value([]), + }), - additionalFiles: thingList({ - class: input.value(AdditionalFile), - }), + wallpaperParts(), + ], - trackSections: thingList({ - class: input.value(TrackSection), - }), + bannerArtwork: [ + exitWithoutDependency({ + dependency: 'bannerArtistContribs', + mode: input.value('empty'), + value: input.value(null), + }), - artistContribs: contributionList({ - date: 'date', - artistProperty: input.value('albumArtistContributions'), - }), + constitutibleArtwork.fromYAMLFieldSpec + .call(this, 'Banner Artwork'), + ], - coverArtistContribs: [ + bannerArtistContribs: [ withCoverArtDate(), contributionList({ date: '#coverArtDate', - artistProperty: input.value('albumCoverArtistContributions'), + artistProperty: input.value('albumBannerArtistContributions'), }), ], - trackCoverArtistContribs: contributionList({ - // May be null, indicating cover art was added for tracks on the date - // each track specifies, or else the track's own release date. - date: 'trackArtDate', - - // This is the "correct" value, but it gets overwritten - with the same - // value - regardless. - artistProperty: input.value('trackCoverArtistContributions'), - }), + bannerFileExtension: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', + }), - wallpaperArtistContribs: [ - withCoverArtDate(), + fileExtension('jpg'), + ], - contributionList({ - date: '#coverArtDate', - artistProperty: input.value('albumWallpaperArtistContributions'), + bannerDimensions: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', }), - ], - bannerArtistContribs: [ - withCoverArtDate(), + dimensions(), + ], - contributionList({ - date: '#coverArtDate', - artistProperty: input.value('albumBannerArtistContributions'), + bannerStyle: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', }), + + simpleString(), ], + // > Update & expose - Groups + groups: referenceList({ class: input.value(Group), find: soupyFind.input('group'), }), - artTags: [ - exitWithoutContribs({ - contribs: 'coverArtistContribs', - value: input.value([]), - }), + // > Update & expose - Content entries - referenceList({ - class: input.value(ArtTag), - find: soupyFind.input('artTag'), - }), - ], + commentary: thingList({ + class: input.value(CommentaryEntry), + }), - referencedArtworks: [ - exitWithoutContribs({ - contribs: 'coverArtistContribs', - value: input.value([]), - }), + creditingSources: thingList({ + class: input.value(CreditingSourcesEntry), + }), - referencedArtworkList(), - ], + // Additional files + + additionalFiles: thingList({ + class: input.value(AdditionalFile), + }), // Update only @@ -314,8 +365,12 @@ export class Album extends Thing { commentatorArtists: commentatorArtists(), hasCoverArt: [ - withHasCoverArt(), - exposeDependency({dependency: '#hasCoverArt'}), + withHasArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), + + exposeDependency({dependency: '#hasArtwork'}), ], hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}), @@ -478,21 +533,14 @@ export class Album extends Thing { static [Thing.yamlDocumentSpec] = { fields: { - 'Album': {property: 'name'}, + // Identifying metadata + 'Album': {property: 'name'}, 'Directory': {property: 'directory'}, 'Directory Suffix': {property: 'directorySuffix'}, 'Suffix Track Directories': {property: 'suffixTrackDirectories'}, - 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, - 'Always Reference Tracks By Directory': { - property: 'alwaysReferenceTracksByDirectory', - }, - - 'Additional Names': { - property: 'additionalNames', - transform: parseAdditionalNames, - }, + 'Always Reference Tracks By Directory': {property: 'alwaysReferenceTracksByDirectory'}, 'Bandcamp Album ID': { property: 'bandcampAlbumIdentifier', @@ -504,18 +552,46 @@ export class Album extends Thing { transform: String, }, + 'Additional Names': { + property: 'additionalNames', + transform: parseAdditionalNames, + }, + 'Date': { property: 'date', transform: parseDate, }, - 'Color': {property: 'color'}, - 'URLs': {property: 'urls'}, + 'Date Added': { + property: 'dateAddedToWiki', + transform: parseDate, + }, + + // Credits and contributors + + 'Artists': { + property: 'artistContribs', + transform: parseContributors, + }, + + // General configuration + + 'Count Tracks In Artist Totals': {property: 'countInArtistTotals'}, 'Has Track Numbers': {property: 'hasTrackNumbers'}, 'Listed on Homepage': {property: 'isListedOnHomepage'}, 'Listed in Galleries': {property: 'isListedInGalleries'}, + // General metadata + + 'Color': {property: 'color'}, + + 'URLs': {property: 'urls'}, + + // Artworks + // (Note - this YAML section is deliberately ordered differently + // than the corresponding property descriptors.) + 'Cover Artwork': { property: 'coverArtworks', transform: @@ -559,27 +635,29 @@ export class Album extends Thing { }), }, + 'Cover Artists': { + property: 'coverArtistContribs', + transform: parseContributors, + }, + 'Cover Art Date': { property: 'coverArtDate', transform: parseDate, }, - 'Default Track Cover Art Date': { - property: 'trackArtDate', - transform: parseDate, + 'Cover Art Dimensions': { + property: 'coverArtDimensions', + transform: parseDimensions, }, - 'Date Added': { - property: 'dateAddedToWiki', - transform: parseDate, + 'Default Track Cover Artists': { + property: 'trackCoverArtistContribs', + transform: parseContributors, }, - 'Cover Art File Extension': {property: 'coverArtFileExtension'}, - 'Track Art File Extension': {property: 'trackCoverArtFileExtension'}, - - 'Cover Art Dimensions': { - property: 'coverArtDimensions', - transform: parseDimensions, + 'Default Track Cover Art Date': { + property: 'trackArtDate', + transform: parseDate, }, 'Default Track Dimensions': { @@ -593,7 +671,6 @@ export class Album extends Thing { }, 'Wallpaper Style': {property: 'wallpaperStyle'}, - 'Wallpaper File Extension': {property: 'wallpaperFileExtension'}, 'Wallpaper Parts': { property: 'wallpaperParts', @@ -605,54 +682,51 @@ export class Album extends Thing { transform: parseContributors, }, - 'Banner Style': {property: 'bannerStyle'}, - 'Banner File Extension': {property: 'bannerFileExtension'}, - 'Banner Dimensions': { property: 'bannerDimensions', transform: parseDimensions, }, - 'Commentary': { - property: 'commentary', - transform: parseCommentary, - }, + 'Banner Style': {property: 'bannerStyle'}, - 'Credit Sources': { - property: 'creditSources', - transform: parseCreditingSources, - }, + 'Cover Art File Extension': {property: 'coverArtFileExtension'}, + 'Track Art File Extension': {property: 'trackCoverArtFileExtension'}, + 'Wallpaper File Extension': {property: 'wallpaperFileExtension'}, + 'Banner File Extension': {property: 'bannerFileExtension'}, - 'Additional Files': { - property: 'additionalFiles', - transform: parseAdditionalFiles, - }, + 'Art Tags': {property: 'artTags'}, 'Referenced Artworks': { property: 'referencedArtworks', transform: parseAnnotatedReferences, }, - 'Franchises': {ignore: true}, + // Groups - 'Artists': { - property: 'artistContribs', - transform: parseContributors, + 'Groups': {property: 'groups'}, + + // Content entries + + 'Commentary': { + property: 'commentary', + transform: parseCommentary, }, - 'Cover Artists': { - property: 'coverArtistContribs', - transform: parseContributors, + 'Crediting Sources': { + property: 'creditingSources', + transform: parseCreditingSources, }, - 'Default Track Cover Artists': { - property: 'trackCoverArtistContribs', - transform: parseContributors, + // Additional files + + 'Additional Files': { + property: 'additionalFiles', + transform: parseAdditionalFiles, }, - 'Groups': {property: 'groups'}, - 'Art Tags': {property: 'artTags'}, + // Shenanigans + 'Franchises': {ignore: true}, 'Review Points': {ignore: true}, }, @@ -696,6 +770,7 @@ export class Album extends Thing { const artworkData = []; const commentaryData = []; const creditingSourceData = []; + const referencingSourceData = []; const lyricsData = []; for (const {header: album, entries} of results) { @@ -709,8 +784,6 @@ export class Album extends Thing { isDefaultTrackSection: true, }); - const albumRef = Thing.getReference(album); - const closeCurrentTrackSection = () => { if ( currentTrackSection.isDefaultTrackSection && @@ -744,7 +817,8 @@ export class Album extends Thing { artworkData.push(...entry.trackArtworks); commentaryData.push(...entry.commentary); - creditingSourceData.push(...entry.creditSources); + creditingSourceData.push(...entry.creditingSources); + referencingSourceData.push(...entry.referencingSources); // TODO: As exposed, Track.lyrics tries to inherit from the main // release, which is impossible before the data's been linked. @@ -767,7 +841,7 @@ export class Album extends Thing { } commentaryData.push(...album.commentary); - creditingSourceData.push(...album.creditSources); + creditingSourceData.push(...album.creditingSources); album.trackSections = trackSections; } @@ -780,6 +854,7 @@ export class Album extends Thing { artworkData, commentaryData, creditingSourceData, + referencingSourceData, lyricsData, }; }, @@ -835,13 +910,19 @@ export class Album extends Thing { artwork.fileExtension, ]; } + + // As of writing, albums don't even have a `duration` property... + // so this function will never be called... but the message stands... + countOwnContributionInDurationTotals(_contrib) { + return false; + } } export class TrackSection extends Thing { static [Thing.friendlyName] = `Track Section`; static [Thing.referenceType] = `track-section`; - static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ + static [Thing.getPropertyDescriptors] = ({Track}) => ({ // Update & expose name: name('Unnamed Track Section'), diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 0ec1ff31..518f616b 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,8 +1,7 @@ export const ART_TAG_DATA_FILE = 'tags.yaml'; import {input} from '#composite'; -import find from '#find'; -import {sortAlphabetically, sortAlbumsTracksChronologically} from '#sort'; +import {sortAlphabetically} from '#sort'; import Thing from '#thing'; import {unique} from '#sugar'; import {isName} from '#validators'; @@ -24,7 +23,6 @@ import { soupyReverse, thingList, urls, - wikiData, } from '#composite/wiki-properties'; import {withAllDescendantArtTags, withAncestorArtTagBaobabTree} @@ -34,11 +32,7 @@ export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; static [Thing.friendlyName] = `Art Tag`; - static [Thing.getPropertyDescriptors] = ({ - AdditionalName, - Album, - Track, - }) => ({ + static [Thing.getPropertyDescriptors] = ({AdditionalName}) => ({ // Update & expose name: name('Unnamed Art Tag'), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 9e329c74..5b67051c 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -26,7 +26,6 @@ import { soupyFind, soupyReverse, urls, - wikiData, } from '#composite/wiki-properties'; import {artistTotalDuration} from '#composite/things/artist'; @@ -35,7 +34,7 @@ export class Artist extends Thing { static [Thing.referenceType] = 'artist'; static [Thing.wikiDataArray] = 'artistData'; - static [Thing.getPropertyDescriptors] = ({Album, Flash, Group, Track}) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose name: name('Unnamed Artist'), @@ -287,7 +286,7 @@ export class Artist extends Thing { let aliasedArtist; try { aliasedArtist = this.aliasedArtist.name; - } catch (_error) { + } catch { aliasedArtist = CacheableObject.getUpdateValue(this, 'aliasedArtist'); } diff --git a/src/data/things/artwork.js b/src/data/things/artwork.js index ac70159c..57c293ca 100644 --- a/src/data/things/artwork.js +++ b/src/data/things/artwork.js @@ -1,5 +1,6 @@ import {inspect} from 'node:util'; +import {colors} from '#cli'; import {input} from '#composite'; import find from '#find'; import Thing from '#thing'; @@ -24,7 +25,7 @@ import { parseDimensions, } from '#yaml'; -import {withIndexInList, withPropertyFromObject} from '#composite/data'; +import {withPropertyFromObject} from '#composite/data'; import { exitWithoutDependency, @@ -64,10 +65,7 @@ import { export class Artwork extends Thing { static [Thing.referenceType] = 'artwork'; - static [Thing.getPropertyDescriptors] = ({ - ArtTag, - Contribution, - }) => ({ + static [Thing.getPropertyDescriptors] = ({ArtTag}) => ({ // Update & expose unqualifiedDirectory: directory({ @@ -79,6 +77,7 @@ export class Artwork extends Thing { label: simpleString(), source: contentString(), + originDetails: contentString(), dateFromThingProperty: simpleString(), @@ -171,6 +170,7 @@ export class Artwork extends Thing { withResolvedContribs({ from: input.updateValue({validate: isContributionList}), date: '#date', + thingProperty: input.thisProperty(), artistProperty: 'artistContribsArtistProperty', }), @@ -371,6 +371,21 @@ export class Artwork extends Thing { attachingArtworks: reverseReferenceList({ reverse: soupyReverse.input('artworksWhichAttach'), }), + + groups: [ + withPropertyFromObject({ + object: 'thing', + property: input.value('groups'), + }), + + exposeDependencyOrContinue({ + dependency: '#thing.groups', + }), + + exposeConstant({ + value: input.value([]), + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -385,6 +400,7 @@ export class Artwork extends Thing { 'Label': {property: 'label'}, 'Source': {property: 'source'}, + 'Origin Details': {property: 'originDetails'}, 'Date': { property: 'date', @@ -456,6 +472,18 @@ export class Artwork extends Thing { return this.thing.getOwnArtworkPath(this); } + countOwnContributionInContributionTotals(contrib) { + if (this.attachAbove) { + return false; + } + + if (contrib.annotation?.startsWith('edits for wiki')) { + return false; + } + + return true; + } + [inspect.custom](depth, options, inspect) { const parts = []; diff --git a/src/data/things/content.js b/src/data/things/content.js index cf8fa1f4..ca41ccaa 100644 --- a/src/data/things/content.js +++ b/src/data/things/content.js @@ -1,10 +1,9 @@ import {input} from '#composite'; -import find from '#find'; import Thing from '#thing'; import {is, isDate} from '#validators'; import {parseDate} from '#yaml'; -import {contentString, referenceList, simpleDate, soupyFind, thing} +import {contentString, simpleDate, soupyFind, thing} from '#composite/wiki-properties'; import { @@ -27,7 +26,7 @@ import { } from '#composite/things/content'; export class ContentEntry extends Thing { - static [Thing.getPropertyDescriptors] = ({Artist}) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose thing: thing(), @@ -51,6 +50,10 @@ export class ContentEntry extends Thing { }, accessKind: [ + exitWithoutDependency({ + dependency: 'accessDate', + }), + exposeUpdateValueOrContinue({ validate: input.value( is(...[ @@ -74,7 +77,7 @@ export class ContentEntry extends Thing { }, exposeConstant({ - value: input.value(null), + value: input.value('accessed'), }), ], @@ -156,6 +159,10 @@ export class CommentaryEntry extends ContentEntry { export class LyricsEntry extends ContentEntry { static [Thing.getPropertyDescriptors] = () => ({ + // Update & expose + + originDetails: contentString(), + // Expose only isWikiLyrics: hasAnnotationPart({ @@ -185,6 +192,14 @@ export class LyricsEntry extends ContentEntry { }, ], }); + + static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(ContentEntry, { + fields: { + 'Origin Details': {property: 'originDetails'}, + }, + }); } export class CreditingSourcesEntry extends ContentEntry {} + +export class ReferencingSourcesEntry extends ContentEntry {} diff --git a/src/data/things/contribution.js b/src/data/things/contribution.js index c92fafb4..90e8eb79 100644 --- a/src/data/things/contribution.js +++ b/src/data/things/contribution.js @@ -5,12 +5,20 @@ import {colors} from '#cli'; import {input} from '#composite'; import {empty} from '#sugar'; import Thing from '#thing'; -import {isStringNonEmpty, isThing, validateReference} from '#validators'; +import {isBoolean, isStringNonEmpty, isThing, validateReference} + from '#validators'; -import {exitWithoutDependency, exposeDependency} from '#composite/control-flow'; import {flag, simpleDate, soupyFind} from '#composite/wiki-properties'; import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + +import { withFilteredList, withNearbyItemFromList, withPropertyFromList, @@ -70,7 +78,26 @@ export class Contribution extends Thing { property: input.thisProperty(), }), - flag(true), + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + { + dependencies: ['thing', input.myself()], + compute: (continuation, { + ['thing']: thing, + [input.myself()]: contribution, + }) => + (thing.countOwnContributionInContributionTotals?.(contribution) + ? true + : thing.countOwnContributionInContributionTotals + ? false + : continuation()), + }, + + exposeConstant({ + value: input.value(true), + }), ], countInDurationTotals: [ @@ -78,7 +105,37 @@ export class Contribution extends Thing { property: input.thisProperty(), }), - flag(true), + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withPropertyFromObject({ + object: 'thing', + property: input.value('duration'), + }), + + exitWithoutDependency({ + dependency: '#thing.duration', + mode: input.value('falsy'), + value: input.value(false), + }), + + { + dependencies: ['thing', input.myself()], + compute: (continuation, { + ['thing']: thing, + [input.myself()]: contribution, + }) => + (thing.countOwnContributionInDurationTotals?.(contribution) + ? true + : thing.countOwnContributionInDurationTotals + ? false + : continuation()), + }, + + exposeConstant({ + value: input.value(true), + }), ], // Update only @@ -238,6 +295,21 @@ export class Contribution extends Thing { dependency: '#nearbyItem', }), ], + + groups: [ + withPropertyFromObject({ + object: 'thing', + property: input.value('groups'), + }), + + exposeDependencyOrContinue({ + dependency: '#thing.groups', + }), + + exposeConstant({ + value: input.value([]), + }), + ], }); [inspect.custom](depth, options, inspect) { @@ -259,7 +331,7 @@ export class Contribution extends Thing { let artist; try { artist = this.artist; - } catch (_error) { + } catch { // Computing artist might crash for any reason - don't distract from // other errors as a result of inspecting this contribution. } diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 11b19ebc..160221f0 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -43,7 +43,6 @@ import { thing, thingList, urls, - wikiData, } from '#composite/wiki-properties'; import {withFlashAct} from '#composite/things/flash'; @@ -57,7 +56,6 @@ export class Flash extends Thing { CommentaryEntry, CreditingSourcesEntry, Track, - FlashAct, WikiInfo, }) => ({ // Update & expose @@ -135,7 +133,7 @@ export class Flash extends Thing { class: input.value(CommentaryEntry), }), - creditSources: thingList({ + creditingSources: thingList({ class: input.value(CreditingSourcesEntry), }), @@ -257,8 +255,8 @@ export class Flash extends Thing { transform: parseCommentary, }, - 'Credit Sources': { - property: 'creditSources', + 'Crediting Sources': { + property: 'creditingSources', transform: parseCreditingSources, }, @@ -461,7 +459,7 @@ export class FlashSide extends Thing { const artworkData = flashData.map(flash => flash.coverArtwork); const commentaryData = flashData.flatMap(flash => flash.commentary); - const creditingSourceData = flashData.flatMap(flash => flash.creditSources); + const creditingSourceData = flashData.flatMap(flash => flash.creditingSources); return { flashData, diff --git a/src/data/things/group.js b/src/data/things/group.js index 4b4c306c..0262a3a5 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -19,7 +19,6 @@ import { thing, thingList, urls, - wikiData, } from '#composite/wiki-properties'; export class Group extends Thing { diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 82bad2d3..3a11c287 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -63,7 +63,6 @@ export class HomepageLayout extends Thing { thingConstructors: { HomepageLayout, HomepageLayoutSection, - HomepageLayoutAlbumsRow, }, }) => ({ title: `Process homepage layout file`, @@ -250,7 +249,7 @@ export class HomepageLayoutActionsRow extends HomepageLayoutRow { export class HomepageLayoutAlbumCarouselRow extends HomepageLayoutRow { static [Thing.friendlyName] = `Homepage Album Carousel Row`; - static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({ + static [Thing.getPropertyDescriptors] = (opts, {Album} = opts) => ({ ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), // Update & expose diff --git a/src/data/things/language.js b/src/data/things/language.js index 4e23cf7f..e3689643 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -865,6 +865,18 @@ export class Language extends Thing { } } + typicallyLowerCase(string) { + // Utter nonsense implementation, so this only works on strings, + // not actual HTML content, and may rudely disrespect *intentful* + // capitalization of whatever goes into it. + + if (typeof string !== 'string') return string; + if (string.length <= 1) return string; + if (/^\S+?[A-Z]/.test(string)) return string; + + return string[0].toLowerCase() + string.slice(1); + } + // Utility function to quickly provide a useful string key // (generally a prefix) to stuff nested beneath it. encapsulate(...args) { diff --git a/src/data/things/sorting-rule.js b/src/data/things/sorting-rule.js index b169a541..ccc4ad89 100644 --- a/src/data/things/sorting-rule.js +++ b/src/data/things/sorting-rule.js @@ -3,7 +3,6 @@ export const SORTING_RULE_DATA_FILE = 'sorting-rules.yaml'; import {readFile, writeFile} from 'node:fs/promises'; import * as path from 'node:path'; -import {input} from '#composite'; import {chunkByProperties, compareArrays, unique} from '#sugar'; import Thing from '#thing'; import {isObject, isStringNonEmpty, anyOf, strictArrayOf} from '#validators'; diff --git a/src/data/things/track.js b/src/data/things/track.js index 557ba2a7..e652de52 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -15,6 +15,7 @@ import { parseCommentary, parseContributors, parseCreditingSources, + parseReferencingSources, parseDate, parseDimensions, parseDuration, @@ -40,7 +41,6 @@ import { import { commentatorArtists, constitutibleArtworkList, - contentString, contributionList, dimensions, directory, @@ -91,12 +91,17 @@ export class Track extends Thing { Artwork, CommentaryEntry, CreditingSourcesEntry, - Flash, LyricsEntry, - TrackSection, + ReferencingSourcesEntry, WikiInfo, }) => ({ - // Update & expose + // > Update & expose - Internal relationships + + album: thing({ + class: input.value(Album), + }), + + // > Update & expose - Identifying metadata name: name('Unnamed Track'), @@ -130,20 +135,93 @@ export class Track extends Thing { }) ], - album: thing({ - class: input.value(Album), + alwaysReferenceByDirectory: [ + withAlwaysReferenceByDirectory(), + exposeDependency({dependency: '#alwaysReferenceByDirectory'}), + ], + + mainReleaseTrack: singleReference({ + class: input.value(Track), + find: soupyFind.input('track'), }), + bandcampTrackIdentifier: simpleString(), + bandcampArtworkIdentifier: simpleString(), + additionalNames: thingList({ class: input.value(AdditionalName), }), - bandcampTrackIdentifier: simpleString(), - bandcampArtworkIdentifier: simpleString(), + dateFirstReleased: simpleDate(), + + // > Update & expose - Credits and contributors + + artistContribs: [ + inheritContributionListFromMainRelease(), + + withDate(), + + withResolvedContribs({ + from: input.updateValue({validate: isContributionList}), + thingProperty: input.thisProperty(), + artistProperty: input.value('trackArtistContributions'), + date: '#date', + }).outputs({ + '#resolvedContribs': '#artistContribs', + }), + + exposeDependencyOrContinue({ + dependency: '#artistContribs', + mode: input.value('empty'), + }), + + withPropertyFromAlbum({ + property: input.value('artistContribs'), + }), + + withRecontextualizedContributionList({ + list: '#album.artistContribs', + artistProperty: input.value('trackArtistContributions'), + }), + + withRedatedContributionList({ + list: '#album.artistContribs', + date: '#date', + }), + + exposeDependency({dependency: '#album.artistContribs'}), + ], + + contributorContribs: [ + inheritContributionListFromMainRelease(), + + withDate(), + + contributionList({ + date: '#date', + artistProperty: input.value('trackContributorContributions'), + }), + ], + + // > Update & expose - General configuration + + countInArtistTotals: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withPropertyFromAlbum({ + property: input.value('countTracksInArtistTotals'), + }), + + exposeDependency({dependency: '#album.countTracksInArtistTotals'}), + ], + + disableUniqueCoverArt: flag(), + + // > Update & expose - General metadata duration: duration(), - urls: urls(), - dateFirstReleased: simpleDate(), color: [ exposeUpdateValueOrContinue({ @@ -166,37 +244,27 @@ export class Track extends Thing { exposeDependency({dependency: '#album.color'}), ], - alwaysReferenceByDirectory: [ - withAlwaysReferenceByDirectory(), - exposeDependency({dependency: '#alwaysReferenceByDirectory'}), - ], - - // Disables presenting the track as though it has its own unique artwork. - // This flag should only be used in select circumstances, i.e. to override - // an album's trackCoverArtists. This flag supercedes that property, as well - // as the track's own coverArtists. - disableUniqueCoverArt: flag(), - - // File extension for track's corresponding media file. This represents the - // track's unique cover artwork, if any, and does not inherit the extension - // of the album's main artwork. It does inherit trackCoverArtFileExtension, - // if present on the album. - coverArtFileExtension: [ - exitWithoutUniqueCoverArt(), + urls: urls(), - exposeUpdateValueOrContinue({ - validate: input.value(isFileExtension), - }), + // > Update & expose - Artworks - withPropertyFromAlbum({ - property: input.value('trackCoverArtFileExtension'), + trackArtworks: [ + exitWithoutUniqueCoverArt({ + value: input.value([]), }), - exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), + constitutibleArtworkList.fromYAMLFieldSpec + .call(this, 'Track Artwork'), + ], - exposeConstant({ - value: input.value('jpg'), + coverArtistContribs: [ + withCoverArtistContribs({ + from: input.updateValue({ + validate: isContributionList, + }), }), + + exposeDependency({dependency: '#coverArtistContribs'}), ], coverArtDate: [ @@ -209,113 +277,59 @@ export class Track extends Thing { exposeDependency({dependency: '#trackArtDate'}), ], - coverArtDimensions: [ + coverArtFileExtension: [ exitWithoutUniqueCoverArt(), - exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue({ + validate: input.value(isFileExtension), + }), withPropertyFromAlbum({ - property: input.value('trackDimensions'), + property: input.value('trackCoverArtFileExtension'), }), - exposeDependencyOrContinue({dependency: '#album.trackDimensions'}), - - dimensions(), - ], - - commentary: thingList({ - class: input.value(CommentaryEntry), - }), - - creditSources: thingList({ - class: input.value(CreditingSourcesEntry), - }), - - lyrics: [ - // TODO: Inherited lyrics are literally the same objects, so of course - // their .thing properties aren't going to point back to this one, and - // certainly couldn't be recontextualized... - inheritFromMainRelease(), + exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - thingList({ - class: input.value(LyricsEntry), + exposeConstant({ + value: input.value('jpg'), }), ], - additionalFiles: thingList({ - class: input.value(AdditionalFile), - }), - - sheetMusicFiles: thingList({ - class: input.value(AdditionalFile), - }), - - midiProjectFiles: thingList({ - class: input.value(AdditionalFile), - }), - - mainReleaseTrack: singleReference({ - class: input.value(Track), - find: soupyFind.input('track'), - }), - - artistContribs: [ - inheritContributionListFromMainRelease(), - - withDate(), - - withResolvedContribs({ - from: input.updateValue({validate: isContributionList}), - thingProperty: input.thisProperty(), - artistProperty: input.value('trackArtistContributions'), - date: '#date', - }).outputs({ - '#resolvedContribs': '#artistContribs', - }), + coverArtDimensions: [ + exitWithoutUniqueCoverArt(), - exposeDependencyOrContinue({ - dependency: '#artistContribs', - mode: input.value('empty'), - }), + exposeUpdateValueOrContinue(), withPropertyFromAlbum({ - property: input.value('artistContribs'), - }), - - withRecontextualizedContributionList({ - list: '#album.artistContribs', - artistProperty: input.value('trackArtistContributions'), + property: input.value('trackDimensions'), }), - withRedatedContributionList({ - list: '#album.artistContribs', - date: '#date', - }), + exposeDependencyOrContinue({dependency: '#album.trackDimensions'}), - exposeDependency({dependency: '#album.artistContribs'}), + dimensions(), ], - contributorContribs: [ - inheritContributionListFromMainRelease(), - - withDate(), + artTags: [ + exitWithoutUniqueCoverArt({ + value: input.value([]), + }), - contributionList({ - date: '#date', - artistProperty: input.value('trackContributorContributions'), + referenceList({ + class: input.value(ArtTag), + find: soupyFind.input('artTag'), }), ], - coverArtistContribs: [ - withCoverArtistContribs({ - from: input.updateValue({ - validate: isContributionList, - }), + referencedArtworks: [ + exitWithoutUniqueCoverArt({ + value: input.value([]), }), - exposeDependency({dependency: '#coverArtistContribs'}), + referencedArtworkList(), ], + // > Update & expose - Referenced tracks + referencedTracks: [ inheritFromMainRelease({ notFoundValue: input.value([]), @@ -338,35 +352,46 @@ export class Track extends Thing { }), ], - trackArtworks: [ - exitWithoutUniqueCoverArt({ - value: input.value([]), - }), + // > Update & expose - Additional files - constitutibleArtworkList.fromYAMLFieldSpec - .call(this, 'Track Artwork'), - ], + additionalFiles: thingList({ + class: input.value(AdditionalFile), + }), - artTags: [ - exitWithoutUniqueCoverArt({ - value: input.value([]), - }), + sheetMusicFiles: thingList({ + class: input.value(AdditionalFile), + }), - referenceList({ - class: input.value(ArtTag), - find: soupyFind.input('artTag'), + midiProjectFiles: thingList({ + class: input.value(AdditionalFile), + }), + + // > Update & expose - Content entries + + lyrics: [ + // TODO: Inherited lyrics are literally the same objects, so of course + // their .thing properties aren't going to point back to this one, and + // certainly couldn't be recontextualized... + inheritFromMainRelease(), + + thingList({ + class: input.value(LyricsEntry), }), ], - referencedArtworks: [ - exitWithoutUniqueCoverArt({ - value: input.value([]), - }), + commentary: thingList({ + class: input.value(CommentaryEntry), + }), - referencedArtworkList(), - ], + creditingSources: thingList({ + class: input.value(CreditingSourcesEntry), + }), + + referencingSources: thingList({ + class: input.value(ReferencingSourcesEntry), + }), - // Update only + // > Update only find: soupyFind(), reverse: soupyReverse(), @@ -386,7 +411,7 @@ export class Track extends Thing { class: input.value(WikiInfo), }), - // Expose only + // > Expose only commentatorArtists: commentatorArtists(), @@ -438,6 +463,16 @@ export class Track extends Thing { exposeDependency({dependency: '#otherReleases'}), ], + groups: [ + withPropertyFromAlbum({ + property: input.value('groups'), + }), + + exposeDependency({ + dependency: '#album.groups', + }), + ], + referencedByTracks: reverseReferenceList({ reverse: soupyReverse.input('tracksWhichReference'), }), @@ -453,14 +488,13 @@ export class Track extends Thing { static [Thing.yamlDocumentSpec] = { fields: { + // Identifying metadata + 'Track': {property: 'name'}, 'Directory': {property: 'directory'}, 'Suffix Directory': {property: 'suffixDirectoryFromAlbum'}, - - 'Additional Names': { - property: 'additionalNames', - transform: parseAdditionalNames, - }, + 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, + 'Main Release': {property: 'mainReleaseTrack'}, 'Bandcamp Track ID': { property: 'bandcampTrackIdentifier', @@ -472,17 +506,71 @@ export class Track extends Thing { transform: String, }, + 'Additional Names': { + property: 'additionalNames', + transform: parseAdditionalNames, + }, + + 'Date First Released': { + property: 'dateFirstReleased', + transform: parseDate, + }, + + // Credits and contributors + + 'Artists': { + property: 'artistContribs', + transform: parseContributors, + }, + + 'Contributors': { + property: 'contributorContribs', + transform: parseContributors, + }, + + // General configuration + + 'Count In Artist Totals': {property: 'countInArtistTotals'}, + + 'Has Cover Art': { + property: 'disableUniqueCoverArt', + transform: value => + (typeof value === 'boolean' + ? !value + : value), + }, + + // General metadata + 'Duration': { property: 'duration', transform: parseDuration, }, 'Color': {property: 'color'}, + 'URLs': {property: 'urls'}, - 'Date First Released': { - property: 'dateFirstReleased', - transform: parseDate, + // Artworks + + 'Track Artwork': { + property: 'trackArtworks', + transform: + parseArtwork({ + thingProperty: 'trackArtworks', + dimensionsFromThingProperty: 'coverArtDimensions', + fileExtensionFromThingProperty: 'coverArtFileExtension', + dateFromThingProperty: 'coverArtDate', + artTagsFromThingProperty: 'artTags', + referencedArtworksFromThingProperty: 'referencedArtworks', + artistContribsFromThingProperty: 'coverArtistContribs', + artistContribsArtistProperty: 'trackCoverArtistContributions', + }), + }, + + 'Cover Artists': { + property: 'coverArtistContribs', + transform: parseContributors, }, 'Cover Art Date': { @@ -497,30 +585,19 @@ export class Track extends Thing { transform: parseDimensions, }, - 'Has Cover Art': { - property: 'disableUniqueCoverArt', - transform: value => - (typeof value === 'boolean' - ? !value - : value), - }, - - 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, + 'Art Tags': {property: 'artTags'}, - 'Lyrics': { - property: 'lyrics', - transform: parseLyrics, + 'Referenced Artworks': { + property: 'referencedArtworks', + transform: parseAnnotatedReferences, }, - 'Commentary': { - property: 'commentary', - transform: parseCommentary, - }, + // Referenced tracks - 'Credit Sources': { - property: 'creditSources', - transform: parseCreditingSources, - }, + 'Referenced Tracks': {property: 'referencedTracks'}, + 'Sampled Tracks': {property: 'sampledTracks'}, + + // Additional files 'Additional Files': { property: 'additionalFiles', @@ -537,54 +614,41 @@ export class Track extends Thing { transform: parseAdditionalFiles, }, - 'Main Release': {property: 'mainReleaseTrack'}, - 'Referenced Tracks': {property: 'referencedTracks'}, - 'Sampled Tracks': {property: 'sampledTracks'}, - - 'Referenced Artworks': { - property: 'referencedArtworks', - transform: parseAnnotatedReferences, - }, - - 'Franchises': {ignore: true}, - 'Inherit Franchises': {ignore: true}, + // Content entries - 'Artists': { - property: 'artistContribs', - transform: parseContributors, + 'Lyrics': { + property: 'lyrics', + transform: parseLyrics, }, - 'Contributors': { - property: 'contributorContribs', - transform: parseContributors, + 'Commentary': { + property: 'commentary', + transform: parseCommentary, }, - 'Cover Artists': { - property: 'coverArtistContribs', - transform: parseContributors, + 'Crediting Sources': { + property: 'creditingSources', + transform: parseCreditingSources, }, - 'Track Artwork': { - property: 'trackArtworks', - transform: - parseArtwork({ - thingProperty: 'trackArtworks', - dimensionsFromThingProperty: 'coverArtDimensions', - fileExtensionFromThingProperty: 'coverArtFileExtension', - dateFromThingProperty: 'coverArtDate', - artTagsFromThingProperty: 'artTags', - referencedArtworksFromThingProperty: 'referencedArtworks', - artistContribsFromThingProperty: 'coverArtistContribs', - artistContribsArtistProperty: 'trackCoverArtistContributions', - }), + 'Referencing Sources': { + property: 'referencingSources', + transform: parseReferencingSources, }, - 'Art Tags': {property: 'artTags'}, + // Shenanigans + 'Franchises': {ignore: true}, + 'Inherit Franchises': {ignore: true}, 'Review Points': {ignore: true}, }, invalidFieldCombinations: [ + {message: `Secondary releases never count in artist totals`, fields: [ + 'Main Release', + 'Count In Artist Totals', + ]}, + {message: `Secondary releases inherit references from the main one`, fields: [ 'Main Release', 'Referenced Tracks', @@ -771,6 +835,30 @@ export class Track extends Thing { ]; } + countOwnContributionInContributionTotals(_contrib) { + if (!this.countInArtistTotals) { + return false; + } + + if (this.isSecondaryRelease) { + return false; + } + + return true; + } + + countOwnContributionInDurationTotals(_contrib) { + if (!this.countInArtistTotals) { + return false; + } + + if (this.isSecondaryRelease) { + return false; + } + + return true; + } + [inspect.custom](depth) { const parts = []; diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 590598be..f97f9027 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -2,7 +2,7 @@ export const WIKI_INFO_FILE = 'wiki-info.yaml'; import {input} from '#composite'; import Thing from '#thing'; -import {parseContributionPresets} from '#yaml'; +import {parseContributionPresets, parseWallpaperParts} from '#yaml'; import { isBoolean, @@ -14,8 +14,17 @@ import { } from '#validators'; import {exitWithoutDependency} from '#composite/control-flow'; -import {contentString, flag, name, referenceList, soupyFind} - from '#composite/wiki-properties'; + +import { + contentString, + fileExtension, + flag, + name, + referenceList, + simpleString, + soupyFind, + wallpaperParts, +} from '#composite/wiki-properties'; export class WikiInfo extends Thing { static [Thing.friendlyName] = `Wiki Info`; @@ -68,6 +77,10 @@ export class WikiInfo extends Thing { }, }, + wikiWallpaperFileExtension: fileExtension('jpg'), + wikiWallpaperStyle: simpleString(), + wikiWallpaperParts: wallpaperParts(), + divideTrackListsByGroups: referenceList({ class: input.value(Group), find: soupyFind.input('group'), @@ -112,18 +125,34 @@ export class WikiInfo extends Thing { fields: { 'Name': {property: 'name'}, 'Short Name': {property: 'nameShort'}, + 'Color': {property: 'color'}, + 'Description': {property: 'description'}, + 'Footer Content': {property: 'footerContent'}, + 'Default Language': {property: 'defaultLanguage'}, + 'Canonical Base': {property: 'canonicalBase'}, - 'Divide Track Lists By Groups': {property: 'divideTrackListsByGroups'}, + + 'Wiki Wallpaper File Extension': {property: 'wikiWallpaperFileExtension'}, + + 'Wiki Wallpaper Style': {property: 'wikiWallpaperStyle'}, + + 'Wiki Wallpaper Parts': { + property: 'wikiWallpaperParts', + transform: parseWallpaperParts, + }, + 'Enable Flashes & Games': {property: 'enableFlashesAndGames'}, 'Enable Listings': {property: 'enableListings'}, 'Enable News': {property: 'enableNews'}, 'Enable Art Tag UI': {property: 'enableArtTagUI'}, 'Enable Group UI': {property: 'enableGroupUI'}, + 'Divide Track Lists By Groups': {property: 'divideTrackListsByGroups'}, + 'Contribution Presets': { property: 'contributionPresets', transform: parseContributionPresets, diff --git a/src/data/yaml.js b/src/data/yaml.js index 2dd1f7e8..9a0295b8 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -930,6 +930,10 @@ export function parseCreditingSources(value, {subdoc, CreditingSourcesEntry}) { return parseContentEntries(CreditingSourcesEntry, value, {subdoc}); } +export function parseReferencingSources(value, {subdoc, ReferencingSourcesEntry}) { + return parseContentEntries(ReferencingSourcesEntry, value, {subdoc}); +} + export function parseLyrics(value, {subdoc, LyricsEntry}) { if ( typeof value === 'string' && @@ -1018,7 +1022,7 @@ export const documentModes = { export function getAllDataSteps() { try { thingConstructors; - } catch (error) { + } catch { throw new Error(`Thing constructors aren't ready yet, can't get all data steps`); } @@ -1638,6 +1642,8 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) { ['lyricsData', [/* find */]], + ['referencingSourceData', [/* find */]], + ['seriesData', [/* find */]], ['trackData', [ diff --git a/src/external-links.js b/src/external-links.js index 1055a391..ab1555bd 100644 --- a/src/external-links.js +++ b/src/external-links.js @@ -4,11 +4,9 @@ import { anyOf, is, isBoolean, - isObject, isStringNonEmpty, looseArrayOf, optional, - validateAllPropertyValues, validateArrayItems, validateInstanceOf, validateProperties, @@ -32,6 +30,9 @@ export const externalLinkContexts = [ 'generic', 'group', 'track', + + 'composerRelease', + 'officialRelease', ]; export const isExternalLinkContext = @@ -257,6 +258,30 @@ export const externalLinkSpec = [ }, { + match: { + domain: '.bandcamp.com', + context: 'composerRelease', + }, + + platform: 'bandcamp.composerRelease', + handle: {domain: /^[^.]+/}, + + icon: 'bandcamp', + }, + + { + match: { + domain: '.bandcamp.com', + context: 'officialRelease', + }, + + platform: 'bandcamp.officialRelease', + handle: {domain: /^[^.]+/}, + + icon: 'bandcamp', + }, + + { match: {domain: '.bandcamp.com'}, platform: 'bandcamp', diff --git a/src/find-reverse.js b/src/find-reverse.js index 6a67ac0f..c99a4a71 100644 --- a/src/find-reverse.js +++ b/src/find-reverse.js @@ -11,7 +11,7 @@ export function getAllSpecs({ }) { try { thingConstructors; - } catch (error) { + } catch { throw new Error(`Thing constructors aren't ready yet, can't get all ${word} specs`); } @@ -52,7 +52,7 @@ export function findSpec(key, { try { thingConstructors; - } catch (error) { + } catch { throw new Error(`Thing constructors aren't ready yet, can't check if "${word}.${key}" available`); } diff --git a/src/find.js b/src/find.js index e7f5cda1..8f2170d4 100644 --- a/src/find.js +++ b/src/find.js @@ -56,11 +56,14 @@ export function processAvailableMatchesByName(data, { if (normalizedName in multipleNameMatches) { multipleNameMatches[normalizedName].push(thing); } else { - multipleNameMatches[normalizedName] = [results[normalizedName], thing]; + multipleNameMatches[normalizedName] = [ + results[normalizedName].thing, + thing, + ]; results[normalizedName] = null; } } else { - results[normalizedName] = thing; + results[normalizedName] = {thing, name}; } } } @@ -87,7 +90,7 @@ export function processAvailableMatchesByDirectory(data, { continue; } - results[directory] = thing; + results[directory] = {thing, directory}; } } @@ -117,13 +120,50 @@ function oopsMultipleNameMatches(mode, { `Returning null for this reference.`); } +function oopsNameCapitalizationMismatch(mode, { + matchingName, + matchedName, +}) { + if (matchingName.length === matchedName.length) { + let a = '', b = ''; + for (let i = 0; i < matchingName.length; i++) { + if ( + matchingName[i] === matchedName[i] || + matchingName[i].toLowerCase() !== matchingName[i].toLowerCase() + ) { + a += matchingName[i]; + b += matchedName[i]; + } else { + a += colors.bright(colors.red(matchingName[i])); + b += colors.bright(colors.green(matchedName[i])); + } + } + + matchingName = a; + matchedName = b; + } + + return warnOrThrow(mode, + `Provided capitalization differs from the matched name. Please resolve:\n` + + `- provided: ${matchingName}\n` + + `- should be: ${matchedName}\n` + + `Returning null for this reference.`); +} + export function prepareMatchByName(mode, {byName, multipleNameMatches}) { return (name) => { const normalizedName = name.toLowerCase(); const match = byName[normalizedName]; if (match) { - return match; + if (name === match.name) { + return match.thing; + } else { + return oopsNameCapitalizationMismatch(mode, { + matchingName: name, + matchedName: match.name, + }); + } } else if (multipleNameMatches[normalizedName]) { return oopsMultipleNameMatches(mode, { name, @@ -154,7 +194,13 @@ export function prepareMatchByDirectory(mode, {referenceTypes, byDirectory}) { }); } - return byDirectory[directory]; + const match = byDirectory[directory]; + + if (match) { + return match.thing; + } else { + return null; + } }; } @@ -345,7 +391,13 @@ function findMixedHelper(config) { }); } - return byDirectory[referenceType][directory]; + const match = byDirectory[referenceType][directory]; + + if (match) { + return match.thing; + } else { + return null; + } }, matchByName: diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 97cf74a9..40505189 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -162,7 +162,6 @@ import { import dimensionsOf from 'image-size'; -import CacheableObject from '#cacheable-object'; import {stringifyCache} from '#cli'; import {commandExists, isMain, promisifyProcess, traverse} from '#node-utils'; import {sortByName} from '#sort'; @@ -447,7 +446,7 @@ async function getImageMagickVersion(binary) { try { await promisifyProcess(proc, false); - } catch (error) { + } catch { return null; } @@ -556,42 +555,56 @@ async function determineThumbtacksNeededForFile({ return mismatchedWithinRightSize; } -async function generateImageThumbnail(imagePath, thumbtack, { +// Write all requested thumbtacks for a source image in one pass +// This saves a lot of disk reads which are probably the main bottleneck +function prepareConvertArgs(filePathInMedia, dirnameInCache, thumbtacks) { + const args = [filePathInMedia, '-strip']; + + const basename = + path.basename(filePathInMedia, path.extname(filePathInMedia)); + + // do larger sizes first + thumbtacks.sort((a, b) => thumbnailSpec[b].size - thumbnailSpec[a].size); + + for (const tack of thumbtacks) { + const {size, quality} = thumbnailSpec[tack]; + const filename = `${basename}.${tack}.jpg`; + const filePathInCache = path.join(dirnameInCache, filename); + args.push( + '(', '+clone', + '-resize', `${size}x${size}>`, + '-interlace', 'Plane', + '-quality', `${quality}%`, + '-write', filePathInCache, + '+delete', ')', + ); + } + + // throw away the (already written) image stream + args.push('null:'); + + return args; +} + +async function generateImageThumbnails(imagePath, thumbtacks, { mediaPath, mediaCachePath, spawnConvert, }) { + if (empty(thumbtacks)) return; + const filePathInMedia = path.join(mediaPath, imagePath); const dirnameInCache = path.join(mediaCachePath, path.dirname(imagePath)); - const filename = - path.basename(imagePath, path.extname(imagePath)) + - `.${thumbtack}.jpg`; - - const filePathInCache = - path.join(dirnameInCache, filename); - await mkdir(dirnameInCache, {recursive: true}); - const specEntry = thumbnailSpec[thumbtack]; - const {size, quality} = specEntry; - - const convertProcess = spawnConvert([ - filePathInMedia, - '-strip', - '-resize', - `${size}x${size}>`, - '-interlace', - 'Plane', - '-quality', - `${quality}%`, - filePathInCache, - ]); - - await promisifyProcess(convertProcess, false); + const convertArgs = + prepareConvertArgs(filePathInMedia, dirnameInCache, thumbtacks); + + await promisifyProcess(spawnConvert(convertArgs), false); } export async function determineMediaCachePath({ @@ -628,7 +641,7 @@ export async function determineMediaCachePath({ try { const files = await readdir(mediaPath); mediaIncludesThumbnailCache = files.includes(CACHE_FILE); - } catch (error) { + } catch { mediaIncludesThumbnailCache = false; } @@ -861,7 +874,7 @@ export async function migrateThumbsIntoDedicatedCacheDirectory({ path.join(mediaPath, CACHE_FILE), path.join(mediaCachePath, CACHE_FILE)); logInfo`Moved thumbnail cache file.`; - } catch (error) { + } catch { logWarn`Failed to move cache file. (${CACHE_FILE})`; logWarn`Check its permissions, or try copying/pasting.`; } @@ -1100,33 +1113,23 @@ export default async function genThumbs({ const writeMessageFn = () => `Writing image thumbnails. [failed: ${numFailed}]`; - const generateCallImageIndices = - imageThumbtacksNeeded - .flatMap(({length}, index) => - Array.from({length}, () => index)); - - const generateCallImagePaths = - generateCallImageIndices - .map(index => imagePaths[index]); - - const generateCallThumbtacks = - imageThumbtacksNeeded.flat(); - const generateCallFns = stitchArrays({ - imagePath: generateCallImagePaths, - thumbtack: generateCallThumbtacks, - }).map(({imagePath, thumbtack}) => () => - generateImageThumbnail(imagePath, thumbtack, { + imagePath: imagePaths, + thumbtacks: imageThumbtacksNeeded, + }).map(({imagePath, thumbtacks}) => () => + generateImageThumbnails(imagePath, thumbtacks, { mediaPath, mediaCachePath, spawnConvert, }).catch(error => { numFailed++; - return ({error}); + return {error}; })); - logInfo`Generating ${generateCallFns.length} thumbnails for ${imagePaths.length} media files.`; + const totalThumbs = imageThumbtacksNeeded.reduce((sum, tacks) => sum + tacks.length, 0); + + logInfo`Generating ${totalThumbs} thumbnails for ${imagePaths.length} media files.`; if (generateCallFns.length > 500) { logInfo`Go get a latte - this could take a while!`; } @@ -1135,37 +1138,30 @@ export default async function genThumbs({ await progressPromiseAll(writeMessageFn, queue(generateCallFns, magickThreads)); - let successfulIndices; + let successfulPaths; { - const erroredIndices = generateCallImageIndices.slice(); - const erroredPaths = generateCallImagePaths.slice(); - const erroredThumbtacks = generateCallThumbtacks.slice(); + const erroredPaths = imagePaths.slice(); const errors = generateCallResults.map(result => result?.error); const {removed} = filterMultipleArrays( - erroredIndices, erroredPaths, - erroredThumbtacks, errors, - (_index, _imagePath, _thumbtack, error) => error); + (_imagePath, error) => error); - successfulIndices = new Set(removed[0]); - - const chunks = - chunkMultipleArrays(erroredPaths, erroredThumbtacks, errors, - (imagePath, lastImagePath) => imagePath !== lastImagePath); + ([successfulPaths] = removed); // TODO: This should obviously be an aggregate error. // ...Just like every other error report here, and those dang aggregates // should be constructable from within the queue, rather than after. - for (const [[imagePath], thumbtacks, errors] of chunks) { - logError`Failed to generate thumbnails for ${imagePath}:`; - for (const {thumbtack, error} of stitchArrays({thumbtack: thumbtacks, error: errors})) { - logError`- ${thumbtack}: ${error}`; - } - } + stitchArrays({ + imagePath: erroredPaths, + error: errors, + }).forEach(({imagePath, error}) => { + logError`Failed to generate thumbnails for ${imagePath}:`; + logError`- ${error}`; + }); if (empty(errors)) { logInfo`All needed thumbnails generated successfully - nice!`; @@ -1179,8 +1175,8 @@ export default async function genThumbs({ imagePaths, imageThumbtacksNeeded, imageDimensions, - (_imagePath, _thumbtacksNeeded, _dimensions, index) => - successfulIndices.has(index)); + (imagePath, _thumbtacksNeeded, _dimensions) => + successfulPaths.includes(imagePath)); for (const { imagePath, @@ -1251,6 +1247,11 @@ export function getExpectedImagePaths(mediaPath, {urls, wikiData}) { .filter(part => part.asset) .map(part => fromRoot.to('media.albumWallpaperPart', album.directory, part.asset))), + + wikiData.wikiInfo.wikiWallpaperParts + .filter(part => part.asset) + .map(part => + fromRoot.to('media.path', part.asset)), ].flat(); sortByName(paths, {getName: path => path}); diff --git a/src/html.js b/src/html.js index 9e4c39ab..dd1d1960 100644 --- a/src/html.js +++ b/src/html.js @@ -53,6 +53,17 @@ export const attributeSpec = { }, }; +let disabledSlotValidation = false; +let disabledTagTracing = false; + +export function disableSlotValidation() { + disabledSlotValidation = true; +} + +export function disableTagTracing() { + disabledTagTracing = true; +} + // Pass to tag() as an attributes key to make tag() return a 8lank tag if the // provided content is empty. Useful for when you'll only 8e showing an element // according to the presence of content that would 8elong there. @@ -223,7 +234,11 @@ export function isBlank(content) { // could include content. These need to be checked too. // Check each of the templates one at a time. for (const template of result) { - const content = template.content; + // Resolve the content all the way down to a tag - + // if it's a template that returns another template, + // that won't do, because we need to detect if its + // final content is a tag marked onlyIfSiblings. + const content = normalize(template); if (content instanceof Tag && content.onlyIfSiblings) { continue; @@ -352,8 +367,10 @@ export class Tag { this.attributes = attributes; this.content = content; - this.#traceError = new Error(); - } + if (!disabledTagTracing) { + this.#traceError = new Error(); + } +} clone() { return Reflect.construct(this.constructor, [ @@ -583,7 +600,7 @@ export class Tag { try { this.content = this.content; - } catch (error) { + } catch { this.#setAttributeFlag(imaginarySibling, false); } } @@ -706,17 +723,19 @@ export class Tag { `of ${inspect(this, {compact: true})}`, {cause: caughtError}); - error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true; - error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = this.#traceError; + if (this.#traceError && !disabledTagTracing) { + error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true; + error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = this.#traceError; - error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [ - /content-function\.js/, - /util\/html\.js/, - ]; + error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [ + /content-function\.js/, + /util\/html\.js/, + ]; - error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [ - /content\/dependencies\/(.*\.js:.*(?=\)))/, - ]; + error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [ + /content\/dependencies\/(.*\.js:.*(?=\)))/, + ]; + } throw error; } @@ -1131,6 +1150,34 @@ export class Attributes { } add(...args) { + // Very common case: add({class: 'foo', id: 'bar'})¡ + // The argument is a plain object (no Template, no Attributes, + // no blessAttributes symbol). We can skip the expensive + // isAttributesAdditionSinglet() validation and flatten/array handling. + if ( + args.length === 1 && + args[0] && + typeof args[0] === 'object' && + !Array.isArray(args[0]) && + !(args[0] instanceof Attributes) && + !(args[0] instanceof Template) && + !Object.hasOwn(args[0], blessAttributes) + ) { + const obj = args[0]; + + // Preserve existing merge semantics by funnelling each key through + // the internal #addOneAttribute helper (handles class/style union, + // unique merging, etc.) but avoid *per-object* validation overhead. + for (const key of Reflect.ownKeys(obj)) { + this.#addOneAttribute(key, obj[key]); + } + + // Match the original return style (list of results) so callers that + // inspect the return continue to work. + return obj; + } + + // Fall back to the original slow-but-thorough implementation switch (args.length) { case 1: isAttributesAdditionSinglet(args[0]); @@ -1142,10 +1189,11 @@ export class Attributes { default: throw new Error( - `Expected array or object, or attribute and value`); + 'Expected array or object, or attribute and value'); } } + with(...args) { const clone = this.clone(); clone.add(...args); @@ -1739,6 +1787,10 @@ export class Template { } static validateSlotValueAgainstDescription(value, description) { + if (disabledSlotValidation) { + return true; + } + if (value === undefined) { throw new TypeError(`Specify value as null or don't specify at all`); } diff --git a/src/replacer.js b/src/replacer.js index 0698eced..779ee78d 100644 --- a/src/replacer.js +++ b/src/replacer.js @@ -8,7 +8,7 @@ import * as marked from 'marked'; import * as html from '#html'; -import {escapeRegex, typeAppearance} from '#sugar'; +import {empty, escapeRegex, typeAppearance} from '#sugar'; import {matchMarkdownLinks} from '#wiki-data'; export const replacerSpec = { @@ -464,7 +464,7 @@ export function squashBackslashes(text) { // a set of characters where the backslash carries meaning // into later formatting (i.e. markdown). Note that we do // NOT compress double backslashes into single backslashes. - return text.replace(/([^\\](?:\\{2})*)\\(?![\\*_~>-])/g, '$1'); + return text.replace(/([^\\](?:\\{2})*)\\(?![\\*_~>.-])/g, '$1'); } export function restoreRawHTMLTags(text) { @@ -526,6 +526,7 @@ export function postprocessComments(inputNodes) { function postprocessHTMLTags(inputNodes, tagName, callback) { const outputNodes = []; + const errors = []; const lastNode = inputNodes.at(-1); @@ -593,10 +594,16 @@ function postprocessHTMLTags(inputNodes, tagName, callback) { return false; })(); - outputNodes.push( - callback(attributes, { - inline, - })); + try { + outputNodes.push( + callback(attributes, { + inline, + })); + } catch (caughtError) { + errors.push(new Error( + `Failed to process ${match[0]}`, + {cause: caughtError})); + } // No longer at the start of a line after the tag - there will at // least be text with only '\n' before the next of this tag that's @@ -619,15 +626,33 @@ function postprocessHTMLTags(inputNodes, tagName, callback) { outputNodes.push(node); } + if (!empty(errors)) { + throw new AggregateError( + errors, + `Errors postprocessing <${tagName}> tags`); + } + return outputNodes; } +function complainAboutMediaSrc(src) { + if (!src) { + throw new Error(`Missing "src" attribute`); + } + + if (src.startsWith('/media/')) { + throw new Error(`Start "src" with "media/", not "/media/"`); + } +} + export function postprocessImages(inputNodes) { return postprocessHTMLTags(inputNodes, 'img', (attributes, {inline}) => { const node = {type: 'image'}; node.src = attributes.get('src'); + complainAboutMediaSrc(node.src); + node.inline = attributes.get('inline') ?? inline; if (attributes.get('link')) node.link = attributes.get('link'); @@ -648,10 +673,13 @@ export function postprocessImages(inputNodes) { export function postprocessVideos(inputNodes) { return postprocessHTMLTags(inputNodes, 'video', - attributes => { + (attributes, {inline}) => { const node = {type: 'video'}; node.src = attributes.get('src'); + complainAboutMediaSrc(node.src); + + node.inline = attributes.get('inline') ?? inline; if (attributes.get('width')) node.width = parseInt(attributes.get('width')); if (attributes.get('height')) node.height = parseInt(attributes.get('height')); @@ -668,8 +696,12 @@ export function postprocessAudios(inputNodes) { const node = {type: 'audio'}; node.src = attributes.get('src'); + complainAboutMediaSrc(node.src); + node.inline = attributes.get('inline') ?? inline; + if (attributes.get('align')) node.align = attributes.get('align'); + if (attributes.get('nameless')) node.nameless = true; return node; }); @@ -821,54 +853,108 @@ export function postprocessExternalLinks(inputNodes) { return outputNodes; } -export function parseContentNodes(input) { +export function parseContentNodes(input, { + errorMode = 'throw', +} = {}) { if (typeof input !== 'string') { throw new TypeError(`Expected input to be string, got ${typeAppearance(input)}`); } - try { - let output = parseNodes(input, 0); - output = postprocessComments(output); - output = postprocessImages(output); - output = postprocessVideos(output); - output = postprocessAudios(output); - output = postprocessHeadings(output); - output = postprocessSummaries(output); - output = postprocessExternalLinks(output); - return output; - } catch (errorNode) { - if (errorNode.type !== 'error') { - throw errorNode; - } + let result = null, error = null; - const { - i, - data: {message}, - } = errorNode; + process: { + try { + result = parseNodes(input, 0); + } catch (caughtError) { + if (caughtError.type === 'error') { + const {i, data: {message}} = caughtError; - let lineStart = input.slice(0, i).lastIndexOf('\n'); - if (lineStart >= 0) { - lineStart += 1; - } else { - lineStart = 0; + let lineStart = input.slice(0, i).lastIndexOf('\n'); + if (lineStart >= 0) { + lineStart += 1; + } else { + lineStart = 0; + } + + let lineEnd = input.slice(i).indexOf('\n'); + if (lineEnd >= 0) { + lineEnd += i; + } else { + lineEnd = input.length; + } + + const line = input.slice(lineStart, lineEnd); + + const cursor = i - lineStart; + + error = + new SyntaxError( + `Parse error (at pos ${i}): ${message}\n` + + line + `\n` + + '-'.repeat(cursor) + '^'); + } else { + error = caughtError; + } + + // A parse error means there's no output to continue with at all, + // so stop here. + break process; } - let lineEnd = input.slice(i).indexOf('\n'); - if (lineEnd >= 0) { - lineEnd += i; - } else { - lineEnd = input.length; + const postprocessErrors = []; + + for (const postprocess of [ + postprocessComments, + postprocessImages, + postprocessVideos, + postprocessAudios, + postprocessHeadings, + postprocessSummaries, + postprocessExternalLinks, + ]) { + try { + result = postprocess(result); + } catch (caughtError) { + const error = + new Error( + `Error in step ${`"${postprocess.name}"`}`, + {cause: caughtError}); + + error[Symbol.for('hsmusic.aggregate.translucent')] = true; + + postprocessErrors.push(error); + } } - const line = input.slice(lineStart, lineEnd); + if (!empty(postprocessErrors)) { + error = + new AggregateError( + postprocessErrors, + `Errors postprocessing content text`); - const cursor = i - lineStart; + error[Symbol.for('hsmusic.aggregate.translucent')] = 'single'; + } + } + + if (errorMode === 'throw') { + if (error) { + throw error; + } else { + return result; + } + } else if (errorMode === 'return') { + if (!result) { + result = [{ + i: 0, + iEnd: input.length, + type: 'text', + data: input, + }]; + } - throw new SyntaxError([ - `Parse error (at pos ${i}): ${message}`, - line, - '-'.repeat(cursor) + '^', - ].join('\n')); + return {error, result}; + } else { + throw new Error(`Unknown errorMode ${errorMode}`); } } diff --git a/src/static/css/site.css b/src/static/css/site.css index 5934e206..a9ed90c6 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -61,7 +61,7 @@ body::before, .wallpaper-part { #page-container { max-width: 1100px; - margin: 0 auto 40px; + margin: 0 auto 38px; padding: 15px 0; } @@ -76,10 +76,25 @@ body::before, .wallpaper-part { height: unset; } +@property --banner-shine { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; +} + #banner { margin: 10px 0; width: 100%; position: relative; + + --banner-shine: 4%; + -webkit-box-reflect: below -12px linear-gradient(transparent, color-mix(in srgb, transparent, var(--banner-shine) white)); + transition: --banner-shine 0.8s; +} + +#banner:hover { + --banner-shine: 35%; + transition-delay: 0.3s; } #banner::after { @@ -261,7 +276,11 @@ body::before, .wallpaper-part { #page-container { background-color: var(--bg-color, rgba(35, 35, 35, 0.8)); color: #ffffff; - box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); + border-bottom: 2px solid #fff1; + box-shadow: + 0 0 40px #0008, + 0 2px 15px -3px #2221, + 0 2px 6px 2px #1113; } #skippers > * { @@ -1503,10 +1522,26 @@ s.spoiler::-moz-selection { background: white; } -span.path { - font-size: 0.9em; +span.path, code.filename { + font-size: 0.95em; font-family: "courier new", monospace; font-weight: 800; + background: #ccc3; + + padding: 0.05em 0.5ch; + border: 1px solid #ccce; + border-radius: 2px; + line-height: 1.4; +} + +.image-details code.filename { + margin-left: -0.4ch; + opacity: 0.8; +} + +.image-details code.filename:hover { + opacity: 1; + cursor: text; } span.path i { @@ -1679,6 +1714,11 @@ p.image-details.origin-details { margin-bottom: 2px; } +p.image-details.origin-details .origin-details { + display: block; + margin-top: 0.25em; +} + .cover-artwork-joiner { z-index: -2; } @@ -1750,6 +1790,15 @@ p.image-details.origin-details { color: var(--primary-color); } +.inherited-commentary-section { + clear: right; + margin-top: 1em; + margin-right: min(4vw, 60px); + border: 2px solid var(--deep-color); + border-radius: 4px; + background: #ffffff07; +} + .commentary-art { float: right; width: 30%; @@ -1777,11 +1826,20 @@ p.image-details.origin-details { padding-left: 40px; } -.lyrics-entry .lyrics-details { +.lyrics-entry .lyrics-details, +.lyrics-entry .origin-details { font-size: 0.9em; font-style: oblique; } +.lyrics-entry .lyrics-details { + margin-bottom: 0; +} + +.lyrics-entry .origin-details { + margin-top: 0.25em; +} + .js-hide, .js-show-once-data, .js-hide-once-data { @@ -1801,12 +1859,20 @@ p.image-details.origin-details { margin-bottom: 1.5em; } -a.align-center, img.align-center, audio.align-center { +.content-image-container.align-full { + width: 100%; +} + +a.align-center, img.align-center, audio.align-center, video.align-center { display: block; margin-left: auto; margin-right: auto; } +a.align-full, img.align-full, video.align-full { + width: 100%; +} + center { margin-top: 1em; margin-bottom: 1em; @@ -2134,6 +2200,13 @@ ul > li.has-details { margin-left: -17px; } +li .origin-details { + display: block; + margin-left: 2ch; + font-size: 0.9em; + font-style: oblique; +} + .album-group-list dt, .group-series-list dt { font-style: oblique; @@ -2184,31 +2257,54 @@ ul > li.has-details { #content hr { border: 1px inset #808080; - width: 100%; +} + +#content hr.split { + color: #808080; } #content hr.split::before { content: "(split)"; - color: #808080; } -#content hr.split { +#content hr.main-separator { + color: var(--dim-color); + clear: none; + margin-top: -0.25em; + margin-bottom: 1.75em; +} + +#content hr.main-separator::before { + content: "♦"; + font-size: 1.2em; +} + +#content hr.split, +#content hr.main-separator { position: relative; overflow: hidden; border: none; } -#content hr.split::after { +#content hr.split::after, +#content hr.main-separator::after { display: inline-block; content: ""; - border: 1px inset #808080; - width: 100%; + width: calc(100% - min(calc(8vw - 35px), 45px)); position: absolute; top: 50%; - margin-top: -2px; margin-left: 10px; } +#content hr.split::after { + border: 1px inset currentColor; + margin-top: -2px; +} + +#content hr.main-separator::after { + border-bottom: 1px solid currentColor; +} + li > ul { margin-top: 5px; } @@ -2581,6 +2677,7 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las .content-video-container, .content-audio-container { width: fit-content; + max-width: 100%; background-color: var(--dark-color); border: 2px solid var(--primary-color); border-radius: 2.5px 2.5px 3px 3px; @@ -2590,6 +2687,7 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las .content-video-container video, .content-audio-container audio { display: block; + max-width: 100%; } .content-video-container.align-center, @@ -2598,6 +2696,11 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las margin-right: auto; } +.content-video-container.align-full, +.content-audio-container.align-full { + width: 100%; +} + .content-audio-container .filename { color: white; font-family: monospace; @@ -2664,6 +2767,12 @@ img { object-fit: cover; } +p > img { + max-width: 100%; + object-fit: contain; + height: auto; +} + .image-inner-area::after { content: ""; display: block; diff --git a/src/static/js/client/hoverable-tooltip.js b/src/static/js/client/hoverable-tooltip.js index 9569de3e..89119a47 100644 --- a/src/static/js/client/hoverable-tooltip.js +++ b/src/static/js/client/hoverable-tooltip.js @@ -118,17 +118,17 @@ export function registerTooltipElement(tooltip) { handleTooltipMouseLeft(tooltip); }); - tooltip.addEventListener('focusin', event => { - handleTooltipReceivedFocus(tooltip, event.relatedTarget); + tooltip.addEventListener('focusin', domEvent => { + handleTooltipReceivedFocus(tooltip, domEvent.relatedTarget); }); - tooltip.addEventListener('focusout', event => { + tooltip.addEventListener('focusout', domEvent => { // This event gets activated for tabbing *between* links inside the // tooltip, which is no good and certainly doesn't represent the focus // leaving the tooltip. - if (currentlyShownTooltipHasFocus(event.relatedTarget)) return; + if (currentlyShownTooltipHasFocus(domEvent.relatedTarget)) return; - handleTooltipLostFocus(tooltip, event.relatedTarget); + handleTooltipLostFocus(tooltip, domEvent.relatedTarget); }); } @@ -158,20 +158,20 @@ export function registerTooltipHoverableElement(hoverable, tooltip) { handleTooltipHoverableMouseLeft(hoverable); }); - hoverable.addEventListener('focusin', event => { - handleTooltipHoverableReceivedFocus(hoverable, event); + hoverable.addEventListener('focusin', domEvent => { + handleTooltipHoverableReceivedFocus(hoverable, domEvent); }); - hoverable.addEventListener('focusout', event => { - handleTooltipHoverableLostFocus(hoverable, event); + hoverable.addEventListener('focusout', domEvent => { + handleTooltipHoverableLostFocus(hoverable, domEvent); }); - hoverable.addEventListener('touchend', event => { - handleTooltipHoverableTouchEnded(hoverable, event); + hoverable.addEventListener('touchend', domEvent => { + handleTooltipHoverableTouchEnded(hoverable, domEvent); }); - hoverable.addEventListener('click', event => { - handleTooltipHoverableClicked(hoverable, event); + hoverable.addEventListener('click', domEvent => { + handleTooltipHoverableClicked(hoverable, domEvent); }); } @@ -416,7 +416,7 @@ function handleTooltipHoverableTouchEnded(hoverable, domEvent) { }, 1200); } -function handleTooltipHoverableClicked(hoverable) { +function handleTooltipHoverableClicked(hoverable, domEvent) { const {state} = info; // Don't navigate away from the page if the this hoverable was recently @@ -426,7 +426,7 @@ function handleTooltipHoverableClicked(hoverable) { state.currentlyActiveHoverable === hoverable && state.hoverableWasRecentlyTouched ) { - event.preventDefault(); + domEvent.preventDefault(); } } diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index c8f42e91..eae1e74e 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -1305,7 +1305,7 @@ async function handleDroppedIntoSearchInput(domEvent) { let droppedURL; try { droppedURL = new URL(droppedText); - } catch (error) { + } catch { droppedURL = null; } diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js index c3002b18..e32b4ad5 100644 --- a/src/static/js/search-worker.js +++ b/src/static/js/search-worker.js @@ -130,7 +130,7 @@ async function loadDatabase() { try { idb = await promisifyIDBRequest(request); - } catch (error) { + } catch { console.warn(`Couldn't load search IndexedDB - won't use an internal cache.`); console.warn(request.error); idb = null; diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 74b05b74..ecc0cc14 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -370,10 +370,14 @@ releaseInfo: _: "Read {LINK}." link: "artist commentary" - readCreditSources: + readCreditingSources: _: "Read {LINK}." link: "crediting sources" + readReferencingSources: + _: "Read {LINK}." + link: "referencing sources" + additionalFiles: heading: "View or download additional files:" @@ -488,7 +492,8 @@ misc: # artistCommentary: artistCommentary: - _: "Artist commentary:" + _: "Artist commentary for {THING}:" + sticky: "Artist commentary:" entry: title: @@ -515,13 +520,10 @@ misc: info: fromMainRelease: >- - This commentary is properly placed on this track's main release, {ALBUM}. + The following commentary is properly placed on this track's main release, {ALBUM}. fromMainRelease.namedDifferently: >- - This commentary is properly placed on this track's main release, {ALBUM}, where it's named {NAME}. - - releaseSpecific: >- - This commentary is specific to this release, {ALBUM}. + The following commentary is properly placed on this track's main release, {ALBUM}, where it's named {NAME}. seeSpecificReleases: >- For release-specific commentary, check out: {ALBUMS}. @@ -618,8 +620,9 @@ misc: trackArt: "{INDEX} track art by {ARTIST}" onlyIndex: "Only" - creditSources: - _: "Crediting sources:" + creditingSources: + _: "Crediting sources for {THING}:" + sticky: "Crediting sources:" # external: # Links which will generally bring you somewhere off of the wiki. @@ -647,7 +650,12 @@ misc: amazonMusic: "Amazon Music" appleMusic: "Apple Music" artstation: "ArtStation" - bandcamp: "Bandcamp" + + bandcamp: + _: "Bandcamp" + + composerRelease: "Bandcamp (composer's release)" + officialRelease: "Bandcamp (official release)" bgreco: _: "bgreco.net" @@ -838,6 +846,10 @@ misc: group: "Groups" track: "Tracks" + referencingSources: + _: "Referencing sources for {THING}:" + sticky: "Referencing sources:" + # skippers: # # These are navigational links that only show up when you're @@ -865,7 +877,7 @@ misc: # Displayed on various info pages. artistCommentary: "Artist commentary" - creditSources: "Crediting sources" + creditingSources: "Crediting sources" # Displayed on artist info page. @@ -886,6 +898,7 @@ misc: sampledBy: "Sampled by..." features: "Features..." featuredIn: "Featured in..." + referencingSources: "Referencing sources" lyrics: "Lyrics" @@ -1322,6 +1335,16 @@ artistPage: flash: _: "{FLASH}" + artwork.accent: + withLabel: >- + {LABEL} + + withAnnotation: >- + {ANNOTATION} + + withLabel.withAnnotation: >- + {LABEL}: {ANNOTATION} + # contributedDurationLine: # This is shown at the top of the artist's track list, provided # any of their tracks have durations at all. diff --git a/src/upd8.js b/src/upd8.js index 86ecab69..ae072d5a 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -42,8 +42,9 @@ import wrap from 'word-wrap'; import {mapAggregate, openAggregate, showAggregate} from '#aggregate'; import CacheableObject from '#cacheable-object'; -import {stringifyCache} from '#cli'; +import {formatDuration, stringifyCache} from '#cli'; import {displayCompositeCacheAnalysis} from '#composite'; +import * as html from '#html'; import find, {bindFind, getAllFindSpecs} from '#find'; import {processLanguageFile, watchLanguageFile, internalDefaultStringsFile} from '#language'; @@ -117,7 +118,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); let COMMIT; try { COMMIT = execSync('git log --format="%h %B" -n 1 HEAD', {cwd: __dirname}).toString().trim(); -} catch (error) { +} catch { COMMIT = '(failed to detect)'; } @@ -518,6 +519,11 @@ async function main() { type: 'flag', }, + 'skip-self-diagnosis': { + help: `Disable some runtime validation for the wiki's own code, which speeds up long builds, but may allow unpredicted corner cases to fail strangely and silently`, + type: 'flag', + }, + 'queue-size': { help: `Process more or fewer disk files at once to optimize performance or avoid I/O errors, unlimited if set to 0 (between 500 and 700 is usually a safe range for building HSMusic on Windows machines)\nDefaults to ${defaultQueueSize}`, type: 'value', @@ -656,7 +662,8 @@ async function main() { const thumbsOnly = cliOptions['thumbs-only'] ?? false; const noInput = cliOptions['no-input'] ?? false; - const showAggregateTraces = cliOptions['show-traces'] ?? false; + const skipSelfDiagnosis = cliOptions['skip-self-diagnosis'] ?? false; + const showTraces = cliOptions['show-traces'] ?? false; const precacheMode = cliOptions['precache-mode'] ?? 'common'; @@ -1156,6 +1163,18 @@ async function main() { return false; } + if (skipSelfDiagnosis) { + logWarn`${'Skipping code self-diagnosis.'} (--skip-self-diagnosis provided)`; + logWarn`This build should run substantially faster, but corner cases`; + logWarn`not previously predicted may fail strangely and silently.`; + + html.disableSlotValidation(); + } + + if (!showTraces) { + html.disableTagTracing(); + } + Object.assign(stepStatusSummary.determineMediaCachePath, { status: STATUS_STARTED_NOT_DONE, timeStart: Date.now(), @@ -1334,7 +1353,7 @@ async function main() { const niceShowAggregate = (error, ...opts) => { showAggregate(error, { - showTraces: showAggregateTraces, + showTraces, pathToFileURL: (f) => path.relative(__dirname, fileURLToPath(f)), ...opts, }); @@ -2419,7 +2438,7 @@ async function main() { }); internalDefaultLanguage = internalDefaultLanguageWatcher.language; - } catch (_error) { + } catch { // No need to display the error here - it's already printed by // watchLanguageFile. errorLoadingInternalDefaultLanguage = true; @@ -3207,6 +3226,7 @@ async function main() { developersComment, languages, missingImagePaths, + niceShowAggregate, thumbsCache, urlSpec, urls, @@ -3269,7 +3289,7 @@ async function main() { } // TODO: isMain detection isn't consistent across platforms here -/* eslint-disable-next-line no-constant-condition */ +// eslint-disable-next-line no-constant-binary-expression if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmusic') { (async () => { let result; @@ -3363,23 +3383,6 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus })(); } -function formatDuration(timeDelta) { - const seconds = timeDelta / 1000; - - if (seconds > 90) { - const modSeconds = Math.floor(seconds % 60); - const minutes = Math.floor(seconds - seconds % 60) / 60; - return `${minutes}m${modSeconds}s`; - } - - if (seconds < 0.1) { - return 'instant'; - } - - const precision = (seconds > 1 ? 3 : 2); - return `${seconds.toPrecision(precision)}s`; -} - function showStepStatusSummary() { const longestNameLength = Math.max(... diff --git a/src/urls-default.yaml b/src/urls-default.yaml index 74225efd..cbdd8a23 100644 --- a/src/urls-default.yaml +++ b/src/urls-default.yaml @@ -11,7 +11,7 @@ yamlAliases: # part of a build. This is so that multiple builds of a wiki can coexist # served from the same server / file system root: older builds' HTML files # refer to earlier values of STATIC_VERSION, avoiding name collisions. - - &staticVersion 5p1 + - &staticVersion 5p2 data: prefix: 'data/' diff --git a/src/urls.js b/src/urls.js index 9cc4a554..b51ea459 100644 --- a/src/urls.js +++ b/src/urls.js @@ -129,6 +129,9 @@ export function generateURLs(urlSpec) { if (template === null) { // Self-diagnose, brutally. + // TODO: This variable isn't used, and has never been used, + // but it might have been *meant* to be used? + // eslint-disable-next-line no-unused-vars const otherTemplateKey = (device ? 'posix' : 'device'); const {value: {[templateKey]: otherTemplate}} = diff --git a/src/web-routes.js b/src/web-routes.js index b68dccbf..a7115bbd 100644 --- a/src/web-routes.js +++ b/src/web-routes.js @@ -4,8 +4,10 @@ import {fileURLToPath} from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +/* eslint-disable no-unused-vars */ const codeSrcPath = __dirname; const codeRootPath = path.resolve(codeSrcPath, '..'); +/* eslint-enable no-unused-vars */ function getNodeDependencyRootPath(dependencyName) { return ( diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index d55ab215..afbf8b2f 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -24,6 +24,7 @@ export function bindUtilities({ language, languages, missingImagePaths, + niceShowAggregate, pagePath, pagePathStringFromRoot, thumbsCache, @@ -42,6 +43,7 @@ export function bindUtilities({ language, languages, missingImagePaths, + niceShowAggregate, pagePath, pagePathStringFromRoot, thumb, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index ecb9df21..5dece8d0 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -253,7 +253,7 @@ export async function go({ let url; try { url = new URL(request.url, `http://${request.headers.host}`); - } catch (error) { + } catch { response.writeHead(500, contentTypePlain); response.end('Failed to parse request URL\n'); return; @@ -300,7 +300,7 @@ export async function go({ let filePath; try { filePath = path.resolve(localDirectory, decodeURI(safePath.split('/').join(path.sep))); - } catch (error) { + } catch { response.writeHead(404, contentTypePlain); response.end(`File not found for: ${safePath}`); console.log(`${requestHead} [404] ${pathname}`); diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index d363811d..b5ded04c 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -106,8 +106,6 @@ export async function go({ universalUtilities, - mediaPath, - defaultLanguage, languages, urls, |