diff options
Diffstat (limited to 'src/content/dependencies')
85 files changed, 3795 insertions, 3413 deletions
diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js index f504cf80..68120b23 100644 --- a/src/content/dependencies/generateAdditionalFilesList.js +++ b/src/content/dependencies/generateAdditionalFilesList.js @@ -15,6 +15,8 @@ export default { generate: (slots, {html}) => html.tag('ul', {class: 'additional-files-list'}, + {[html.onlyIfContent]: true}, + stitchArrays({ chunk: slots.chunks, items: slots.chunkItems, diff --git a/src/content/dependencies/generateAdditionalFilesListChunk.js b/src/content/dependencies/generateAdditionalFilesListChunk.js index 5804115a..e66560fc 100644 --- a/src/content/dependencies/generateAdditionalFilesListChunk.js +++ b/src/content/dependencies/generateAdditionalFilesListChunk.js @@ -17,37 +17,31 @@ export default { }, }, - generate(slots, {html, language}) { - const summary = - html.tag('summary', - html.tag('span', - language.$('releaseInfo.additionalFiles.entry', { - title: - html.tag('span', {class: 'group-name'}, - slots.title), - }))); - - const description = - html.tag('li', {class: 'entry-description'}, - {[html.onlyIfContent]: true}, - slots.description); - - const items = - (html.isBlank(slots.items) - ? html.tag('li', - language.$('releaseInfo.additionalFiles.entry.noFilesAvailable')) - : slots.items); - - const content = - html.tag('ul', [description, items]); - - const details = - html.tag('details', - html.isBlank(slots.items) && - {open: true}, - - [summary, content]); - - return html.tag('li', details); - }, + generate: (slots, {html, language}) => + language.encapsulate('releaseInfo.additionalFiles.entry', capsule => + html.tag('li', + html.tag('details', + html.isBlank(slots.items) && + {open: true}, + + [ + html.tag('summary', + html.tag('span', + language.$(capsule, { + title: + html.tag('span', {class: 'group-name'}, + slots.title), + }))), + + html.tag('ul', [ + html.tag('li', {class: 'entry-description'}, + {[html.onlyIfContent]: true}, + slots.description), + + (html.isBlank(slots.items) + ? html.tag('li', + language.$(capsule, 'noFilesAvailable')) + : slots.items), + ]), + ]))), }; diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 7879269f..c14640af 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -1,4 +1,4 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { contentDependencies: [ @@ -130,11 +130,11 @@ export default { return data; }, - generate(data, relations, {html, language}) { - return relations.layout - .slots({ + generate: (data, relations, {html, language}) => + language.encapsulate('albumCommentaryPage', pageCapsule => + relations.layout.slots({ title: - language.$('albumCommentaryPage.title', { + language.$(pageCapsule, 'title', { album: data.name, }), @@ -146,7 +146,7 @@ export default { mainClasses: ['long-content'], mainContent: [ html.tag('p', - language.$('albumCommentaryPage.infoLine', { + language.$(pageCapsule, 'infoLine', { words: html.tag('b', language.formatWordCount(data.wordCount, {unit: true})), @@ -156,34 +156,41 @@ export default { language.countCommentaryEntries(data.entryCount, {unit: true})), })), - relations.albumCommentaryEntries && [ - relations.albumCommentaryHeading.slots({ - tag: 'h3', - color: data.color, - - title: - language.$('albumCommentaryPage.entry.title.albumCommentary', { - album: relations.albumCommentaryLink, - }), - - accent: - !empty(relations.albumCommentaryListeningLinks) && - language.$('albumCommentaryPage.entry.title.albumCommentary.accent', { - listeningLinks: - language.formatUnitList( - relations.albumCommentaryListeningLinks - .map(link => link.slots({ - context: 'album', - tab: 'separate', - }))), - }), - }), - - relations.albumCommentaryCover - ?.slots({mode: 'commentary'}), - - relations.albumCommentaryEntries, - ], + relations.albumCommentaryEntries && + language.encapsulate(pageCapsule, 'entry', entryCapsule => [ + language.encapsulate(entryCapsule, 'title.albumCommentary', titleCapsule => + relations.albumCommentaryHeading.slots({ + tag: 'h3', + color: data.color, + + title: + language.$(titleCapsule, { + album: relations.albumCommentaryLink, + }), + + stickyTitle: + language.$(titleCapsule, 'sticky', { + album: data.name, + }), + + accent: + language.$(titleCapsule, 'accent', { + [language.onlyIfOptions]: ['listeningLinks'], + listeningLinks: + language.formatUnitList( + relations.albumCommentaryListeningLinks + .map(link => link.slots({ + context: 'album', + tab: 'separate', + }))), + }), + })), + + relations.albumCommentaryCover + ?.slots({mode: 'commentary'}), + + relations.albumCommentaryEntries, + ]), stitchArrays({ heading: relations.trackCommentaryHeadings, @@ -201,31 +208,33 @@ export default { cover, entries, color, - }) => [ - heading.slots({ - tag: 'h3', - id: directory, - color, - - title: - language.$('albumCommentaryPage.entry.title.trackCommentary', { - track: link, - }), - - accent: - !empty(listeningLinks) && - language.$('albumCommentaryPage.entry.title.trackCommentary.accent', { - listeningLinks: - language.formatUnitList( - listeningLinks.map(link => - link.slot('tab', 'separate'))), - }), - }), + }) => + language.encapsulate(pageCapsule, 'entry', entryCapsule => [ + language.encapsulate(entryCapsule, 'title.trackCommentary', titleCapsule => + heading.slots({ + tag: 'h3', + attributes: {id: directory}, + color, + + title: + language.$(titleCapsule, { + track: link, + }), + + accent: + language.$(titleCapsule, 'accent', { + [language.onlyIfOptions]: ['listeningLinks'], + listeningLinks: + language.formatUnitList( + listeningLinks.map(link => + link.slot('tab', 'separate'))), + }), + })), cover?.slots({mode: 'commentary'}), entries.map(entry => entry.slot('color', color)), - ]), + ])), ], navLinkStyle: 'hierarchical', @@ -246,6 +255,5 @@ export default { ], leftSidebar: relations.sidebar, - }); - }, + })), }; diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index aa025688..44d49c54 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -160,11 +160,11 @@ export default { return data; }, - generate(data, relations, {language}) { - return relations.layout - .slots({ + generate: (data, relations, {language}) => + language.encapsulate('albumGalleryPage', pageCapsule => + relations.layout.slots({ title: - language.$('albumGalleryPage.title', { + language.$(pageCapsule, 'title', { album: data.name, }), @@ -223,6 +223,5 @@ export default { ], secondaryNav: relations.secondaryNav, - }); - }, + })), }; diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index 739a6669..1bffe2d0 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -1,8 +1,3 @@ -import {sortAlbumsTracksChronologically} from '#sort'; -import {empty} from '#sugar'; - -import getChronologyRelations from '../util/getChronologyRelations.js'; - export default { contentDependencies: [ 'generateAlbumAdditionalFilesList', @@ -15,148 +10,93 @@ export default { 'generateAlbumSocialEmbed', 'generateAlbumStyleRules', 'generateAlbumTrackList', - 'generateChronologyLinks', 'generateCommentarySection', 'generateContentHeading', 'generatePageLayout', - 'linkAlbum', 'linkAlbumCommentary', 'linkAlbumGallery', - 'linkArtist', - 'linkTrack', - 'transformContent', ], extraDependencies: ['html', 'language'], - relations(relation, album) { - const relations = {}; - const sections = relations.sections = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); - - relations.socialEmbed = - relation('generateAlbumSocialEmbed', album); - - relations.coverArtistChronologyContributions = - getChronologyRelations(album, { - contributions: album.coverArtistContribs ?? [], - - linkArtist: artist => relation('linkArtist', artist), - - linkThing: trackOrAlbum => - (trackOrAlbum.album - ? relation('linkTrack', trackOrAlbum) - : relation('linkAlbum', trackOrAlbum)), - - getThings(artist) { - const getDate = thing => thing.coverArtDate ?? thing.date; - - const things = [ - ...artist.albumsAsCoverArtist, - ...artist.tracksAsCoverArtist, - ].filter(getDate); - - return sortAlbumsTracksChronologically(things, {getDate}); - }, - }); - - relations.albumNavAccent = - relation('generateAlbumNavAccent', album, null); - - relations.chronologyLinks = - relation('generateChronologyLinks'); - - relations.secondaryNav = - relation('generateAlbumSecondaryNav', album); - - relations.sidebar = - relation('generateAlbumSidebar', album, null); - - if (album.hasCoverArt) { - relations.cover = - relation('generateAlbumCoverArtwork', album); - } - - if (album.hasBannerArt) { - relations.banner = - relation('generateAlbumBanner', album); - } + relations: (relation, album) => ({ + layout: + relation('generatePageLayout'), - // Section: Release info + albumStyleRules: + relation('generateAlbumStyleRules', album, null), - relations.releaseInfo = - relation('generateAlbumReleaseInfo', album); + socialEmbed: + relation('generateAlbumSocialEmbed', album), - // Section: Extra links + albumNavAccent: + relation('generateAlbumNavAccent', album, null), - const extra = sections.extra = {}; + secondaryNav: + relation('generateAlbumSecondaryNav', album), - if (album.tracks.some(t => t.hasUniqueCoverArt)) { - extra.galleryLink = - relation('linkAlbumGallery', album); - } + sidebar: + relation('generateAlbumSidebar', album, null), - if (album.commentary || album.tracks.some(t => t.commentary)) { - extra.commentaryLink = - relation('linkAlbumCommentary', album); - } + cover: + (album.hasCoverArt + ? relation('generateAlbumCoverArtwork', album) + : null), - // Section: Track list + banner: + (album.hasBannerArt + ? relation('generateAlbumBanner', album) + : null), - relations.trackList = - relation('generateAlbumTrackList', album); + contentHeading: + relation('generateContentHeading'), - // Section: Additional files + releaseInfo: + relation('generateAlbumReleaseInfo', album), - if (!empty(album.additionalFiles)) { - const additionalFiles = sections.additionalFiles = {}; + galleryLink: + (album.tracks.some(t => t.hasUniqueCoverArt) + ? relation('linkAlbumGallery', album) + : null), - additionalFiles.heading = - relation('generateContentHeading'); + commentaryLink: + (album.commentary || album.tracks.some(t => t.commentary) + ? relation('linkAlbumCommentary', album) + : null), - additionalFiles.additionalFilesList = - relation('generateAlbumAdditionalFilesList', album, album.additionalFiles); - } + trackList: + relation('generateAlbumTrackList', album), - // Section: Artist commentary + additionalFilesList: + relation('generateAlbumAdditionalFilesList', + album, + album.additionalFiles), - if (album.commentary) { - sections.artistCommentary = - relation('generateCommentarySection', album.commentary); - } + artistCommentarySection: + relation('generateCommentarySection', album.commentary), + }), - return relations; - }, + data: (album) => ({ + name: + album.name, - data(album) { - const data = {}; + color: + album.color, - data.name = album.name; - data.color = album.color; + dateAddedToWiki: + album.dateAddedToWiki, + }), - if (!empty(album.additionalFiles)) { - data.numAdditionalFiles = album.additionalFiles.length; - } - - data.dateAddedToWiki = album.dateAddedToWiki; - - return data; - }, - - generate(data, relations, {html, language}) { - const {sections: sec} = relations; - - return relations.layout - .slots({ - title: language.$('albumPage.title', {album: data.name}), - headingMode: 'sticky', + generate: (data, relations, {html, language}) => + language.encapsulate('albumPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + album: data.name, + }), color: data.color, + headingMode: 'sticky', styleRules: [relations.albumStyleRules], cover: @@ -173,38 +113,44 @@ export default { {[html.onlyIfContent]: true}, {[html.joinChildren]: html.tag('br')}, - [ - sec.additionalFiles && - language.$('releaseInfo.additionalFiles.shortcut', { + language.encapsulate('releaseInfo', capsule => [ + !html.isBlank(relations.additionalFilesList) && + language.$(capsule, 'additionalFiles.shortcut', { link: html.tag('a', {href: '#additional-files'}, - language.$('releaseInfo.additionalFiles.shortcut.link')), - }), - - sec.extra.galleryLink && sec.extra.commentaryLink && - language.$('releaseInfo.viewGalleryOrCommentary', { - gallery: - sec.extra.galleryLink - .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.gallery')), - commentary: - sec.extra.commentaryLink - .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.commentary')), - }), - - sec.extra.galleryLink && !sec.extra.commentaryLink && - language.$('releaseInfo.viewGallery', { - link: - sec.extra.galleryLink - .slot('content', language.$('releaseInfo.viewGallery.link')), + language.$(capsule, 'additionalFiles.shortcut.link')), }), - !sec.extra.galleryLink && sec.extra.commentaryLink && - language.$('releaseInfo.viewCommentary', { - link: - sec.extra.commentaryLink - .slot('content', language.$('releaseInfo.viewCommentary.link')), - }), - ]), + (relations.galleryLink && relations.commentaryLink + ? language.encapsulate(capsule, 'viewGalleryOrCommentary', capsule => + language.$(capsule, { + gallery: + relations.galleryLink + .slot('content', language.$(capsule, 'gallery')), + + commentary: + relations.commentaryLink + .slot('content', language.$(capsule, 'commentary')), + })) + + : relations.galleryLink + ? language.encapsulate(capsule, 'viewGallery', capsule => + language.$(capsule, { + link: + relations.galleryLink + .slot('content', language.$(capsule, 'link')), + })) + + : relations.commentaryLink + ? language.encapsulate(capsule, 'viewCommentary', capsule => + language.$(capsule, { + link: + relations.commentaryLink + .slot('content', language.$(capsule, 'link')), + })) + + : html.blank()), + ])), relations.trackList, @@ -212,28 +158,25 @@ export default { {[html.onlyIfContent]: true}, {[html.joinChildren]: html.tag('br')}, - [ - data.dateAddedToWiki && - language.$('releaseInfo.addedToWiki', { - date: language.formatDate(data.dateAddedToWiki), - }), - ]), - - sec.additionalFiles && [ - sec.additionalFiles.heading - .slots({ - id: 'additional-files', - title: - language.$('releaseInfo.additionalFiles.heading', { - additionalFiles: - language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}), - }), + language.encapsulate('releaseInfo', capsule => [ + language.$(capsule, 'addedToWiki', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.dateAddedToWiki), }), + ])), + + language.encapsulate('releaseInfo.additionalFiles', capsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'additional-files'}, + title: language.$(capsule, 'heading'), + }), - sec.additionalFiles.additionalFilesList, - ], + relations.additionalFilesList, + ])), - sec.artistCommentary, + relations.artistCommentarySection, ], navLinkStyle: 'hierarchical', @@ -249,16 +192,6 @@ export default { }, ], - navContent: - relations.chronologyLinks.slots({ - chronologyInfoSets: [ - { - headingString: 'misc.chronology.heading.coverArt', - contributions: relations.coverArtistChronologyContributions, - }, - ], - }), - banner: relations.banner ?? null, bannerPosition: 'top', @@ -267,6 +200,5 @@ export default { leftSidebar: relations.sidebar, socialEmbed: relations.socialEmbed, - }); - }, + })), }; diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index 121af439..4b6fb062 100644 --- a/src/content/dependencies/generateAlbumNavAccent.js +++ b/src/content/dependencies/generateAlbumNavAccent.js @@ -62,18 +62,21 @@ export default { }, generate(data, relations, slots, {html, language}) { + const albumNavCapsule = language.encapsulate('albumPage.nav'); + const trackNavCapsule = language.encapsulate('trackPage.nav'); + const {content: extraLinks = []} = slots.showExtraLinks && {content: [ (!data.galleryIsStub || slots.currentExtra === 'gallery') && relations.albumGalleryLink?.slots({ attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('albumPage.nav.gallery'), + content: language.$(albumNavCapsule, 'gallery'), }), relations.albumCommentaryLink?.slots({ attributes: {class: slots.currentExtra === 'commentary' && 'current'}, - content: language.$('albumPage.nav.commentary'), + content: language.$(albumNavCapsule, 'commentary'), }), ]}; @@ -94,8 +97,8 @@ export default { {href: '#', 'data-random': 'track-in-sidebar'}, (data.isTrackPage - ? language.$('trackPage.nav.random') - : language.$('albumPage.nav.randomTrack'))); + ? language.$(trackNavCapsule, 'random') + : language.$(albumNavCapsule, 'randomTrack'))); const allLinks = [ ...previousNextLinks, diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js index 6fc1375b..28227f45 100644 --- a/src/content/dependencies/generateAlbumReleaseInfo.js +++ b/src/content/dependencies/generateAlbumReleaseInfo.js @@ -23,11 +23,9 @@ export default { relations.bannerArtistContributionsLine = relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs); - if (!empty(album.urls)) { - relations.externalLinks = - album.urls.map(url => - relation('linkExternal', url)); - } + relations.externalLinks = + album.urls.map(url => + relation('linkExternal', url)); return relations; }, @@ -43,55 +41,77 @@ export default { data.coverArtDate = album.coverArtDate; } - data.duration = accumulateSum(album.tracks, track => track.duration); - data.durationApproximate = album.tracks.length > 1; + const durationTerms = + album.tracks + .map(track => track.duration) + .filter(value => value > 0); + + if (empty(durationTerms)) { + data.duration = null; + data.durationApproximate = null; + } else { + data.duration = accumulateSum(durationTerms); + data.durationApproximate = album.tracks.length > 1; + } data.numTracks = album.tracks.length; return data; }, - generate(data, relations, {html, language}) { - return html.tags([ - html.tag('p', - {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, + generate: (data, relations, {html, language}) => + language.encapsulate('releaseInfo', capsule => + html.tags([ + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, - [ - relations.artistContributionsLine - .slots({stringKey: 'releaseInfo.by'}), + [ + relations.artistContributionsLine.slots({ + stringKey: capsule + '.by', + chronologyKind: 'album', + }), - relations.coverArtistContributionsLine - .slots({stringKey: 'releaseInfo.coverArtBy'}), + relations.coverArtistContributionsLine.slots({ + stringKey: capsule + '.coverArtBy', + chronologyKind: 'coverArt', + }), - relations.wallpaperArtistContributionsLine - .slots({stringKey: 'releaseInfo.wallpaperArtBy'}), + relations.wallpaperArtistContributionsLine.slots({ + stringKey: capsule + '.wallpaperArtBy', + chronologyKind: 'wallpaperArt', + }), - relations.bannerArtistContributionsLine - .slots({stringKey: 'releaseInfo.bannerArtBy'}), + relations.bannerArtistContributionsLine.slots({ + stringKey: capsule + '.bannerArtBy', + chronologyKind: 'bannerArt', + }), - data.date && - language.$('releaseInfo.released', { + language.$(capsule, 'released', { + [language.onlyIfOptions]: ['date'], date: language.formatDate(data.date), }), - data.coverArtDate && - language.$('releaseInfo.artReleased', { + language.$(capsule, 'artReleased', { + [language.onlyIfOptions]: ['date'], date: language.formatDate(data.coverArtDate), }), - data.duration && - language.$('releaseInfo.duration', { + language.$(capsule, 'duration', { + [language.onlyIfOptions]: ['duration'], duration: language.formatDuration(data.duration, { approximate: data.durationApproximate, }), }), - ]), + ]), - relations.externalLinks && html.tag('p', - language.$('releaseInfo.listenOn', { + {[html.onlyIfContent]: true}, + + language.$(capsule, 'listenOn', { + [language.onlyIfOptions]: ['links'], + links: language.formatDisjunctionList( relations.externalLinks @@ -105,6 +125,5 @@ export default { : 'albumMultipleTracks'), ]))), })), - ]); - }, + ])), }; diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js index 00a96c31..f3be74f7 100644 --- a/src/content/dependencies/generateAlbumSidebarGroupBox.js +++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js @@ -1,5 +1,5 @@ import {sortChronologically} from '#sort'; -import {atOffset, empty} from '#sugar'; +import {atOffset} from '#sugar'; export default { contentDependencies: [ @@ -77,40 +77,50 @@ export default { }, generate: (relations, slots, {html, language}) => - relations.box.slots({ - attributes: {class: 'individual-group-sidebar-box'}, - content: [ - html.tag('h1', - language.$('albumSidebar.groupBox.title', { - group: relations.groupLink, - })), - - slots.mode === 'album' && - relations.description - ?.slot('mode', 'multiline'), - - !empty(relations.externalLinks) && + language.encapsulate('albumSidebar.groupBox', boxCapsule => + relations.box.slots({ + attributes: {class: 'individual-group-sidebar-box'}, + content: [ + html.tag('h1', + language.$(boxCapsule, 'title', { + group: relations.groupLink, + })), + + slots.mode === 'album' && + relations.description + ?.slot('mode', 'multiline'), + html.tag('p', + {[html.onlyIfContent]: true}, + language.$('releaseInfo.visitOn', { + [language.onlyIfOptions]: ['links'], + links: language.formatDisjunctionList( relations.externalLinks .map(link => link.slot('context', 'group'))), })), - slots.mode === 'album' && - relations.nextAlbumLink && - html.tag('p', {class: 'group-chronology-link'}, - language.$('albumSidebar.groupBox.next', { - album: relations.nextAlbumLink, - })), + slots.mode === 'album' && + html.tag('p', {class: 'group-chronology-link'}, + {[html.onlyIfContent]: true}, - slots.mode === 'album' && - relations.previousAlbumLink && - html.tag('p', {class: 'group-chronology-link'}, - language.$('albumSidebar.groupBox.previous', { - album: relations.previousAlbumLink, - })), - ], - }), + language.$(boxCapsule, 'next', { + [language.onlyIfOptions]: ['album'], + + album: relations.nextAlbumLink, + })), + + slots.mode === 'album' && + html.tag('p', {class: 'group-chronology-link'}, + {[html.onlyIfContent]: true}, + + language.$(boxCapsule, 'previous', { + [language.onlyIfOptions]: ['album'], + + album: relations.previousAlbumLink, + })), + ], + })), }; diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index aa5c723d..d0c46060 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -55,10 +55,12 @@ export default { }, generate(data, relations, slots, {getColors, html, language}) { + const capsule = language.encapsulate('albumSidebar.trackList'); + const sectionName = html.tag('span', {class: 'group-name'}, (data.isDefaultTrackSection - ? language.$('albumSidebar.trackList.fallbackSectionName') + ? language.$(capsule, 'fallbackSectionName') : data.name)); let colorStyle; @@ -78,7 +80,7 @@ export default { data.tracksAreMissingCommentary[index] && {class: 'no-commentary'}, - language.$('albumSidebar.trackList.item', { + language.$(capsule, 'item', { track: (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index] ? trackLink.slots({ @@ -117,14 +119,17 @@ export default { colorStyle, html.tag('span', - (data.hasTrackNumbers - ? language.$('albumSidebar.trackList.group.withRange', { - group: sectionName, - range: `${data.firstTrackNumber}–${data.lastTrackNumber}` - }) - : language.$('albumSidebar.trackList.group', { - group: sectionName, - })))), + language.encapsulate(capsule, 'group', workingCapsule => { + const workingOptions = {group: sectionName}; + + if (data.hasTrackNumbers) { + workingCapsule += '.withRange'; + workingOptions.range = + `${data.firstTrackNumber}–${data.lastTrackNumber}`; + } + + return language.$(workingCapsule, workingOptions); + }))), (data.hasTrackNumbers ? html.tag('ol', diff --git a/src/content/dependencies/generateAlbumSocialEmbed.js b/src/content/dependencies/generateAlbumSocialEmbed.js index c8b123fe..7500109e 100644 --- a/src/content/dependencies/generateAlbumSocialEmbed.js +++ b/src/content/dependencies/generateAlbumSocialEmbed.js @@ -41,34 +41,34 @@ export default { return data; }, - generate(data, relations, {absoluteTo, language, urls}) { - return relations.socialEmbed.slots({ - title: - language.$('albumPage.socialEmbed.title', { - album: data.albumName, - }), - - description: relations.description, - - headingContent: - (data.hasHeading - ? language.$('albumPage.socialEmbed.heading', { - group: data.headingGroupName, - }) - : null), - - headingLink: - (data.hasHeading - ? absoluteTo('localized.groupGallery', data.headingGroupDirectory) - : null), - - imagePath: - (data.hasImage - ? '/' + - urls - .from('shared.root') - .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension) - : null), - }); - }, + generate: (data, relations, {absoluteTo, language, urls}) => + language.encapsulate('albumPage.socialEmbed', embedCapsule => + relations.socialEmbed.slots({ + title: + language.$(embedCapsule, 'title', { + album: data.albumName, + }), + + description: relations.description, + + headingContent: + (data.hasHeading + ? language.$(embedCapsule, 'heading', { + group: data.headingGroupName, + }) + : null), + + headingLink: + (data.hasHeading + ? absoluteTo('localized.groupGallery', data.headingGroupDirectory) + : null), + + imagePath: + (data.hasImage + ? '/' + + urls + .from('shared.root') + .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension) + : null), + })), }; diff --git a/src/content/dependencies/generateAlbumTrackList.js b/src/content/dependencies/generateAlbumTrackList.js index ee06b9e6..a3435bea 100644 --- a/src/content/dependencies/generateAlbumTrackList.js +++ b/src/content/dependencies/generateAlbumTrackList.js @@ -147,21 +147,30 @@ export default { durationApproximate, startIndex, }) => [ - heading.slots({ - tag: 'dt', - title: - (duration === 0 - ? language.$('trackList.section', { - section: name, - }) - : language.$('trackList.section.withDuration', { - section: name, - duration: + language.encapsulate('trackList.section', capsule => + heading.slots({ + tag: 'dt', + + title: + language.encapsulate(capsule, capsule => { + const options = {section: name}; + + if (duration !== 0) { + capsule += '.withDuration'; + options.duration = language.formatDuration(duration, { approximate: durationApproximate, - }), - })), - }), + }); + } + + return language.$(capsule, options); + }), + + stickyTitle: + language.$(capsule, 'sticky', { + section: name, + }), + })), html.tag('dd', html.tag(listTag, diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js index 7190fb4c..7d5d2c6e 100644 --- a/src/content/dependencies/generateAlbumTrackListItem.js +++ b/src/content/dependencies/generateAlbumTrackListItem.js @@ -80,54 +80,51 @@ export default { }, }, - generate(data, relations, slots, {getColors, html, language}) { - let colorStyle; - if (data.color) { - const {primary} = getColors(data.color); - colorStyle = {style: `--primary-color: ${primary}`}; - } - - const parts = ['trackList.item']; - const options = {}; - - options.track = - relations.trackLink - .slot('color', false); - - const collapseDuration = - (slots.collapseDurationScope === 'track' - ? !data.trackHasDuration - : slots.collapseDurationScope === 'section' - ? !data.sectionHasDuration - : slots.collapseDurationScope === 'album' - ? !data.albumHasDuration - : false); - - if (!collapseDuration) { - parts.push('withDuration'); - - options.duration = - (data.trackHasDuration - ? language.$('trackList.item.withDuration.duration', { - duration: - language.formatDuration(data.duration), - }) - : relations.missingDuration); - } - - if (data.showArtists) { - parts.push('withArtists'); - options.by = - html.tag('span', {class: 'by'}, - html.metatag('chunkwrap', {split: ','}, - html.resolve( - language.$('trackList.item.withArtists.by', { - artists: language.formatConjunctionList(relations.contributionLinks), - })))); - } - - return html.tag('li', - colorStyle, - language.formatString(...parts, options)); - }, + generate: (data, relations, slots, {getColors, html, language}) => + language.encapsulate('trackList.item', itemCapsule => + html.tag('li', + data.color && + {style: `--primary-color: ${getColors(data.color).primary}`}, + + language.encapsulate(itemCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.track = + relations.trackLink + .slot('color', false); + + const collapseDuration = + (slots.collapseDurationScope === 'track' + ? !data.trackHasDuration + : slots.collapseDurationScope === 'section' + ? !data.sectionHasDuration + : slots.collapseDurationScope === 'album' + ? !data.albumHasDuration + : false); + + if (!collapseDuration) { + workingCapsule += '.withDuration'; + workingOptions.duration = + (data.trackHasDuration + ? language.$(itemCapsule, 'withDuration.duration', { + duration: + language.formatDuration(data.duration), + }) + : relations.missingDuration); + } + + if (data.showArtists) { + workingCapsule += '.withArtists'; + workingOptions.by = + html.tag('span', {class: 'by'}, + html.metatag('chunkwrap', {split: ','}, + html.resolve( + language.$(itemCapsule, 'withArtists.by', { + artists: + language.formatConjunctionList(relations.contributionLinks), + })))); + } + + return language.$(workingCapsule, workingOptions); + }))), }; diff --git a/src/content/dependencies/generateAlbumTrackListMissingDuration.js b/src/content/dependencies/generateAlbumTrackListMissingDuration.js index 6d4a6ec8..b5917982 100644 --- a/src/content/dependencies/generateAlbumTrackListMissingDuration.js +++ b/src/content/dependencies/generateAlbumTrackListMissingDuration.js @@ -11,23 +11,25 @@ export default { }), generate: (relations, {html, language}) => - relations.textWithTooltip.slots({ - attributes: {class: 'missing-duration'}, - customInteractionCue: true, + language.encapsulate('trackList.item.withDuration', itemCapsule => + language.encapsulate(itemCapsule, 'duration', durationCapsule => + relations.textWithTooltip.slots({ + attributes: {class: 'missing-duration'}, + customInteractionCue: true, - text: - language.$('trackList.item.withDuration.duration', { - duration: - html.tag('span', {class: 'text-with-tooltip-interaction-cue'}, - language.$('trackList.item.withDuration.duration.missing')), - }), + text: + language.$(durationCapsule, { + duration: + html.tag('span', {class: 'text-with-tooltip-interaction-cue'}, + language.$(durationCapsule, 'missing')), + }), - tooltip: - relations.tooltip.slots({ - attributes: {class: 'missing-duration-tooltip'}, + tooltip: + relations.tooltip.slots({ + attributes: {class: 'missing-duration-tooltip'}, - content: - language.$('trackList.item.withDuration.duration.missing.info'), - }), - }), + content: + language.$(durationCapsule, 'missing.info'), + }), + }))), }; diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js index eae48f05..c51faeba 100644 --- a/src/content/dependencies/generateArtTagGalleryPage.js +++ b/src/content/dependencies/generateArtTagGalleryPage.js @@ -85,11 +85,11 @@ export default { return data; }, - generate(data, relations, {html, language}) { - return relations.layout - .slots({ + generate: (data, relations, {html, language}) => + language.encapsulate('tagPage', pageCapsule => + relations.layout.slots({ title: - language.$('tagPage.title', { + language.$(pageCapsule, 'title', { tag: data.name, }), @@ -100,7 +100,7 @@ export default { mainClasses: ['top-index'], mainContent: [ html.tag('p', {class: 'quick-info'}, - language.$('tagPage.infoLine', { + language.$(pageCapsule, 'infoLine', { coverArts: language.countCoverArts(data.numArtworks, { unit: true, }), @@ -143,11 +143,10 @@ export default { { html: - language.$('tagPage.nav.tag', { + language.$(pageCapsule, 'nav.tag', { tag: relations.artTagMainLink, }), }, ], - }); - }, + })), }; diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js index db8f123f..28f06a21 100644 --- a/src/content/dependencies/generateArtistGalleryPage.js +++ b/src/content/dependencies/generateArtistGalleryPage.js @@ -14,10 +14,12 @@ export default { extraDependencies: ['html', 'language'], query(artist) { - const things = [ - ...artist.albumsAsCoverArtist, - ...artist.tracksAsCoverArtist, - ]; + const things = + ([ + artist.albumCoverArtistContributions, + artist.trackCoverArtistContributions, + ]).flat() + .map(({thing}) => thing); sortAlbumsTracksChronologically(things, { latestFirst: true, @@ -82,11 +84,11 @@ export default { return data; }, - generate(data, relations, {html, language}) { - return relations.layout - .slots({ + generate: (data, relations, {html, language}) => + language.encapsulate('artistGalleryPage', pageCapsule => + relations.layout.slots({ title: - language.$('artistGalleryPage.title', { + language.$(pageCapsule, 'title', { artist: data.name, }), @@ -95,10 +97,11 @@ export default { mainClasses: ['top-index'], mainContent: [ html.tag('p', {class: 'quick-info'}, - language.$('artistGalleryPage.infoLine', { - coverArts: language.countCoverArts(data.numArtworks, { - unit: true, - }), + language.$(pageCapsule, 'infoLine', { + coverArts: + language.countCoverArts(data.numArtworks, { + unit: true, + }), })), relations.coverGrid @@ -117,6 +120,7 @@ export default { dimensions, })), + // TODO: Can this be [language.onlyIfOptions]? info: data.otherCoverArtists.map(names => (names === null @@ -135,6 +139,5 @@ export default { currentExtra: 'gallery', }) .content, - }) - }, + })), } diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js index 1725d4b9..f84d00de 100644 --- a/src/content/dependencies/generateArtistGroupContributionsInfo.js +++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js @@ -131,94 +131,104 @@ export default { countUnit: {validate: v => v.is('tracks', 'artworks')}, }, - generate(data, relations, slots, {html, language}) { - if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) { - return html.blank(); - } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) { - return html.blank(); - } + generate: (data, relations, slots, {html, language}) => + language.encapsulate('artistPage.groupContributions', capsule => { + if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) { + return html.blank(); + } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) { + return html.blank(); + } - const getCounts = counts => - counts.map(count => { - switch (slots.countUnit) { - case 'tracks': return language.countTracks(count, {unit: true}); - case 'artworks': return language.countArtworks(count, {unit: true}); - } - }); - - // We aren't displaying the "~" approximate symbol here for now. - // The general notion that these sums aren't going to be 100% accurate - // is made clear by the "XYZ has contributed ~1:23:45 hours of music..." - // line that's always displayed above this table. - const getDurations = (durations, approximate) => - stitchArrays({ - duration: durations, - approximate: approximate, - }).map(({duration}) => language.formatDuration(duration)); - - const topLevelClasses = [ - 'group-contributions-sorted-by-' + slots.sort, - slots.visible && 'visible', - ]; - - return html.tags([ - html.tag('dt', {class: topLevelClasses}, - (slots.showSortButton - ? language.$('artistPage.groupContributions.title.withSortButton', { - title: slots.title, - sort: - html.tag('a', {class: 'group-contributions-sort-button'}, - {href: '#'}, - - (slots.sort === 'count' - ? language.$('artistPage.groupContributions.title.sorting.count') - : language.$('artistPage.groupContributions.title.sorting.duration'))), - }) - : slots.title)), - - html.tag('dd', {class: topLevelClasses}, - html.tag('ul', {class: 'group-contributions-table'}, - {role: 'list'}, - - (slots.sort === 'count' - ? stitchArrays({ - group: relations.groupLinksSortedByCount, - count: getCounts(data.groupCountsSortedByCount), - duration: - getDurations( - data.groupDurationsSortedByCount, - data.groupDurationsApproximateSortedByCount), - }).map(({group, count, duration}) => - html.tag('li', - html.tag('div', {class: 'group-contributions-row'}, [ - group, - html.tag('span', {class: 'group-contributions-metrics'}, - // When sorting by count, duration details aren't necessarily - // available for all items. - (slots.showBothColumns && duration - ? language.$('artistPage.groupContributions.item.countDurationAccent', {count, duration}) - : language.$('artistPage.groupContributions.item.countAccent', {count}))), - ]))) - - : stitchArrays({ - group: relations.groupLinksSortedByDuration, - count: getCounts(data.groupCountsSortedByDuration), - duration: - getDurations( - data.groupDurationsSortedByDuration, - data.groupDurationsApproximateSortedByDuration), - }).map(({group, count, duration}) => - html.tag('li', - html.tag('div', {class: 'group-contributions-row'}, [ - group, - html.tag('span', {class: 'group-contributions-metrics'}, - // Count details are always available, since they're just the - // number of contributions directly. And duration details are - // guaranteed for every item when sorting by duration. - (slots.showBothColumns - ? language.$('artistPage.groupContributions.item.durationCountAccent', {duration, count}) - : language.$('artistPage.groupContributions.item.durationAccent', {duration}))), - ])))))), - ]); - }, + const getCounts = counts => + counts.map(count => { + switch (slots.countUnit) { + case 'tracks': return language.countTracks(count, {unit: true}); + case 'artworks': return language.countArtworks(count, {unit: true}); + } + }); + + // We aren't displaying the "~" approximate symbol here for now. + // The general notion that these sums aren't going to be 100% accurate + // is made clear by the "XYZ has contributed ~1:23:45 hours of music..." + // line that's always displayed above this table. + const getDurations = (durations, approximate) => + stitchArrays({ + duration: durations, + approximate: approximate, + }).map(({duration}) => language.formatDuration(duration)); + + const topLevelClasses = [ + 'group-contributions-sorted-by-' + slots.sort, + slots.visible && 'visible', + ]; + + // TODO: It feels pretty awkward that this component is the only one that + // has enough knowledge to decide if the sort button is even applicable... + const switchingSortPossible = + !empty(relations.groupLinksSortedByCount) && + !empty(relations.groupLinksSortedByDuration); + + return html.tags([ + html.tag('dt', {class: topLevelClasses}, + language.encapsulate(capsule, 'title', capsule => + (switchingSortPossible && slots.showSortButton + ? language.$(capsule, 'withSortButton', { + title: slots.title, + sort: + html.tag('a', {class: 'group-contributions-sort-button'}, + {href: '#'}, + + (slots.sort === 'count' + ? language.$(capsule, 'sorting.count') + : language.$(capsule, 'sorting.duration'))), + }) + : slots.title))), + + html.tag('dd', {class: topLevelClasses}, + html.tag('ul', {class: 'group-contributions-table'}, + {role: 'list'}, + + (slots.sort === 'count' + ? stitchArrays({ + group: relations.groupLinksSortedByCount, + count: getCounts(data.groupCountsSortedByCount), + duration: + getDurations( + data.groupDurationsSortedByCount, + data.groupDurationsApproximateSortedByCount), + }).map(({group, count, duration}) => + language.encapsulate(capsule, 'item', capsule => + html.tag('li', + html.tag('div', {class: 'group-contributions-row'}, [ + group, + html.tag('span', {class: 'group-contributions-metrics'}, + // When sorting by count, duration details aren't necessarily + // available for all items. + (slots.showBothColumns && duration + ? language.$(capsule, 'countDurationAccent', {count, duration}) + : language.$(capsule, 'countAccent', {count}))), + ])))) + + : stitchArrays({ + group: relations.groupLinksSortedByDuration, + count: getCounts(data.groupCountsSortedByDuration), + duration: + getDurations( + data.groupDurationsSortedByDuration, + data.groupDurationsApproximateSortedByDuration), + }).map(({group, count, duration}) => + language.encapsulate(capsule, 'item', capsule => + html.tag('li', + html.tag('div', {class: 'group-contributions-row'}, [ + group, + html.tag('span', {class: 'group-contributions-metrics'}, + // Count details are always available, since they're just the + // number of contributions directly. And duration details are + // guaranteed for every item when sorting by duration. + (slots.showBothColumns + ? language.$(capsule, 'durationCountAccent', {duration, count}) + : language.$(capsule, 'durationAccent', {duration}))), + ]))))))), + ]); + }), }; diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index ac9209a7..f9ce7e3b 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -1,5 +1,4 @@ import {empty, unique} from '#sugar'; -import {getTotalDuration} from '#wiki-data'; export default { contentDependencies: [ @@ -12,131 +11,112 @@ export default { 'generateContentHeading', 'generateCoverArtwork', 'generatePageLayout', - 'linkAlbum', 'linkArtistGallery', 'linkExternal', - 'linkGroup', - 'linkTrack', 'transformContent', ], - extraDependencies: ['html', 'language', 'wikiData'], - - sprawl({wikiInfo}) { - return { - enableFlashesAndGames: wikiInfo.enableFlashesAndGames, - }; - }, - - query(sprawl, artist) { - return { - // 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. - allTracks: - unique([...artist.tracksAsArtist, ...artist.tracksAsContributor]), - - // Artworks are different, though. We intentionally duplicate album data - // objects when the artist has contributed some combination of cover art, - // wallpaper, and banner - these each count as a unique contribution. - allArtworks: [ - ...artist.albumsAsCoverArtist, - ...artist.albumsAsWallpaperArtist, - ...artist.albumsAsBannerArtist, - ...artist.tracksAsCoverArtist, - ], - - // Banners and wallpapers don't show up in the artist gallery page, only - // cover art. - hasGallery: - !empty(artist.albumsAsCoverArtist) || - !empty(artist.tracksAsCoverArtist), - }; - }, - - relations(relation, query, sprawl, artist) { - const relations = {}; - const sections = relations.sections = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.artistNavLinks = - relation('generateArtistNavLinks', artist); - - if (artist.hasAvatar) { - relations.cover = - relation('generateCoverArtwork', []); - } - - if (artist.contextNotes) { - const contextNotes = sections.contextNotes = {}; - contextNotes.content = relation('transformContent', artist.contextNotes); - } - - if (!empty(artist.urls)) { - const visit = sections.visit = {}; - visit.externalLinks = - artist.urls.map(url => - relation('linkExternal', url)); - } - - if (!empty(query.allTracks)) { - const tracks = sections.tracks = {}; - tracks.heading = relation('generateContentHeading'); - tracks.list = relation('generateArtistInfoPageTracksChunkedList', artist); - tracks.groupInfo = relation('generateArtistGroupContributionsInfo', query.allTracks); - } - - if (!empty(query.allArtworks)) { - const artworks = sections.artworks = {}; - artworks.heading = relation('generateContentHeading'); - artworks.list = relation('generateArtistInfoPageArtworksChunkedList', artist); - artworks.groupInfo = - relation('generateArtistGroupContributionsInfo', query.allArtworks); - - if (query.hasGallery) { - artworks.artistGalleryLink = - relation('linkArtistGallery', artist); - } - } - - if (sprawl.enableFlashesAndGames && !empty(artist.flashesAsContributor)) { - const flashes = sections.flashes = {}; - flashes.heading = relation('generateContentHeading'); - flashes.list = relation('generateArtistInfoPageFlashesChunkedList', artist); - } - - if (!empty(artist.albumsAsCommentator) || !empty(artist.tracksAsCommentator)) { - const commentary = sections.commentary = {}; - commentary.heading = relation('generateContentHeading'); - commentary.list = relation('generateArtistInfoPageCommentaryChunkedList', artist); - } - - return relations; - }, - - data(query, sprawl, artist) { - const data = {}; - - data.name = artist.name; - data.directory = artist.directory; - - if (artist.hasAvatar) { - data.avatarFileExtension = artist.avatarFileExtension; - } - - data.totalTrackCount = query.allTracks.length; - data.totalDuration = getTotalDuration(query.allTracks, {originalReleasesOnly: true}); - - return data; - }, - - generate(data, relations, {html, language}) { - const {sections: sec} = relations; - - return relations.layout - .slots({ + extraDependencies: ['html', 'language'], + + query: (artist) => ({ + // Even if an artist has served as both "artist" (compositional) and + // "contributor" (instruments, production, etc) on the same track, that + // track only counts as one unique contribution in the list. + allTracks: + unique( + ([ + artist.trackArtistContributions, + artist.trackContributorContributions, + ]).flat() + .map(({thing}) => thing)), + + // Artworks are different, though. We intentionally duplicate album data + // objects when the artist has contributed some combination of cover art, + // wallpaper, and banner - these each count as a unique contribution. + allArtworks: + ([ + artist.albumCoverArtistContributions, + artist.albumWallpaperArtistContributions, + artist.albumBannerArtistContributions, + artist.trackCoverArtistContributions, + ]).flat() + .map(({thing}) => thing), + + // Banners and wallpapers don't show up in the artist gallery page, only + // cover art. + hasGallery: + !empty(artist.albumCoverArtistContributions) || + !empty(artist.trackCoverArtistContributions), + }), + + relations: (relation, query, artist) => ({ + layout: + relation('generatePageLayout'), + + artistNavLinks: + relation('generateArtistNavLinks', artist), + + cover: + (artist.hasAvatar + ? relation('generateCoverArtwork', []) + : null), + + contentHeading: + relation('generateContentHeading'), + + contextNotes: + relation('transformContent', artist.contextNotes), + + visitLinks: + artist.urls + .map(url => relation('linkExternal', url)), + + tracksChunkedList: + relation('generateArtistInfoPageTracksChunkedList', artist), + + tracksGroupInfo: + relation('generateArtistGroupContributionsInfo', query.allTracks), + + artworksChunkedList: + relation('generateArtistInfoPageArtworksChunkedList', artist), + + artworksGroupInfo: + relation('generateArtistGroupContributionsInfo', query.allArtworks), + + artistGalleryLink: + (query.hasGallery + ? relation('linkArtistGallery', artist) + : null), + + flashesChunkedList: + relation('generateArtistInfoPageFlashesChunkedList', artist), + + commentaryChunkedList: + relation('generateArtistInfoPageCommentaryChunkedList', artist), + }), + + data: (query, artist) => ({ + name: + artist.name, + + directory: + artist.directory, + + avatarFileExtension: + (artist.hasAvatar + ? artist.avatarFileExtension + : null), + + totalTrackCount: + query.allTracks.length, + + totalDuration: + artist.totalDuration, + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('artistPage', pageCapsule => + relations.layout.slots({ title: data.name, headingMode: 'sticky', @@ -152,67 +132,85 @@ export default { : null), mainContent: [ - sec.contextNotes && [ - html.tag('p', language.$('releaseInfo.note')), + html.tags([ + html.tag('p', + {[html.onlyIfSiblings]: true}, + language.$('releaseInfo.note')), + html.tag('blockquote', - sec.contextNotes.content), - ], + {[html.onlyIfContent]: true}, + relations.contextNotes), + ]), - sec.visit && - html.tag('p', - language.$('releaseInfo.visitOn', { - links: - language.formatDisjunctionList( - sec.visit.externalLinks - .map(link => link.slot('context', 'artist'))), - })), - - sec.artworks?.artistGalleryLink && - html.tag('p', - language.$('artistPage.viewArtGallery', { - link: sec.artworks.artistGalleryLink.slots({ - content: language.$('artistPage.viewArtGallery.link'), - }), - })), + html.tag('p', + {[html.onlyIfContent]: true}, - (sec.tracks || sec.artworsk || sec.flashes || sec.commentary) && - html.tag('p', - language.$('misc.jumpTo.withLinks', { - links: language.formatUnitList( - [ - sec.tracks && - html.tag('a', - {href: '#tracks'}, - language.$('artistPage.trackList.title')), - - sec.artworks && - html.tag('a', - {href: '#art'}, - language.$('artistPage.artList.title')), - - sec.flashes && - html.tag('a', - {href: '#flashes'}, - language.$('artistPage.flashList.title')), - - sec.commentary && - html.tag('a', - {href: '#commentary'}, - language.$('artistPage.commentaryList.title')), - ].filter(Boolean)), - })), - - sec.tracks && [ - sec.tracks.heading + language.$('releaseInfo.visitOn', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatDisjunctionList( + relations.visitLinks + .map(link => link.slot('context', 'artist'))), + })), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.encapsulate(pageCapsule, 'viewArtGallery', capsule => + language.$(capsule, { + [language.onlyIfOptions]: ['link'], + + link: + relations.artistGalleryLink?.slots({ + content: + language.$(capsule, 'link'), + }), + }))), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$('misc.jumpTo.withLinks', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatUnitList([ + !html.isBlank(relations.tracksChunkedList) && + html.tag('a', + {href: '#tracks'}, + language.$(pageCapsule, 'trackList.title')), + + !html.isBlank(relations.artworksChunkedList) && + html.tag('a', + {href: '#art'}, + language.$(pageCapsule, 'artList.title')), + + !html.isBlank(relations.flashesChunkedList) && + html.tag('a', + {href: '#flashes'}, + language.$(pageCapsule, 'flashList.title')), + + !html.isBlank(relations.commentaryChunkedList) && + html.tag('a', + {href: '#commentary'}, + language.$(pageCapsule, 'commentaryList.title')), + ].filter(Boolean)), + })), + + html.tags([ + relations.contentHeading.clone() .slots({ tag: 'h2', - id: 'tracks', - title: language.$('artistPage.trackList.title'), + attributes: {id: 'tracks'}, + title: language.$(pageCapsule, 'trackList.title'), }), data.totalDuration > 0 && html.tag('p', - language.$('artistPage.contributedDurationLine', { + {[html.onlyIfSiblings]: true}, + + language.$(pageCapsule, 'contributedDurationLine', { artist: data.name, duration: language.formatDuration(data.totalDuration, { @@ -221,82 +219,86 @@ export default { }), })), - sec.tracks.list - .slots({ - groupInfo: [ - sec.tracks.groupInfo - .clone() + relations.tracksChunkedList.slots({ + groupInfo: + language.encapsulate(pageCapsule, 'groupContributions', capsule => [ + relations.tracksGroupInfo.clone() .slots({ - title: language.$('artistPage.groupContributions.title.music'), + title: language.$(capsule, 'title.music'), showSortButton: true, sort: 'count', countUnit: 'tracks', visible: true, }), - sec.tracks.groupInfo - .clone() + relations.tracksGroupInfo.clone() .slots({ - title: language.$('artistPage.groupContributions.title.music'), + title: language.$(capsule, 'title.music'), showSortButton: true, sort: 'duration', countUnit: 'tracks', visible: false, }), - ], - }), - ], + ]), + }), + ]), - sec.artworks && [ - sec.artworks.heading + html.tags([ + relations.contentHeading.clone() .slots({ tag: 'h2', - id: 'art', - title: language.$('artistPage.artList.title'), + attributes: {id: 'art'}, + title: language.$(pageCapsule, 'artList.title'), }), - sec.artworks.artistGalleryLink && - html.tag('p', - language.$('artistPage.viewArtGallery.orBrowseList', { - link: sec.artworks.artistGalleryLink.slots({ - content: language.$('artistPage.viewArtGallery.link'), - }), - })), + html.tag('p', + {[html.onlyIfContent]: true}, - sec.artworks.list + language.encapsulate(pageCapsule, 'viewArtGallery', capsule => + language.$(capsule, 'orBrowseList', { + [language.onlyIfOptions]: ['link'], + + link: + relations.artistGalleryLink?.slots({ + content: language.$(capsule, 'link'), + }), + }))), + + relations.artworksChunkedList .slots({ groupInfo: - sec.artworks.groupInfo - .slots({ - title: language.$('artistPage.groupContributions.title.artworks'), - showBothColumns: false, - sort: 'count', - countUnit: 'artworks', - }), + language.encapsulate(pageCapsule, 'groupContributions', capsule => + relations.artworksGroupInfo + .slots({ + title: language.$(capsule, 'title.artworks'), + showBothColumns: false, + sort: 'count', + countUnit: 'artworks', + })), }), - ], + ]), - sec.flashes && [ - sec.flashes.heading + html.tags([ + relations.contentHeading.clone() .slots({ tag: 'h2', - id: 'flashes', - title: language.$('artistPage.flashList.title'), + attributes: {id: 'flashes'}, + title: language.$(pageCapsule, 'flashList.title'), }), - sec.flashes.list, - ], + relations.flashesChunkedList, + ]), - sec.commentary && [ - sec.commentary.heading + html.tags([ + relations.contentHeading.clone() .slots({ tag: 'h2', - id: 'commentary', - title: language.$('artistPage.commentaryList.title'), + attributes: {id: 'commentary'}, + title: language.$(pageCapsule, 'commentaryList.title'), }), - sec.commentary.list, - ], + relations.commentaryChunkedList, + ]), ], navLinkStyle: 'hierarchical', @@ -306,6 +308,5 @@ export default { showExtraLinks: true, }) .content, - }); - }, + })), }; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js new file mode 100644 index 00000000..2b10df3e --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js @@ -0,0 +1,34 @@ +export default { + contentDependencies: [ + 'generateArtistInfoPageChunk', + 'generateArtistInfoPageArtworksChunkItem', + 'linkAlbum', + ], + + relations: (relation, album, contribs) => ({ + template: + relation('generateArtistInfoPageChunk'), + + albumLink: + relation('linkAlbum', album), + + items: + contribs + .map(contrib => + relation('generateArtistInfoPageArtworksChunkItem', contrib)), + }), + + data: (_album, contribs) => ({ + dates: + contribs + .map(contrib => contrib.date), + }), + + generate: (data, relations) => + relations.template.slots({ + mode: 'album', + albumLink: relations.albumLink, + dates: data.dates, + items: relations.items, + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js new file mode 100644 index 00000000..e8d887b1 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js @@ -0,0 +1,62 @@ +export default { + contentDependencies: [ + 'generateArtistInfoPageChunkItem', + 'generateArtistInfoPageOtherArtistLinks', + 'linkTrack', + ], + + extraDependencies: ['html', 'language'], + + query: (contrib) => ({ + kind: + (contrib.isBannerArtistContribution + ? 'banner' + : contrib.isWallpaperArtistContribution + ? 'wallpaper' + : contrib.isForAlbum + ? 'album-cover' + : 'track-cover'), + }), + + relations: (relation, query, contrib) => ({ + template: + relation('generateArtistInfoPageChunkItem'), + + trackLink: + (query.kind === 'track-cover' + ? relation('linkTrack', contrib.thing) + : null), + + otherArtistLinks: + relation('generateArtistInfoPageOtherArtistLinks', [contrib]), + }), + + data: (query, contrib) => ({ + kind: + query.kind, + + annotation: + contrib.annotation, + }), + + generate: (data, relations, {html, language}) => + relations.template.slots({ + otherArtistLinks: relations.otherArtistLinks, + + annotation: data.annotation, + + content: + language.encapsulate('artistPage.creditList.entry', capsule => + (data.kind === 'track-cover' + ? language.$(capsule, 'track', { + track: relations.trackLink, + }) + : html.tag('i', + language.encapsulate(capsule, 'album', capsule => + (data.kind === 'wallpaper' + ? language.$(capsule, 'wallpaperArt') + : data.kind === 'banner' + ? language.$(capsule, 'bannerArt') + : language.$(capsule, 'coverArt')))))), + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index 44fb42f2..caefb7a3 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -1,241 +1,58 @@ -import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort'; -import {chunkByProperties, stitchArrays} from '#sugar'; +import {sortAlbumsTracksChronologically, sortContributionsChronologically} + from '#sort'; +import {chunkByConditions, stitchArrays} from '#sugar'; export default { contentDependencies: [ - 'generateArtistInfoPageChunk', 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageOtherArtistLinks', - 'linkAlbum', - 'linkTrack', + 'generateArtistInfoPageArtworksChunk', ], - extraDependencies: ['html', 'language'], - query(artist) { - // TODO: Add and integrate wallpaper and banner date fields (#90) - // This will probably only happen once all artworks follow a standard - // shape (#70) and get their own sorting function. Read for more info: - // https://github.com/hsmusic/hsmusic-wiki/issues/90#issuecomment-1607422961 - - const processEntry = ({thing, type, track, album, contribs}) => ({ - thing: thing, - entry: { - type: type, - track: track, - album: album, - contribs: contribs, - date: thing.coverArtDate ?? thing.date, - }, - }); - - const processAlbumEntry = ({type, album, contribs}) => - processEntry({ - thing: album, - type: type, - track: null, - album: album, - contribs: contribs, - }); - - const processTrackEntry = ({type, track, contribs}) => - processEntry({ - thing: track, - type: type, - track: track, - album: track.album, - contribs: contribs, - }); - - const processAlbumEntries = ({type, albums, contribs}) => - stitchArrays({ - album: albums, - contribs: contribs, - }).map(entry => - processAlbumEntry({type, ...entry})); - - const processTrackEntries = ({type, tracks, contribs}) => - stitchArrays({ - track: tracks, - contribs: contribs, - }).map(entry => - processTrackEntry({type, ...entry})); - - const { - albumsAsCoverArtist, - albumsAsWallpaperArtist, - albumsAsBannerArtist, - tracksAsCoverArtist, - } = artist; + const query = {}; - const albumsAsCoverArtistContribs = - albumsAsCoverArtist - .map(album => album.coverArtistContribs); - - const albumsAsWallpaperArtistContribs = - albumsAsWallpaperArtist - .map(album => album.wallpaperArtistContribs); - - const albumsAsBannerArtistContribs = - albumsAsBannerArtist - .map(album => album.bannerArtistContribs); - - const tracksAsCoverArtistContribs = - tracksAsCoverArtist - .map(track => track.coverArtistContribs); - - const albumsAsCoverArtistEntries = - processAlbumEntries({ - type: 'albumCover', - albums: albumsAsCoverArtist, - contribs: albumsAsCoverArtistContribs, - }); - - const albumsAsWallpaperArtistEntries = - processAlbumEntries({ - type: 'albumWallpaper', - albums: albumsAsWallpaperArtist, - contribs: albumsAsWallpaperArtistContribs, - }); - - const albumsAsBannerArtistEntries = - processAlbumEntries({ - type: 'albumBanner', - albums: albumsAsBannerArtist, - contribs: albumsAsBannerArtistContribs, - }); - - const tracksAsCoverArtistEntries = - processTrackEntries({ - type: 'trackCover', - tracks: tracksAsCoverArtist, - contribs: tracksAsCoverArtistContribs, - }); - - const entries = [ - ...albumsAsCoverArtistEntries, - ...albumsAsWallpaperArtistEntries, - ...albumsAsBannerArtistEntries, - ...tracksAsCoverArtistEntries, + const allContributions = [ + ...artist.albumCoverArtistContributions, + ...artist.albumWallpaperArtistContributions, + ...artist.albumBannerArtistContributions, + ...artist.trackCoverArtistContributions, ]; - sortEntryThingPairs(entries, - things => sortAlbumsTracksChronologically(things, { - getDate: thing => thing.coverArtDate ?? thing.date, - })); - - const chunks = - chunkByProperties( - entries.map(({entry}) => entry), - ['album', 'date']); - - return {chunks}; + sortContributionsChronologically( + allContributions, + sortAlbumsTracksChronologically); + + query.contribs = + chunkByConditions(allContributions, [ + ({date: date1}, {date: date2}) => + +date1 !== +date2, + ({thing: thing1}, {thing: thing2}) => + (thing1.album ?? thing1) !== + (thing2.album ?? thing2), + ]); + + query.albums = + query.contribs + .map(contribs => contribs[0].thing) + .map(thing => thing.album ?? thing); + + return query; }, - relations(relation, query, artist) { - return { - chunkedList: - relation('generateArtistInfoPageChunkedList'), - - chunks: - query.chunks.map(() => relation('generateArtistInfoPageChunk')), - - albumLinks: - query.chunks.map(({album}) => relation('linkAlbum', album)), - - items: - query.chunks.map(({chunk}) => - chunk.map(() => relation('generateArtistInfoPageChunkItem'))), - - itemTrackLinks: - query.chunks.map(({chunk}) => - chunk.map(({track}) => track ? relation('linkTrack', track) : null)), - - itemOtherArtistLinks: - query.chunks.map(({chunk}) => - chunk.map(({contribs}) => relation('generateArtistInfoPageOtherArtistLinks', contribs, artist))), - }; - }, - - data(query, artist) { - return { - chunkDates: - query.chunks.map(({date}) => date), - - itemTypes: - query.chunks.map(({chunk}) => - chunk.map(({type}) => type)), + relations: (relation, query, _artist) => ({ + chunkedList: + relation('generateArtistInfoPageChunkedList'), - itemContributions: - query.chunks.map(({chunk}) => - chunk.map(({contribs}) => - contribs - .find(contrib => contrib.artist === artist) - .annotation)), - }; - }, - - generate(data, relations, {html, language}) { - return relations.chunkedList.slots({ - chunks: - stitchArrays({ - chunk: relations.chunks, - albumLink: relations.albumLinks, - date: data.chunkDates, - - items: relations.items, - itemTrackLinks: relations.itemTrackLinks, - itemOtherArtistLinks: relations.itemOtherArtistLinks, - itemTypes: data.itemTypes, - itemContributions: data.itemContributions, - }).map(({ - chunk, - albumLink, - date, - - items, - itemTrackLinks, - itemOtherArtistLinks, - itemTypes, - itemContributions, - }) => - chunk.slots({ - mode: 'album', - albumLink, - date, - - items: - stitchArrays({ - item: items, - trackLink: itemTrackLinks, - otherArtistLinks: itemOtherArtistLinks, - type: itemTypes, - contribution: itemContributions, - }).map(({ - item, - trackLink, - otherArtistLinks, - type, - contribution, - }) => - item.slots({ - otherArtistLinks, - annotation: contribution, - - content: - (type === 'trackCover' - ? language.$('artistPage.creditList.entry.track', { - track: trackLink, - }) - : html.tag('i', - language.$('artistPage.creditList.entry.album.' + { - albumWallpaper: 'wallpaperArt', - albumBanner: 'bannerArt', - albumCover: 'coverArt', - }[type]))), - })), - })), - }); - }, + chunks: + stitchArrays({ + album: query.albums, + contribs: query.contribs, + }).map(({album, contribs}) => + relation('generateArtistInfoPageArtworksChunk', album, contribs)), + }), + + generate: (relations) => + relations.chunkedList.slots({ + chunks: relations.chunks, + }), }; diff --git a/src/content/dependencies/generateArtistInfoPageChunk.js b/src/content/dependencies/generateArtistInfoPageChunk.js index 40943914..c16d50f3 100644 --- a/src/content/dependencies/generateArtistInfoPageChunk.js +++ b/src/content/dependencies/generateArtistInfoPageChunk.js @@ -1,3 +1,5 @@ +import {empty} from '#sugar'; + export default { extraDependencies: ['html', 'language'], @@ -21,15 +23,33 @@ export default { mutable: false, }, - date: {validate: v => v.isDate}, - dateRangeStart: {validate: v => v.isDate}, - dateRangeEnd: {validate: v => v.isDate}, + dates: { + validate: v => v.sparseArrayOf(v.isDate), + }, duration: {validate: v => v.isDuration}, durationApproximate: {type: 'boolean'}, }, generate(slots, {html, language}) { + let earliestDate = null; + let latestDate = null; + let onlyDate = null; + + if (!empty(slots.dates)) { + earliestDate = + slots.dates + .reduce((a, b) => a <= b ? a : b); + + latestDate = + slots.dates + .reduce((a, b) => a <= b ? b : a); + + if (+earliestDate === +latestDate) { + onlyDate = earliestDate; + } + } + let accentedLink; accent: { @@ -40,9 +60,9 @@ export default { const options = {album: accentedLink}; const parts = ['artistPage.creditList.album']; - if (slots.date) { + if (onlyDate) { parts.push('withDate'); - options.date = language.formatDate(slots.date); + options.date = language.formatDate(onlyDate); } if (slots.duration) { @@ -63,16 +83,13 @@ export default { const options = {act: accentedLink}; const parts = ['artistPage.creditList.flashAct']; - if ( - slots.dateRangeStart && - slots.dateRangeEnd && - slots.dateRangeStart !== slots.dateRangeEnd - ) { - parts.push('withDateRange'); - options.dateRange = language.formatDateRange(slots.dateRangeStart, slots.dateRangeEnd); - } else if (slots.dateRangeStart || slots.date) { + if (onlyDate) { parts.push('withDate'); - options.date = language.formatDate(slots.dateRangeStart ?? slots.date); + options.date = language.formatDate(onlyDate); + } else if (earliestDate && latestDate) { + parts.push('withDateRange'); + options.dateRange = + language.formatDateRange(earliestDate, latestDate); } accentedLink = language.formatString(...parts, options); diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js index b6f40727..9d406c67 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js @@ -1,3 +1,5 @@ +import {empty} from '#sugar'; + export default { extraDependencies: ['html', 'language'], @@ -19,42 +21,38 @@ export default { rerelease: {type: 'boolean'}, }, - generate(slots, {html, language}) { - let accentedContent = slots.content; - - accent: { - if (slots.rerelease) { - accentedContent = - language.$('artistPage.creditList.entry.rerelease', { - entry: accentedContent, - }); - - break accent; - } - - const parts = ['artistPage.creditList.entry']; - const options = {entry: accentedContent}; - - if (slots.otherArtistLinks) { - parts.push('withArtists'); - options.artists = language.formatConjunctionList(slots.otherArtistLinks); - } - - if (!html.isBlank(slots.annotation)) { - parts.push('withAnnotation'); - options.annotation = slots.annotation; - } - - if (parts.length === 1) { - break accent; - } - - accentedContent = language.formatString(...parts, options); - } - - return ( + generate: (slots, {html, language}) => + language.encapsulate('artistPage.creditList.entry', entryCapsule => html.tag('li', slots.rerelease && {class: 'rerelease'}, - accentedContent)); - }, + + language.encapsulate(entryCapsule, workingCapsule => { + const workingOptions = {entry: slots.content}; + + if (slots.rerelease) { + workingCapsule += '.rerelease'; + 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; + } + }))), }; diff --git a/src/content/dependencies/generateArtistInfoPageChunkedList.js b/src/content/dependencies/generateArtistInfoPageChunkedList.js index 8503d014..e7915ab7 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageChunkedList.js @@ -13,11 +13,8 @@ export default { }, }, - generate(slots, {html}) { - return ( - html.tag('dl', [ - slots.groupInfo, - slots.chunks, - ])); - }, + generate: (slots, {html}) => + html.tag('dl', + {[html.onlyIfContent]: true}, + [slots.groupInfo, slots.chunks]), }; diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js index 133095ea..72bbf1b6 100644 --- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js @@ -10,7 +10,6 @@ export default { contentDependencies: [ 'generateArtistInfoPageChunk', 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageOtherArtistLinks', 'linkAlbum', 'linkFlash', 'linkFlashAct', @@ -217,53 +216,52 @@ export default { itemAnnotations, itemTypes, }) => - (chunkType === 'album' - ? chunk.slots({ - mode: 'album', - albumLink: chunkLink, - items: - stitchArrays({ - item: items, - link: itemLinks, - annotation: itemAnnotations, - type: itemTypes, - }).map(({item, link, annotation, type}) => - item.slots({ - annotation: - (annotation - ? annotation.slot('mode', 'inline') - : null), - - content: - (type === 'album' - ? html.tag('i', - language.$('artistPage.creditList.entry.album.commentary')) - : language.$('artistPage.creditList.entry.track', { - track: link, - })), - })), - }) - : chunkType === 'flash-act' - ? chunk.slots({ - mode: 'flash', - flashActLink: chunkLink, - items: - stitchArrays({ - item: items, - link: itemLinks, - annotation: itemAnnotations, - }).map(({item, link, annotation}) => - item.slots({ - annotation: - (annotation - ? annotation.slot('mode', 'inline') - : null), - - content: - language.$('artistPage.creditList.entry.flash', { - flash: link, - }), - })), - }) - : null))), + language.encapsulate('artistPage.creditList.entry', capsule => + (chunkType === 'album' + ? chunk.slots({ + mode: 'album', + albumLink: chunkLink, + items: + stitchArrays({ + item: items, + link: itemLinks, + annotation: itemAnnotations, + type: itemTypes, + }).map(({item, link, annotation, type}) => + item.slots({ + annotation: + (annotation + ? annotation.slot('mode', 'inline') + : null), + + content: + (type === 'album' + ? html.tag('i', + language.$(capsule, 'album.commentary')) + : language.$(capsule, 'track', {track: link})), + })), + }) + : chunkType === 'flash-act' + ? chunk.slots({ + mode: 'flash', + flashActLink: chunkLink, + items: + stitchArrays({ + item: items, + link: itemLinks, + annotation: itemAnnotations, + }).map(({item, link, annotation}) => + item.slots({ + annotation: + (annotation + ? annotation.slot('mode', 'inline') + : null), + + content: + language.$(capsule, 'flash', { + flash: link, + }), + })), + }) + : null)))), }; diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunk.js b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js new file mode 100644 index 00000000..8aa7223a --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js @@ -0,0 +1,34 @@ +export default { + contentDependencies: [ + 'generateArtistInfoPageChunk', + 'generateArtistInfoPageFlashesChunkItem', + 'linkFlashAct', + ], + + relations: (relation, flashAct, contribs) => ({ + template: + relation('generateArtistInfoPageChunk'), + + flashActLink: + relation('linkFlashAct', flashAct), + + items: + contribs + .map(contrib => + relation('generateArtistInfoPageFlashesChunkItem', contrib)), + }), + + data: (_flashAct, contribs) => ({ + dates: + contribs + .map(contrib => contrib.date), + }), + + generate: (data, relations) => + relations.template.slots({ + mode: 'flash', + flashActLink: relations.flashActLink, + dates: data.dates, + items: relations.items, + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js new file mode 100644 index 00000000..e4908bf9 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js @@ -0,0 +1,34 @@ +export default { + contentDependencies: ['generateArtistInfoPageChunkItem', 'linkFlash'], + + extraDependencies: ['language'], + + relations: (relation, contrib) => ({ + // Flashes and games can list multiple contributors as collaborative + // credits, but we don't display these on the artist page, since they + // usually involve many artists crediting a larger team where collaboration + // isn't as relevant (without more particular details that aren't tracked + // on the wiki). + + template: + relation('generateArtistInfoPageChunkItem'), + + flashLink: + relation('linkFlash', contrib.thing), + }), + + data: (contrib) => ({ + annotation: + contrib.annotation, + }), + + generate: (data, relations, {language}) => + relations.template.slots({ + annotation: data.annotation, + + content: + language.$('artistPage.creditList.entry.flash', { + flash: relations.flashLink, + }), + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js index 447e697e..b347faf5 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js @@ -1,149 +1,62 @@ -import {sortEntryThingPairs, sortFlashesChronologically} from '#sort'; -import {chunkByProperties, stitchArrays} from '#sugar'; +import {sortContributionsChronologically, sortFlashesChronologically} + from '#sort'; +import {chunkByConditions, stitchArrays} from '#sugar'; export default { contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageChunkItem', - 'linkFlash', + 'generateArtistInfoPageChunkedList', + 'generateArtistInfoPageFlashesChunk', ], - extraDependencies: ['html', 'language'], + extraDependencies: ['wikiData'], - query(artist) { - const processFlashEntry = ({flash, contribs}) => ({ - thing: flash, - entry: { - flash: flash, - act: flash.act, - contribs: contribs, - }, - }); + sprawl: ({wikiInfo}) => ({ + enableFlashesAndGames: + wikiInfo.enableFlashesAndGames, + }), - const processFlashEntries = ({flashes, contribs}) => - stitchArrays({ - flash: flashes, - contribs: contribs, - }).map(processFlashEntry); - - const {flashesAsContributor} = artist; - - const flashesAsContributorContribs = - flashesAsContributor - .map(flash => flash.contributorContribs); - - const flashesAsContributorEntries = - processFlashEntries({ - flashes: flashesAsContributor, - contribs: flashesAsContributorContribs, - }); - - const entries = [ - ...flashesAsContributorEntries, - ]; - - sortEntryThingPairs(entries, sortFlashesChronologically); - - const chunks = - chunkByProperties( - entries.map(({entry}) => entry), - ['act']); - - return {chunks}; - }, + query(sprawl, artist) { + const query = {}; - relations(relation, query) { - // Flashes and games can list multiple contributors as collaborative - // credits, but we don't display these on the artist page, since they - // usually involve many artists crediting a larger team where collaboration - // isn't as relevant (without more particular details that aren't tracked - // on the wiki). + const allContributions = + (sprawl.enableFlashesAndGames + ? [ + ...artist.flashContributorContributions, + ] + : []); - return { - chunks: - query.chunks.map(() => relation('generateArtistInfoPageChunk')), + sortContributionsChronologically( + allContributions, + sortFlashesChronologically); - actLinks: - query.chunks.map(({chunk}) => - relation('linkFlash', chunk[0].flash)), + query.contribs = + chunkByConditions(allContributions, [ + ({thing: flash1}, {thing: flash2}) => + flash1.act !== flash2.act, + ]); - items: - query.chunks.map(({chunk}) => - chunk.map(() => relation('generateArtistInfoPageChunkItem'))), + query.flashActs = + query.contribs + .map(contribs => contribs[0].thing) + .map(thing => thing.act); - itemFlashLinks: - query.chunks.map(({chunk}) => - chunk.map(({flash}) => relation('linkFlash', flash))), - }; + return query; }, - data(query, artist) { - return { - actNames: - query.chunks.map(({act}) => act.name), + relations: (relation, query, _sprawl, _artist) => ({ + chunkedList: + relation('generateArtistInfoPageChunkedList'), - firstDates: - query.chunks.map(({chunk}) => chunk[0].flash.date ?? null), - - lastDates: - query.chunks.map(({chunk}) => chunk.at(-1).flash.date ?? null), - - itemContributions: - query.chunks.map(({chunk}) => - chunk.map(({contribs}) => - contribs - .find(contrib => contrib.artist === artist) - .annotation)), - }; - }, - - generate(data, relations, {html, language}) { - return html.tag('dl', + chunks: stitchArrays({ - chunk: relations.chunks, - actLink: relations.actLinks, - actName: data.actNames, - firstDate: data.firstDates, - lastDate: data.lastDates, - - items: relations.items, - itemFlashLinks: relations.itemFlashLinks, - itemContributions: data.itemContributions, - }).map(({ - chunk, - actLink, - actName, - firstDate, - lastDate, - - items, - itemFlashLinks, - itemContributions, - }) => - chunk.slots({ - mode: 'flash', - flashActLink: actLink.slot('content', actName), - dateRangeStart: firstDate, - dateRangeEnd: lastDate, - - items: - stitchArrays({ - item: items, - flashLink: itemFlashLinks, - contribution: itemContributions, - }).map(({ - item, - flashLink, - contribution, - }) => - item.slots({ - annotation: contribution, - - content: - language.$('artistPage.creditList.entry.flash', { - flash: flashLink, - }), - })), - }))); - }, + flashAct: query.flashActs, + contribs: query.contribs, + }).map(({flashAct, contribs}) => + relation('generateArtistInfoPageFlashesChunk', flashAct, contribs)), + }), + + generate: (relations) => + relations.chunkedList.slots({ + chunks: relations.chunks, + }), }; diff --git a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js index 471ee26c..dcee9c00 100644 --- a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js +++ b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js @@ -1,24 +1,30 @@ -import {empty} from '#sugar'; +import {unique} from '#sugar'; export default { contentDependencies: ['linkArtist'], - relations(relation, contribs, artist) { - const otherArtistContribs = - contribs.filter(contrib => contrib.artist !== artist); + query(contribs) { + const associatedContributionsByOtherArtists = + contribs + .flatMap(ownContrib => + ownContrib.associatedContributions + .filter(associatedContrib => + associatedContrib.artist !== ownContrib.artist)); - if (empty(otherArtistContribs)) { - return {}; - } + const otherArtists = + unique( + associatedContributionsByOtherArtists + .map(contrib => contrib.artist)); - const otherArtistLinks = - otherArtistContribs - .map(contrib => relation('linkArtist', contrib.artist)); - - return {otherArtistLinks}; + return {otherArtists}; }, - generate(relations) { - return relations.otherArtistLinks ?? null; - }, + relations: (relation, query) => ({ + artistLinks: + query.otherArtists + .map(artist => relation('linkArtist', artist)), + }), + + generate: (relations) => + relations.artistLinks, }; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunk.js b/src/content/dependencies/generateArtistInfoPageTracksChunk.js new file mode 100644 index 00000000..b42e4165 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js @@ -0,0 +1,67 @@ +import {unique} from '#sugar'; +import {getTotalDuration} from '#wiki-data'; + +export default { + contentDependencies: [ + 'generateArtistInfoPageChunk', + 'generateArtistInfoPageTracksChunkItem', + 'linkAlbum', + ], + + relations: (relation, artist, album, trackContribLists) => ({ + template: + relation('generateArtistInfoPageChunk'), + + albumLink: + relation('linkAlbum', album), + + // Intentional mapping here: each item may be associated with + // more than one contribution. + items: + trackContribLists.map(trackContribs => + relation('generateArtistInfoPageTracksChunkItem', + artist, + trackContribs)), + }), + + data(_artist, album, trackContribLists) { + const data = {}; + + const contribs = + trackContribLists.flat(); + + data.dates = + contribs + .map(contrib => contrib.date); + + // TODO: Duration stuff should *maybe* be in proper data logic? Maaaybe? + const durationTerms = + unique( + contribs + .filter(contrib => contrib.countInDurationTotals) + .map(contrib => contrib.thing) + .filter(track => track.isOriginalRelease) + .filter(track => track.duration > 0)); + + data.duration = + getTotalDuration(durationTerms); + + data.durationApproximate = + durationTerms.length > 1; + + return data; + }, + + generate: (data, relations) => + relations.template.slots({ + mode: 'album', + + albumLink: relations.albumLink, + + dates: data.dates, + duration: data.duration, + durationApproximate: data.durationApproximate, + + items: relations.items, + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js new file mode 100644 index 00000000..96976826 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js @@ -0,0 +1,115 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generateArtistInfoPageChunkItem', + 'generateArtistInfoPageOtherArtistLinks', + 'linkTrack', + ], + + extraDependencies: ['html', 'language'], + + query (_artist, contribs) { + const query = {}; + + // TODO: Very mysterious what to do if the set of contributions is, + // in total, associated with more than one thing. No design yet. + query.track = + contribs[0].thing; + + const creditedAsArtist = + contribs + .some(contrib => contrib.isArtistContribution); + + const creditedAsContributor = + contribs + .some(contrib => contrib.isContributorContribution); + + const annotatedContribs = + contribs + .filter(contrib => contrib.annotation); + + const annotatedArtistContribs = + annotatedContribs + .filter(contrib => contrib.isArtistContribution); + + const annotatedContributorContribs = + annotatedContribs + .filter(contrib => contrib.isContributorContribution); + + // Don't display annotations associated with crediting in the + // Contributors field if the artist is also credited as an Artist + // *and* the Artist-field contribution is non-annotated. This is + // so that we don't misrepresent the artist - the contributor + // annotation tends to be for "secondary" and performance roles. + // For example, this avoids crediting Marcy Nabors on Renewed + // Return seemingly only for "bass clarinet" when they're also + // the one who composed and arranged Renewed Return! + if ( + creditedAsArtist && + creditedAsContributor && + empty(annotatedArtistContribs) + ) { + query.displayedContributions = null; + } else if ( + !empty(annotatedArtistContribs) || + !empty(annotatedContributorContribs) + ) { + query.displayedContributions = [ + ...annotatedArtistContribs, + ...annotatedContributorContribs, + ]; + } + + return query; + }, + + relations: (relation, query, artist, contribs) => ({ + template: + relation('generateArtistInfoPageChunkItem'), + + trackLink: + relation('linkTrack', query.track), + + otherArtistLinks: + relation('generateArtistInfoPageOtherArtistLinks', contribs), + }), + + data: (query) => ({ + duration: + query.track.duration, + + rerelease: + query.track.isRerelease, + + contribAnnotations: + (query.displayedContributions + ? query.displayedContributions + .map(contrib => contrib.annotation) + : null), + }), + + generate: (data, relations, {html, language}) => + relations.template.slots({ + otherArtistLinks: relations.otherArtistLinks, + rerelease: data.rerelease, + + annotation: + (data.contribAnnotations + ? language.formatUnitList(data.contribAnnotations) + : html.blank()), + + content: + language.encapsulate('artistPage.creditList.entry.track', workingCapsule => { + const workingOptions = {track: relations.trackLink}; + + if (data.duration) { + workingCapsule += '.withDuration'; + workingOptions.duration = + language.formatDuration(data.duration); + } + + return language.$(workingCapsule, workingOptions); + }), + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index bce6cedf..7c01accb 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -1,293 +1,65 @@ -import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort'; -import {accumulateSum, chunkByProperties, empty, stitchArrays} from '#sugar'; +import {sortAlbumsTracksChronologically, sortContributionsChronologically} + from '#sort'; +import {chunkByConditions, stitchArrays} from '#sugar'; export default { contentDependencies: [ - 'generateArtistInfoPageChunk', 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageOtherArtistLinks', - 'linkAlbum', - 'linkTrack', + 'generateArtistInfoPageTracksChunk', ], - extraDependencies: ['html', 'language'], - query(artist) { - const processTrackEntry = ({track, contribs}) => ({ - thing: track, - entry: { - track: track, - album: track.album, - date: track.date, - contribs: contribs, - }, - }); - - const processTrackEntries = ({tracks, contribs}) => - stitchArrays({ - track: tracks, - contribs: contribs, - }).map(processTrackEntry); - - const {tracksAsArtist, tracksAsContributor} = artist; - - const tracksAsArtistAndContributor = - tracksAsArtist - .filter(track => tracksAsContributor.includes(track)); - - const tracksAsArtistOnly = - tracksAsArtist - .filter(track => !tracksAsContributor.includes(track)); - - const tracksAsContributorOnly = - tracksAsContributor - .filter(track => !tracksAsArtist.includes(track)); - - const tracksAsArtistAndContributorContribs = - tracksAsArtistAndContributor - .map(track => [ - ... - track.artistContribs - .map(contrib => ({...contrib, kind: 'artist'})), - ... - track.contributorContribs - .map(contrib => ({...contrib, kind: 'contributor'})), - ]); - - const tracksAsArtistOnlyContribs = - tracksAsArtistOnly - .map(track => track.artistContribs - .map(contrib => ({...contrib, kind: 'artist'}))); - - const tracksAsContributorOnlyContribs = - tracksAsContributorOnly - .map(track => track.contributorContribs - .map(contrib => ({...contrib, kind: 'contributor'}))); + const query = {}; - const tracksAsArtistAndContributorEntries = - processTrackEntries({ - tracks: tracksAsArtistAndContributor, - contribs: tracksAsArtistAndContributorContribs, - }); - - const tracksAsArtistOnlyEntries = - processTrackEntries({ - tracks: tracksAsArtistOnly, - contribs: tracksAsArtistOnlyContribs, - }); - - const tracksAsContributorOnlyEntries = - processTrackEntries({ - tracks: tracksAsContributorOnly, - contribs: tracksAsContributorOnlyContribs, - }); - - const entries = [ - ...tracksAsArtistAndContributorEntries, - ...tracksAsArtistOnlyEntries, - ...tracksAsContributorOnlyEntries, + const allContributions = [ + ...artist.trackArtistContributions, + ...artist.trackContributorContributions, ]; - sortEntryThingPairs(entries, sortAlbumsTracksChronologically); - - const chunks = - chunkByProperties( - entries.map(({entry}) => entry), - ['album', 'date']); - - return {chunks}; - }, - - relations(relation, query, artist) { - return { - chunkedList: - relation('generateArtistInfoPageChunkedList'), - - chunks: - query.chunks.map(() => relation('generateArtistInfoPageChunk')), - - albumLinks: - query.chunks.map(({album}) => relation('linkAlbum', album)), - - items: - query.chunks.map(({chunk}) => - chunk.map(() => relation('generateArtistInfoPageChunkItem'))), - - trackLinks: - query.chunks.map(({chunk}) => - chunk.map(({track}) => relation('linkTrack', track))), - - trackOtherArtistLinks: - query.chunks.map(({chunk}) => - chunk.map(({contribs}) => relation('generateArtistInfoPageOtherArtistLinks', contribs, artist))), - }; - }, - - data(query, artist) { - return { - chunkDates: - query.chunks.map(({date}) => date), - - chunkDurations: - query.chunks.map(({chunk}) => - accumulateSum( - chunk - .filter(({track}) => track.duration && track.originalReleaseTrack === null) - .map(({track}) => track.duration))), - - chunkDurationsApproximate: - query.chunks.map(({chunk}) => - chunk - .filter(({track}) => track.duration && track.originalReleaseTrack === null) - .length > 1), - - trackDurations: - query.chunks.map(({chunk}) => - chunk.map(({track}) => track.duration)), - - trackContributions: - query.chunks.map(({chunk}) => - chunk - .map(({contribs}) => - contribs.filter(contrib => contrib.artist === artist)) - .map(ownContribs => ({ - creditedAsArtist: - ownContribs - .some(({kind}) => kind === 'artist'), - - creditedAsContributor: - ownContribs - .some(({kind}) => kind === 'contributor'), - - annotatedContribs: - ownContribs - .filter(({annotation}) => annotation), - })) - .map(({annotatedContribs, ...rest}) => ({ - ...rest, - - annotatedArtistContribs: - annotatedContribs - .filter(({kind}) => kind === 'artist'), - - annotatedContributorContribs: - annotatedContribs - .filter(({kind}) => kind === 'contributor'), - })) - .map(({ - creditedAsArtist, - creditedAsContributor, - annotatedArtistContribs, - annotatedContributorContribs, - }) => { - // Don't display annotations associated with crediting in the - // Contributors field if the artist is also credited as an Artist - // *and* the Artist-field contribution is non-annotated. This is - // so that we don't misrepresent the artist - the contributor - // annotation tends to be for "secondary" and performance roles. - // For example, this avoids crediting Marcy Nabors on Renewed - // Return seemingly only for "bass clarinet" when they're also - // the one who composed and arranged Renewed Return! - if ( - creditedAsArtist && - creditedAsContributor && - empty(annotatedArtistContribs) - ) { - return []; - } - - return [ - ...annotatedArtistContribs, - ...annotatedContributorContribs, - ]; - }) - .map(contribs => - contribs.map(({annotation}) => annotation)) - .map(contributions => - (empty(contributions) - ? null - : contributions))), - - trackRereleases: - query.chunks.map(({chunk}) => - chunk.map(({track}) => track.originalReleaseTrack !== null)), - }; + sortContributionsChronologically( + allContributions, + sortAlbumsTracksChronologically); + + query.contribs = + // First chunk by (contribution) date and album. + chunkByConditions(allContributions, [ + ({date: date1}, {date: date2}) => + +date1 !== +date2, + ({thing: track1}, {thing: track2}) => + track1.album !== track2.album, + ]).map(contribs => + // Then, *within* the boundaries of the existing chunks, + // chunk contributions to the same thing together. + chunkByConditions(contribs, [ + ({thing: thing1}, {thing: thing2}) => + thing1 !== thing2, + ])); + + query.albums = + query.contribs + .map(contribs => + contribs[0][0].thing.album); + + return query; }, - generate(data, relations, {html, language}) { - return relations.chunkedList.slots({ - chunks: - stitchArrays({ - chunk: relations.chunks, - albumLink: relations.albumLinks, - date: data.chunkDates, - duration: data.chunkDurations, - durationApproximate: data.chunkDurationsApproximate, - - items: relations.items, - trackLinks: relations.trackLinks, - trackOtherArtistLinks: relations.trackOtherArtistLinks, - trackDurations: data.trackDurations, - trackContributions: data.trackContributions, - trackRereleases: data.trackRereleases, - }).map(({ - chunk, - albumLink, - date, - duration, - durationApproximate, + relations: (relation, query, artist) => ({ + chunkedList: + relation('generateArtistInfoPageChunkedList'), - items, - trackLinks, - trackOtherArtistLinks, - trackDurations, - trackContributions, - trackRereleases, - }) => - chunk.slots({ - mode: 'album', - albumLink, - date, - duration, - durationApproximate, - - items: - stitchArrays({ - item: items, - trackLink: trackLinks, - otherArtistLinks: trackOtherArtistLinks, - duration: trackDurations, - contribution: trackContributions, - rerelease: trackRereleases, - }).map(({ - item, - trackLink, - otherArtistLinks, - duration, - contribution, - rerelease, - }) => - item.slots({ - otherArtistLinks, - rerelease, - - annotation: - (contribution - ? language.formatUnitList(contribution) - : html.blank()), - - content: - (duration - ? language.$('artistPage.creditList.entry.track.withDuration', { - track: trackLink, - duration: language.formatDuration(duration), - }) - : language.$('artistPage.creditList.entry.track', { - track: trackLink, - })), - })), - })), - }); - }, + chunks: + stitchArrays({ + album: query.albums, + contribs: query.contribs, + }).map(({album, contribs}) => + relation('generateArtistInfoPageTracksChunk', + artist, + album, + contribs)), + }), + + generate: (relations) => + relations.chunkedList.slots({ + chunks: relations.chunks, + }), }; diff --git a/src/content/dependencies/generateArtistNavLinks.js b/src/content/dependencies/generateArtistNavLinks.js index aa95dba2..527e4741 100644 --- a/src/content/dependencies/generateArtistNavLinks.js +++ b/src/content/dependencies/generateArtistNavLinks.js @@ -24,8 +24,8 @@ export default { relation('linkArtist', artist); if ( - !empty(artist.albumsAsCoverArtist) || - !empty(artist.tracksAsCoverArtist) + !empty(artist.albumCoverArtistContributions) || + !empty(artist.trackCoverArtistContributions) ) { relations.artistGalleryLink = relation('linkArtistGallery', artist); diff --git a/src/content/dependencies/generateChronologyLinks.js b/src/content/dependencies/generateChronologyLinks.js deleted file mode 100644 index 8ec6ee0a..00000000 --- a/src/content/dependencies/generateChronologyLinks.js +++ /dev/null @@ -1,82 +0,0 @@ -import {accumulateSum, empty} from '#sugar'; - -export default { - extraDependencies: ['html', 'language'], - - slots: { - chronologyInfoSets: { - validate: v => - v.strictArrayOf( - v.validateProperties({ - headingString: v.isString, - contributions: v.strictArrayOf(v.validateProperties({ - index: v.isCountingNumber, - artistLink: v.isHTML, - previousLink: v.isHTML, - nextLink: v.isHTML, - })), - })), - } - }, - - generate(slots, {html, language}) { - if (empty(slots.chronologyInfoSets)) { - return html.blank(); - } - - const totalContributionCount = - accumulateSum( - slots.chronologyInfoSets, - ({contributions}) => contributions.length); - - if (totalContributionCount === 0) { - return html.blank(); - } - - if (totalContributionCount > 8) { - return html.tag('div', {class: 'chronology'}, - language.$('misc.chronology.seeArtistPages')); - } - - return html.tags( - slots.chronologyInfoSets.map(({ - headingString, - contributions, - }) => - contributions.map(({ - index, - artistLink, - previousLink, - nextLink, - }) => { - const heading = - html.tag('span', {class: 'heading'}, - language.$(headingString, { - index: language.formatIndex(index), - artist: artistLink, - })); - - const navigation = - (previousLink || nextLink) && - html.tag('span', {class: 'buttons'}, - language.formatUnitList([ - previousLink?.slots({ - tooltipStyle: 'browser', - color: false, - content: language.$('misc.nav.previous'), - }), - - nextLink?.slots({ - tooltipStyle: 'browser', - color: false, - content: language.$('misc.nav.next'), - }), - ].filter(Boolean))); - - return html.tag('div', {class: 'chronology'}, - (navigation - ? language.$('misc.chronology.withNavigation', {heading, navigation}) - : heading)); - }))); - }, -}; diff --git a/src/content/dependencies/generateColorStyleVariables.js b/src/content/dependencies/generateColorStyleVariables.js index 069d85dd..5270dbe4 100644 --- a/src/content/dependencies/generateColorStyleVariables.js +++ b/src/content/dependencies/generateColorStyleVariables.js @@ -32,6 +32,7 @@ export default { dim, deep, deepGhost, + lightGhost, bg, bgBlack, shadow, @@ -43,6 +44,7 @@ export default { `--dim-color: ${dim}`, `--deep-color: ${deep}`, `--deep-ghost-color: ${deepGhost}`, + `--light-ghost-color: ${lightGhost}`, `--bg-color: ${bg}`, `--bg-black-color: ${bgBlack}`, `--shadow-color: ${shadow}`, diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index 522a0284..7c4aed80 100644 --- a/src/content/dependencies/generateCommentaryEntry.js +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -43,56 +43,71 @@ export default { color: {validate: v => v.isColor}, }, - generate(data, relations, slots, {html, language}) { - const artistsSpan = - html.tag('span', {class: 'commentary-entry-artists'}, - (relations.artistsContent - ? relations.artistsContent.slot('mode', 'inline') - : relations.artistLinks - ? language.formatConjunctionList(relations.artistLinks) - : language.$('misc.artistCommentary.entry.title.noArtists'))); - - const accentParts = ['misc.artistCommentary.entry.title.accent']; - const accentOptions = {}; - - if (relations.annotationContent) { - accentParts.push('withAnnotation'); - accentOptions.annotation = - relations.annotationContent.slot('mode', 'inline'); - } - - if (data.date) { - accentParts.push('withDate'); - accentOptions.date = - language.formatDate(data.date); - } - - const accent = - (accentParts.length > 1 - ? html.tag('span', {class: 'commentary-entry-accent'}, - language.$(...accentParts, accentOptions)) - : null); - - const titleParts = ['misc.artistCommentary.entry.title']; - const titleOptions = {artists: artistsSpan}; - - if (accent) { - titleParts.push('withAccent'); - titleOptions.accent = accent; - } - - const style = - slots.color && - relations.colorStyle.slot('color', slots.color); - - return html.tags([ - html.tag('p', {class: 'commentary-entry-heading'}, - style, - language.$(...titleParts, titleOptions)), - - html.tag('blockquote', {class: 'commentary-entry-body'}, - style, - relations.bodyContent.slot('mode', 'multiline')), - ]); - }, + generate: (data, relations, slots, {html, language}) => + language.encapsulate('misc.artistCommentary.entry', entryCapsule => + html.tags([ + html.tag('p', {class: 'commentary-entry-heading'}, + slots.color && + relations.colorStyle.clone() + .slot('color', slots.color), + + language.encapsulate(entryCapsule, 'title', titleCapsule => [ + html.tag('time', + {[html.onlyIfContent]: true}, + + language.$(titleCapsule, 'date', { + [language.onlyIfOptions]: ['date'], + + date: + language.formatDate(data.date), + })), + + language.encapsulate(titleCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.artists = + html.tag('span', {class: 'commentary-entry-artists'}, + (relations.artistsContent + ? relations.artistsContent.slot('mode', 'inline') + : relations.artistLinks + ? language.formatConjunctionList(relations.artistLinks) + : language.$(titleCapsule, 'noArtists'))); + + const accent = + html.tag('span', {class: 'commentary-entry-accent'}, + {[html.onlyIfContent]: true}, + + language.encapsulate(titleCapsule, 'accent', accentCapsule => + language.encapsulate(accentCapsule, workingCapsule => { + const workingOptions = {}; + + if (relations.annotationContent) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = + relations.annotationContent.slot('mode', 'inline'); + } + + if (workingCapsule === accentCapsule) { + return html.blank(); + } else { + return language.$(workingCapsule, workingOptions); + } + }))); + + if (!html.isBlank(accent)) { + workingCapsule += '.withAccent'; + workingOptions.accent = accent; + } + + return language.$(workingCapsule, workingOptions); + }), + ])), + + html.tag('blockquote', {class: 'commentary-entry-body'}, + slots.color && + relations.colorStyle.clone() + .slot('color', slots.color), + + relations.bodyContent.slot('mode', 'multiline')), + ])), }; diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js index 3c3504d2..d68ba42e 100644 --- a/src/content/dependencies/generateCommentaryIndexPage.js +++ b/src/content/dependencies/generateCommentaryIndexPage.js @@ -57,46 +57,48 @@ export default { }; }, - generate(data, relations, {html, language}) { - return relations.layout.slots({ - title: language.$('commentaryIndex.title'), - - headingMode: 'static', - - mainClasses: ['long-content'], - mainContent: [ - html.tag('p', language.$('commentaryIndex.infoLine', { - words: - html.tag('b', - language.formatWordCount(data.totalWordCount, {unit: true})), - - entries: - html.tag('b', - language.countCommentaryEntries(data.totalEntryCount, {unit: true})), - })), - - html.tag('p', - language.$('commentaryIndex.albumList.title')), - - html.tag('ul', - stitchArrays({ - albumLink: relations.albumLinks, - wordCount: data.wordCounts, - entryCount: data.entryCounts, - }).map(({albumLink, wordCount, entryCount}) => - html.tag('li', - language.$('commentaryIndex.albumList.item', { - album: albumLink, - words: language.formatWordCount(wordCount, {unit: true}), - entries: language.countCommentaryEntries(entryCount, {unit: true}), - })))), - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {auto: 'current'}, - ], - }); - }, + generate: (data, relations, {html, language}) => + language.encapsulate('commentaryIndex', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title'), + + headingMode: 'static', + + mainClasses: ['long-content'], + mainContent: [ + html.tag('p', language.$(pageCapsule, 'infoLine', { + words: + html.tag('b', + language.formatWordCount(data.totalWordCount, {unit: true})), + + entries: + html.tag('b', + language.countCommentaryEntries(data.totalEntryCount, {unit: true})), + })), + + language.encapsulate(pageCapsule, 'albumList', listCapsule => [ + html.tag('p', + language.$(listCapsule, 'title')), + + html.tag('ul', + stitchArrays({ + albumLink: relations.albumLinks, + wordCount: data.wordCounts, + entryCount: data.entryCounts, + }).map(({albumLink, wordCount, entryCount}) => + html.tag('li', + language.$(listCapsule, 'item', { + album: albumLink, + words: language.formatWordCount(wordCount, {unit: true}), + entries: language.countCommentaryEntries(entryCount, {unit: true}), + })))), + ]), + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {auto: 'current'}, + ], + })), }; diff --git a/src/content/dependencies/generateCommentarySection.js b/src/content/dependencies/generateCommentarySection.js index 8ae1b2d0..c5090660 100644 --- a/src/content/dependencies/generateCommentarySection.js +++ b/src/content/dependencies/generateCommentarySection.js @@ -1,3 +1,5 @@ +import {empty} from '#sugar'; + export default { contentDependencies: [ 'transformContent', @@ -12,16 +14,29 @@ export default { relation('generateContentHeading'), entries: - entries.map(entry => - relation('generateCommentaryEntry', entry)), + (entries + ? entries.map(entry => + relation('generateCommentaryEntry', entry)) + : []), + }), + + data: (entries) => ({ + firstEntryIsDated: + (empty(entries) + ? null + : !!entries[0].date), }), - generate: (relations, {html, language}) => + generate: (data, relations, {html, language}) => html.tags([ relations.heading .slots({ - id: 'artist-commentary', - title: language.$('misc.artistCommentary') + title: language.$('misc.artistCommentary'), + attributes: [ + {id: 'artist-commentary'}, + data.firstEntryIsDated && + {class: 'first-entry-is-dated'}, + ], }), relations.entries, diff --git a/src/content/dependencies/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js index 469db876..f52bc043 100644 --- a/src/content/dependencies/generateContentHeading.js +++ b/src/content/dependencies/generateContentHeading.js @@ -12,23 +12,35 @@ export default { mutable: false, }, + stickyTitle: { + type: 'html', + mutable: false, + }, + accent: { type: 'html', mutable: false, }, + attributes: { + type: 'attributes', + mutable: false, + }, + color: {validate: v => v.isColor}, - id: {type: 'string'}, - tag: {type: 'string', default: 'p'}, + tag: { + type: 'string', + default: 'p', + }, }, generate: (relations, slots, {html}) => html.tag(slots.tag, {class: 'content-heading'}, {tabindex: '0'}, + {[html.onlyIfSiblings]: true}, - slots.id && - {id: slots.id}, + slots.attributes, slots.color && relations.colorStyle.slot('color', slots.color), @@ -38,6 +50,10 @@ export default { {[html.onlyIfContent]: true}, slots.title), + html.tag('template', {class: 'content-heading-sticky-title'}, + {[html.onlyIfContent]: true}, + slots.stickyTitle), + html.tag('span', {class: 'content-heading-accent'}, {[html.onlyIfContent]: true}, slots.accent), diff --git a/src/content/dependencies/generateContributionList.js b/src/content/dependencies/generateContributionList.js index 6401e65e..8e8c5020 100644 --- a/src/content/dependencies/generateContributionList.js +++ b/src/content/dependencies/generateContributionList.js @@ -2,20 +2,28 @@ export default { contentDependencies: ['linkContribution'], extraDependencies: ['html'], - relations: (relation, contributions) => - ({contributionLinks: - contributions - .map(contrib => relation('linkContribution', contrib))}), + relations: (relation, contributions) => ({ + contributionLinks: + contributions + .map(contrib => relation('linkContribution', contrib)), + }), - generate: (relations, {html}) => + slots: { + chronologyKind: {type: 'string'}, + }, + + generate: (relations, slots, {html}) => html.tag('ul', - relations.contributionLinks.map(contributionLink => - html.tag('li', - contributionLink - .slots({ - showIcons: true, + {[html.onlyIfContent]: true}, + + relations.contributionLinks + .map(contributionLink => + html.tag('li', + contributionLink.slots({ + showExternalLinks: true, showContribution: true, + showChronology: true, preventWrapping: false, - iconMode: 'tooltip', + chronologyKind: slots.chronologyKind, })))), }; diff --git a/src/content/dependencies/generateContributionTooltip.js b/src/content/dependencies/generateContributionTooltip.js new file mode 100644 index 00000000..3a31014d --- /dev/null +++ b/src/content/dependencies/generateContributionTooltip.js @@ -0,0 +1,48 @@ +export default { + contentDependencies: [ + 'generateContributionTooltipChronologySection', + 'generateContributionTooltipExternalLinkSection', + 'generateTooltip', + ], + + extraDependencies: ['html'], + + relations: (relation, contribution) => ({ + tooltip: + relation('generateTooltip'), + + externalLinkSection: + relation('generateContributionTooltipExternalLinkSection', contribution), + + chronologySection: + relation('generateContributionTooltipChronologySection', contribution), + }), + + slots: { + showExternalLinks: {type: 'boolean'}, + showChronology: {type: 'boolean'}, + + chronologyKind: {type: 'string'}, + }, + + generate: (relations, slots, {html}) => + relations.tooltip.slots({ + attributes: + {class: 'contribution-tooltip'}, + + contentAttributes: { + [html.joinChildren]: + html.tag('span', {class: 'tooltip-divider'}), + }, + + content: [ + slots.showExternalLinks && + relations.externalLinkSection, + + slots.showChronology && + relations.chronologySection.slots({ + kind: slots.chronologyKind, + }), + ], + }), +}; diff --git a/src/content/dependencies/generateContributionTooltipChronologySection.js b/src/content/dependencies/generateContributionTooltipChronologySection.js new file mode 100644 index 00000000..78c9051c --- /dev/null +++ b/src/content/dependencies/generateContributionTooltipChronologySection.js @@ -0,0 +1,117 @@ +export default { + contentDependencies: ['linkAnythingMan'], + extraDependencies: ['html', 'language'], + + query(contribution) { + let previous = contribution; + while (previous && previous.thing === contribution.thing) { + previous = previous.previousBySameArtist; + } + + let next = contribution; + while (next && next.thing === contribution.thing) { + next = next.nextBySameArtist; + } + + return {previous, next}; + }, + + relations: (relation, query, _contribution) => ({ + previousLink: + (query.previous + ? relation('linkAnythingMan', query.previous.thing) + : null), + + nextLink: + (query.next + ? relation('linkAnythingMan', query.next.thing) + : null), + }), + + data: (query, _contribution) => ({ + previousName: + (query.previous + ? query.previous.thing.name + : null), + + nextName: + (query.next + ? query.next.thing.name + : null), + }), + + slots: { + kind: { + validate: v => + v.is( + 'album', + 'bannerArt', + 'coverArt', + 'flash', + 'track', + 'trackArt', + 'trackContribution', + 'wallpaperArt'), + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('misc.artistLink.chronology', capsule => + html.tags([ + html.tags([ + relations.previousLink?.slots({ + attributes: {class: 'chronology-link'}, + content: [ + html.tag('span', {class: 'chronology-symbol'}, + language.$(capsule, 'previous.symbol')), + + html.tag('span', {class: 'chronology-text'}, + language.sanitize(data.previousName)), + ], + }), + + html.tag('span', {class: 'chronology-info'}, + {[html.onlyIfSiblings]: true}, + + language.encapsulate(capsule, 'previous.info', workingCapsule => { + const workingOptions = {}; + + if (slots.kind) { + workingCapsule += '.withKind'; + workingOptions.kind = + language.$(capsule, 'kind', slots.kind); + } + + return language.$(workingCapsule, workingOptions); + })), + ]), + + html.tags([ + relations.nextLink?.slots({ + attributes: {class: 'chronology-link'}, + content: [ + html.tag('span', {class: 'chronology-symbol'}, + language.$(capsule, 'next.symbol')), + + html.tag('span', {class: 'chronology-text'}, + language.sanitize(data.nextName)), + ], + }), + + html.tag('span', {class: 'chronology-info'}, + {[html.onlyIfSiblings]: true}, + + language.encapsulate(capsule, 'next.info', workingCapsule => { + const workingOptions = {}; + + if (slots.kind) { + workingCapsule += '.withKind'; + workingOptions.kind = + language.$(capsule, 'kind', slots.kind); + } + + return language.$(workingCapsule, workingOptions); + })) + ]), + ])), +}; diff --git a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js new file mode 100644 index 00000000..4f9a23ed --- /dev/null +++ b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js @@ -0,0 +1,70 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateExternalHandle', + 'generateExternalIcon', + 'generateExternalPlatform', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, contribution) => ({ + icons: + contribution.artist.urls + .map(url => relation('generateExternalIcon', url)), + + handles: + contribution.artist.urls + .map(url => relation('generateExternalHandle', url)), + + platforms: + contribution.artist.urls + .map(url => relation('generateExternalPlatform', url)), + }), + + data: (contribution) => ({ + urls: contribution.artist.urls, + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('misc.artistLink', capsule => + html.tags( + stitchArrays({ + icon: relations.icons, + handle: relations.handles, + platform: relations.platforms, + url: data.urls, + }).map(({icon, handle, platform, url}) => { + for (const template of [icon, handle, platform]) { + template.setSlot('context', 'artist'); + } + + return [ + html.tag('a', {class: 'external-link'}, + {href: url}, + + [ + icon, + + html.tag('span', {class: 'external-handle'}, + (html.isBlank(handle) + ? platform + : handle)), + ]), + + html.tag('span', {class: 'external-platform'}, + // This is a pretty ridiculous hack, but we currently + // don't have a way of telling formatExternalLink to *not* + // use the fallback string, which just formats the URL as + // its host/domain... so is technically detectable. + (((new URL(url)) + .host + .endsWith( + html.resolve(platform, {normalize: 'string'}))) + + ? language.$(capsule, 'noExternalLinkPlatformName') + : platform)), + ]; + }))), +}; diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index 90c9db98..3d5a614f 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -1,4 +1,4 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { contentDependencies: ['image', 'linkArtTag'], @@ -89,14 +89,15 @@ export default { ...sizeSlots, }), - !empty(relations.tagLinks) && - html.tag('ul', {class: 'image-details'}, - stitchArrays({ - tagLink: relations.tagLinks, - preferShortName: data.preferShortName, - }).map(({tagLink, preferShortName}) => - html.tag('li', - tagLink.slot('preferShortName', preferShortName)))), + html.tag('ul', {class: 'image-details'}, + {[html.onlyIfContent]: true}, + + stitchArrays({ + tagLink: relations.tagLinks, + preferShortName: data.preferShortName, + }).map(({tagLink, preferShortName}) => + html.tag('li', + tagLink.slot('preferShortName', preferShortName)))), ]); case 'thumbnail': diff --git a/src/content/dependencies/generateExternalHandle.js b/src/content/dependencies/generateExternalHandle.js new file mode 100644 index 00000000..8c0368a4 --- /dev/null +++ b/src/content/dependencies/generateExternalHandle.js @@ -0,0 +1,20 @@ +import {isExternalLinkContext} from '#external-links'; + +export default { + extraDependencies: ['html', 'language'], + + data: (url) => ({url}), + + slots: { + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, slots, {language}) => + language.formatExternalLink(data.url, { + style: 'handle', + context: slots.context, + }), +}; diff --git a/src/content/dependencies/generateExternalIcon.js b/src/content/dependencies/generateExternalIcon.js new file mode 100644 index 00000000..637af658 --- /dev/null +++ b/src/content/dependencies/generateExternalIcon.js @@ -0,0 +1,26 @@ +import {isExternalLinkContext} from '#external-links'; + +export default { + extraDependencies: ['html', 'language', 'to'], + + data: (url) => ({url}), + + slots: { + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, slots, {html, language, to}) => + html.tag('span', {class: 'external-icon'}, + html.tag('svg', + html.tag('use', { + href: + to('staticMisc.icon', + language.formatExternalLink(data.url, { + style: 'icon-id', + context: slots.context, + })), + }))), +}; diff --git a/src/content/dependencies/generateExternalPlatform.js b/src/content/dependencies/generateExternalPlatform.js new file mode 100644 index 00000000..c4f63ecf --- /dev/null +++ b/src/content/dependencies/generateExternalPlatform.js @@ -0,0 +1,20 @@ +import {isExternalLinkContext} from '#external-links'; + +export default { + extraDependencies: ['html', 'language'], + + data: (url) => ({url}), + + slots: { + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, slots, {language}) => + language.formatExternalLink(data.url, { + style: 'platform', + context: slots.context, + }), +}; diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js index 17078124..1fa6de51 100644 --- a/src/content/dependencies/generateFlashActGalleryPage.js +++ b/src/content/dependencies/generateFlashActGalleryPage.js @@ -11,7 +11,7 @@ export default { 'linkFlashIndex', ], - extraDependencies: ['html', 'language'], + extraDependencies: ['language'], relations: (relation, act) => ({ layout: @@ -50,42 +50,42 @@ export default { ['media.flashArt', flash.directory, flash.coverArtFileExtension]) }), - generate(data, relations, {html, language}) { - return relations.layout.slots({ - title: - language.$('flashPage.title', { - flash: new html.Tag(null, null, data.name), - }), - - color: data.color, - headingMode: 'static', - - mainClasses: ['flash-index'], - mainContent: [ - relations.coverGrid.slots({ - links: relations.flashLinks, - names: data.flashNames, - lazy: 6, - - images: - stitchArrays({ - image: relations.coverGridImages, - path: data.flashCoverPaths, - }).map(({image, path}) => - image.slot('path', path)), - }), - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {html: relations.flashIndexLink}, - {auto: 'current'}, - ], - - navBottomRowContent: relations.flashActNavAccent, - - leftSidebar: relations.sidebar, - }); - }, + generate: (data, relations, {language}) => + language.encapsulate('flashPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + flash: data.name, + }), + + color: data.color, + headingMode: 'static', + + mainClasses: ['flash-index'], + mainContent: [ + relations.coverGrid.slots({ + links: relations.flashLinks, + names: data.flashNames, + lazy: 6, + + images: + stitchArrays({ + image: relations.coverGridImages, + path: data.flashCoverPaths, + }).map(({image, path}) => + image.slot('path', path)), + }), + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {html: relations.flashIndexLink}, + {auto: 'current'}, + ], + + navBottomRowContent: relations.flashActNavAccent, + + leftSidebar: relations.sidebar, + })), }; diff --git a/src/content/dependencies/generateFlashCoverArtwork.js b/src/content/dependencies/generateFlashCoverArtwork.js index 374fa3f8..af03ae6b 100644 --- a/src/content/dependencies/generateFlashCoverArtwork.js +++ b/src/content/dependencies/generateFlashCoverArtwork.js @@ -1,12 +1,26 @@ export default { contentDependencies: ['generateCoverArtwork'], - relations: (relation) => - ({coverArtwork: relation('generateCoverArtwork')}), + relations: (relation) => ({ + coverArtwork: + relation('generateCoverArtwork'), + }), - data: (flash) => - ({path: ['media.flashArt', flash.directory, flash.coverArtFileExtension]}), + data: (flash) => ({ + path: + ['media.flashArt', flash.directory, flash.coverArtFileExtension], + + color: + flash.color, + + dimensions: + flash.coverArtDimensions, + }), generate: (data, relations) => - relations.coverArtwork.slot('path', data.path), + relations.coverArtwork.slots({ + path: data.path, + color: data.color, + dimensions: data.dimensions, + }), }; diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js index 36bfabae..a21bb49e 100644 --- a/src/content/dependencies/generateFlashIndexPage.js +++ b/src/content/dependencies/generateFlashIndexPage.js @@ -1,4 +1,4 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { contentDependencies: [ @@ -81,74 +81,77 @@ export default { }), generate: (data, relations, {html, language}) => - relations.layout.slots({ - title: language.$('flashIndex.title'), - headingMode: 'static', - - mainClasses: ['flash-index'], - mainContent: [ - !empty(data.jumpLinkLabels) && [ - html.tag('p', {class: 'quick-info'}, - language.$('misc.jumpTo')), - - html.tag('ul', {class: 'quick-info'}, - stitchArrays({ - colorStyle: relations.jumpLinkColorStyles, - anchor: data.jumpLinkAnchors, - label: data.jumpLinkLabels, - }).map(({colorStyle, anchor, label}) => - html.tag('li', - html.tag('a', - {href: '#' + anchor}, - colorStyle, - label)))), - ], + language.encapsulate('flashIndex', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title'), + headingMode: 'static', + + mainClasses: ['flash-index'], + mainContent: [ + html.tags([ + html.tag('p', {class: 'quick-info'}, + {[html.onlyIfSiblings]: true}, + language.$('misc.jumpTo')), + + html.tag('ul', {class: 'quick-info'}, + {[html.onlyIfContent]: true}, + stitchArrays({ + colorStyle: relations.jumpLinkColorStyles, + anchor: data.jumpLinkAnchors, + label: data.jumpLinkLabels, + }).map(({colorStyle, anchor, label}) => + html.tag('li', + html.tag('a', + {href: '#' + anchor}, + colorStyle, + label)))), + ]), - stitchArrays({ - colorStyle: relations.actColorStyles, - actLink: relations.actLinks, - anchor: data.actAnchors, - - coverGrid: relations.actCoverGrids, - coverGridImages: relations.actCoverGridImages, - coverGridLinks: relations.actCoverGridLinks, - coverGridNames: data.actCoverGridNames, - coverGridPaths: data.actCoverGridPaths, - }).map(({ - colorStyle, - actLink, - anchor, - - coverGrid, - coverGridImages, - coverGridLinks, - coverGridNames, - coverGridPaths, - }, index) => [ - html.tag('h2', - {id: anchor}, + stitchArrays({ + colorStyle: relations.actColorStyles, + actLink: relations.actLinks, + anchor: data.actAnchors, + + coverGrid: relations.actCoverGrids, + coverGridImages: relations.actCoverGridImages, + coverGridLinks: relations.actCoverGridLinks, + coverGridNames: data.actCoverGridNames, + coverGridPaths: data.actCoverGridPaths, + }).map(({ colorStyle, - actLink), - - coverGrid.slots({ - links: coverGridLinks, - names: coverGridNames, - lazy: index === 0 ? 4 : true, - - images: - stitchArrays({ - image: coverGridImages, - path: coverGridPaths, - }).map(({image, path}) => - image.slot('path', path)), - }), - ]), - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {auto: 'current'}, - ], - }), + actLink, + anchor, + + coverGrid, + coverGridImages, + coverGridLinks, + coverGridNames, + coverGridPaths, + }, index) => [ + html.tag('h2', + {id: anchor}, + colorStyle, + actLink), + + coverGrid.slots({ + links: coverGridLinks, + names: coverGridNames, + lazy: index === 0 ? 4 : true, + + images: + stitchArrays({ + image: coverGridImages, + path: coverGridPaths, + }).map(({image, path}) => + image.slot('path', path)), + }), + ]), + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {auto: 'current'}, + ], + })), }; diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js index 05964936..d06f0c01 100644 --- a/src/content/dependencies/generateFlashInfoPage.js +++ b/src/content/dependencies/generateFlashInfoPage.js @@ -19,180 +19,151 @@ export default { query(flash) { const query = {}; - if (flash.page || !empty(flash.urls)) { - query.urls = []; + query.urls = []; - if (flash.page) { - query.urls.push(`https://homestuck.com/story/${flash.page}`); - } + if (flash.page) { + query.urls.push(`https://homestuck.com/story/${flash.page}`); + } - if (!empty(flash.urls)) { - query.urls.push(...flash.urls); - } + if (!empty(flash.urls)) { + query.urls.push(...flash.urls); } return query; }, - relations(relation, query, flash) { - const relations = {}; - const sections = relations.sections = {}; - - relations.layout = - relation('generatePageLayout'); + relations: (relation, query, flash) => ({ + layout: + relation('generatePageLayout'), - relations.sidebar = - relation('generateFlashActSidebar', flash.act, flash); + sidebar: + relation('generateFlashActSidebar', flash.act, flash), - if (query.urls) { - relations.externalLinks = - query.urls.map(url => relation('linkExternal', url)); - } - - // TODO: Flashes always have cover art (#175) - /* eslint-disable-next-line no-constant-condition */ - if (true) { - relations.cover = - relation('generateFlashCoverArtwork', flash); - } + externalLinks: + query.urls + .map(url => relation('linkExternal', url)), - // Section: navigation bar + cover: + relation('generateFlashCoverArtwork', flash), - const nav = sections.nav = {}; + contentHeading: + relation('generateContentHeading'), - nav.flashActLink = - relation('linkFlashAct', flash.act); + flashActLink: + relation('linkFlashAct', flash.act), - nav.flashNavAccent = - relation('generateFlashNavAccent', flash); + flashNavAccent: + relation('generateFlashNavAccent', flash), - // Section: Featured tracks - - if (!empty(flash.featuredTracks)) { - const featuredTracks = sections.featuredTracks = {}; - - featuredTracks.heading = - relation('generateContentHeading'); - - featuredTracks.list = - relation('generateTrackList', flash.featuredTracks); - } - - // Section: Contributors - - if (!empty(flash.contributorContribs)) { - const contributors = sections.contributors = {}; - - contributors.heading = - relation('generateContentHeading'); - - contributors.list = - relation('generateContributionList', flash.contributorContribs); - } - - // Section: Artist commentary - - if (flash.commentary) { - sections.artistCommentary = - relation('generateCommentarySection', flash.commentary); - } + featuredTracksList: + relation('generateTrackList', flash.featuredTracks), - return relations; - }, + contributorContributionList: + relation('generateContributionList', flash.contributorContribs), - data(query, flash) { - const data = {}; + artistCommentarySection: + relation('generateCommentarySection', flash.commentary), + }), - data.name = flash.name; - data.color = flash.color; - data.date = flash.date; + data: (_query, flash) => ({ + name: + flash.name, - return data; - }, + color: + flash.color, - generate(data, relations, {html, language}) { - const {sections: sec} = relations; + date: + flash.date, + }), - return relations.layout.slots({ - title: - language.$('flashPage.title', { - flash: data.name, - }), + generate: (data, relations, {html, language}) => + language.encapsulate('flashPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + flash: data.name, + }), - color: data.color, - headingMode: 'sticky', + color: data.color, + headingMode: 'sticky', - cover: - (relations.cover - ? relations.cover.slots({ - alt: language.$('misc.alt.flashArt'), - }) - : null), + cover: + (relations.cover + ? relations.cover.slots({ + alt: language.$('misc.alt.flashArt'), + }) + : null), - mainContent: [ - html.tag('p', - language.$('releaseInfo.released', { - date: language.formatDate(data.date), - })), + mainContent: [ + html.tag('p', + language.$('releaseInfo.released', { + date: language.formatDate(data.date), + })), - relations.externalLinks && html.tag('p', + {[html.onlyIfContent]: true}, + language.$('releaseInfo.playOn', { + [language.onlyIfOptions]: ['links'], + links: language.formatDisjunctionList( relations.externalLinks .map(link => link.slot('context', 'flash'))), })), - html.tag('p', - {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, - - [ - sec.artistCommentary && - language.$('releaseInfo.readCommentary', { - link: html.tag('a', - {href: '#artist-commentary'}, - language.$('releaseInfo.readCommentary.link')), + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + language.encapsulate('releaseInfo', capsule => [ + !html.isBlank(relations.artistCommentarySection) && + language.encapsulate(capsule, 'readCommentary', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#artist-commentary'}, + language.$(capsule, 'link')), + })), + ])), + + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'features'}, + title: + language.$('releaseInfo.tracksFeatured', { + flash: html.tag('i', data.name), + }), }), - ]), - sec.featuredTracks && [ - sec.featuredTracks.heading - .slots({ - id: 'features', - title: - language.$('releaseInfo.tracksFeatured', { - flash: html.tag('i', data.name), - }), - }), + relations.featuredTracksList, + ]), - sec.featuredTracks.list, - ], + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'contributors'}, + title: language.$('releaseInfo.contributors'), + }), - sec.contributors && [ - sec.contributors.heading - .slots({ - id: 'contributors', - title: language.$('releaseInfo.contributors'), + relations.contributorContributionList.slots({ + chronologyKind: 'flash', }), + ]), - sec.contributors.list, + relations.artistCommentarySection, ], - sec.artistCommentary, - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {html: sec.nav.flashActLink.slot('color', false)}, - {auto: 'current'}, - ], + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {html: relations.flashActLink.slot('color', false)}, + {auto: 'current'}, + ], - navBottomRowContent: sec.nav.flashNavAccent, + navBottomRowContent: relations.flashNavAccent, - leftSidebar: relations.sidebar, - }); - }, + leftSidebar: relations.sidebar, + })), }; diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index d07847c6..ceb54322 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -10,6 +10,7 @@ export default { 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', + 'generateQuickDescription', 'image', 'linkAlbum', 'linkListing', @@ -55,6 +56,9 @@ export default { .map(album => relation('image', album.artTags)); } + relations.quickDescription = + relation('generateQuickDescription', group); + relations.coverGrid = relation('generateCoverGrid'); @@ -107,10 +111,10 @@ export default { return data; }, - generate(data, relations, {html, language}) { - return relations.layout - .slots({ - title: language.$('groupGalleryPage.title', {group: data.name}), + generate: (data, relations, {html, language}) => + language.encapsulate('groupGalleryPage', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title', {group: data.name}), headingMode: 'static', color: data.color, @@ -128,8 +132,10 @@ export default { image.slot('path', path)), }), + relations.quickDescription, + html.tag('p', {class: 'quick-info'}, - language.$('groupGalleryPage.infoLine', { + language.$(pageCapsule, 'infoLine', { tracks: html.tag('b', language.countTracks(data.numTracks, { @@ -193,6 +199,5 @@ export default { secondaryNav: relations.secondaryNav ?? null, - }); - }, + })), }; diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js index b5b456aa..87f35656 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -1,210 +1,82 @@ -import {empty, stitchArrays} from '#sugar'; - export default { contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateColorStyleAttribute', - 'generateContentHeading', + 'generateGroupInfoPageAlbumsSection', 'generateGroupNavLinks', 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', - 'linkAlbum', 'linkExternal', - 'linkGroupGallery', - 'linkGroup', 'transformContent', ], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({wikiInfo}) { - return { - enableGroupUI: wikiInfo.enableGroupUI, - }; - }, - - query(sprawl, group) { - const albums = - group.albums; - - const albumGroups = - albums - .map(album => album.groups); - - const albumOtherCategory = - albumGroups - .map(groups => groups - .map(group => group.category) - .find(category => category !== group.category)); - - const albumOtherGroups = - stitchArrays({ - groups: albumGroups, - category: albumOtherCategory, - }).map(({groups, category}) => - groups - .filter(group => group.category === category)); - - return {albums, albumOtherGroups}; - }, - - relations(relation, query, sprawl, group) { - const relations = {}; - const sec = relations.sections = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.navLinks = - relation('generateGroupNavLinks', group); - - if (sprawl.enableGroupUI) { - relations.secondaryNav = - relation('generateGroupSecondaryNav', group); - - relations.sidebar = - relation('generateGroupSidebar', group); - } + sprawl: ({wikiInfo}) => ({ + enableGroupUI: + wikiInfo.enableGroupUI, + }), - sec.info = {}; + relations: (relation, sprawl, group) => ({ + layout: + relation('generatePageLayout'), - if (!empty(group.urls)) { - sec.info.visitLinks = - group.urls - .map(url => relation('linkExternal', url)); - } + navLinks: + relation('generateGroupNavLinks', group), - if (group.description) { - sec.info.description = - relation('transformContent', group.description); - } + secondaryNav: + (sprawl.enableGroupUI + ? relation('generateGroupSecondaryNav', group) + : null), - if (!empty(query.albums)) { - sec.albums = {}; + sidebar: + (sprawl.enableGroupUI + ? relation('generateGroupSidebar', group) + : null), - sec.albums.heading = - relation('generateContentHeading'); + visitLinks: + group.urls + .map(url => relation('linkExternal', url)), - sec.albums.galleryLink = - relation('linkGroupGallery', group); + description: + relation('transformContent', group.description), - sec.albums.albumColorStyles = - query.albums - .map(album => relation('generateColorStyleAttribute', album.color)); + albumSection: + relation('generateGroupInfoPageAlbumsSection', group), + }), - sec.albums.albumLinks = - query.albums - .map(album => relation('linkAlbum', album)); + data: (_sprawl, group) => ({ + name: + group.name, - sec.albums.otherGroupLinks = - query.albumOtherGroups - .map(groups => groups - .map(group => relation('linkGroup', group))); + color: + group.color, + }), - sec.albums.datetimestamps = - group.albums.map(album => - (album.date - ? relation('generateAbsoluteDatetimestamp', album.date) - : null)); - } - - return relations; - }, - - data(query, sprawl, group) { - const data = {}; - - data.name = group.name; - data.color = group.color; - - return data; - }, - - generate(data, relations, {html, language}) { - const {sections: sec} = relations; - - return relations.layout - .slots({ - title: language.$('groupInfoPage.title', {group: data.name}), + generate: (data, relations, {html, language}) => + language.encapsulate('groupInfoPage', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title', {group: data.name}), headingMode: 'sticky', color: data.color, mainContent: [ - sec.info.visitLinks && - html.tag('p', - language.$('releaseInfo.visitOn', { - links: - language.formatDisjunctionList( - sec.info.visitLinks - .map(link => link.slot('context', 'group'))), - })), + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$('releaseInfo.visitOn', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatDisjunctionList( + relations.visitLinks + .map(link => link.slot('context', 'group'))), + })), html.tag('blockquote', {[html.onlyIfContent]: true}, - sec.info.description - ?.slot('mode', 'multiline')), - - sec.albums && [ - sec.albums.heading - .slots({ - tag: 'h2', - title: language.$('groupInfoPage.albumList.title'), - }), - - html.tag('p', - language.$('groupInfoPage.viewAlbumGallery', { - link: - sec.albums.galleryLink - .slot('content', language.$('groupInfoPage.viewAlbumGallery.link')), - })), - - html.tag('ul', - stitchArrays({ - albumLink: sec.albums.albumLinks, - otherGroupLinks: sec.albums.otherGroupLinks, - datetimestamp: sec.albums.datetimestamps, - albumColorStyle: sec.albums.albumColorStyles, - }).map(({ - albumLink, - otherGroupLinks, - datetimestamp, - albumColorStyle, - }) => { - const prefix = 'groupInfoPage.albumList.item'; - const parts = [prefix]; - const options = {}; - - options.album = - albumLink.slot('color', false); - - if (datetimestamp) { - parts.push('withYear'); - options.yearAccent = - language.$(prefix, 'yearAccent', { - year: - datetimestamp.slots({style: 'year', tooltip: true}), - }); - } - - if (!empty(otherGroupLinks)) { - parts.push('withOtherGroup'); - options.otherGroupAccent = - html.tag('span', {class: 'other-group-accent'}, - language.$(prefix, 'otherGroupAccent', { - groups: - language.formatConjunctionList( - otherGroupLinks.map(groupLink => - groupLink.slot('color', false))), - })); - } - - return ( - html.tag('li', - albumColorStyle, - language.$(...parts, options))); - })), - ], + relations.description.slot('mode', 'multiline')), + + relations.albumSection, ], leftSidebar: @@ -217,6 +89,5 @@ export default { navLinks: relations.navLinks.content, secondaryNav: relations.secondaryNav ?? null, - }); - }, + })), }; diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js new file mode 100644 index 00000000..8899e98e --- /dev/null +++ b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js @@ -0,0 +1,136 @@ +import {empty} from '#sugar'; +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateAbsoluteDatetimestamp', + 'generateColorStyleAttribute', + 'generateContentHeading', + 'linkAlbum', + 'linkGroupGallery', + 'linkGroup', + ], + + extraDependencies: ['html', 'language'], + + query(group) { + const albums = + group.albums; + + const albumGroups = + albums + .map(album => album.groups); + + const albumOtherCategory = + albumGroups + .map(groups => groups + .map(group => group.category) + .find(category => category !== group.category)); + + const albumOtherGroups = + stitchArrays({ + groups: albumGroups, + category: albumOtherCategory, + }).map(({groups, category}) => + groups + .filter(group => group.category === category)); + + return {albums, albumOtherGroups}; + }, + + relations: (relation, query, group) => ({ + contentHeading: + relation('generateContentHeading'), + + galleryLink: + relation('linkGroupGallery', group), + + albumColorStyles: + query.albums + .map(album => relation('generateColorStyleAttribute', album.color)), + + albumLinks: + query.albums + .map(album => relation('linkAlbum', album)), + + otherGroupLinks: + query.albumOtherGroups + .map(groups => groups + .map(group => relation('linkGroup', group))), + + datetimestamps: + group.albums.map(album => + (album.date + ? relation('generateAbsoluteDatetimestamp', album.date) + : null)), + }), + + generate: (relations, {html, language}) => + language.encapsulate('groupInfoPage', pageCapsule => + language.encapsulate(pageCapsule, 'albumList', listCapsule => + html.tags([ + relations.contentHeading + .slots({ + tag: 'h2', + title: language.$(listCapsule, 'title'), + }), + + html.tag('p', + {[html.onlyIfSiblings]: true}, + + language.encapsulate(pageCapsule, 'viewAlbumGallery', capsule => + language.$(capsule, { + link: + relations.galleryLink + .slot('content', language.$(capsule, 'link')), + }))), + + html.tag('ul', + {[html.onlyIfContent]: true}, + + stitchArrays({ + albumLink: relations.albumLinks, + otherGroupLinks: relations.otherGroupLinks, + datetimestamp: relations.datetimestamps, + albumColorStyle: relations.albumColorStyles, + }).map(({ + albumLink, + otherGroupLinks, + datetimestamp, + albumColorStyle, + }) => + html.tag('li', + albumColorStyle, + + language.encapsulate(listCapsule, 'item', itemCapsule => + language.encapsulate(itemCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.album = + albumLink.slot('color', false); + + if (datetimestamp) { + workingCapsule += '.withYear'; + workingOptions.yearAccent = + language.$(itemCapsule, 'yearAccent', { + year: + datetimestamp.slots({style: 'year', tooltip: true}), + }); + } + + if (!empty(otherGroupLinks)) { + workingCapsule += '.withOtherGroup'; + workingOptions.otherGroupAccent = + html.tag('span', {class: 'other-group-accent'}, + language.$(itemCapsule, 'otherGroupAccent', { + groups: + language.formatConjunctionList( + otherGroupLinks.map(groupLink => + groupLink.slot('color', false))), + })); + } + + return language.$(workingCapsule, workingOptions); + }))))), + ]))), +}; diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js index 69de373b..d52c77b8 100644 --- a/src/content/dependencies/generateGroupSidebarCategoryDetails.js +++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js @@ -46,37 +46,37 @@ export default { }, }, - generate(data, relations, slots, {html, language}) { - return html.tag('details', - data.isCurrentCategory && - {class: 'current', open: true}, - - [ - html.tag('summary', - relations.colorStyle, - - html.tag('span', - language.$('groupSidebar.groupList.category', { - category: - html.tag('span', {class: 'group-name'}, - data.name), - }))), - - html.tag('ul', - stitchArrays(({ - infoLink: relations.groupInfoLinks, - galleryLink: relations.groupGalleryLinks, - })).map(({infoLink, galleryLink}, index) => - html.tag('li', - index === data.currentGroupIndex && - {class: 'current'}, - - language.$('groupSidebar.groupList.item', { - group: - (slots.currentExtra === 'gallery' - ? galleryLink ?? infoLink - : infoLink), - })))), - ]); - }, + generate: (data, relations, slots, {html, language}) => + language.encapsulate('groupSidebar.groupList', capsule => + html.tag('details', + data.isCurrentCategory && + {class: 'current', open: true}, + + [ + html.tag('summary', + relations.colorStyle, + + html.tag('span', + language.$(capsule, 'category', { + category: + html.tag('span', {class: 'group-name'}, + data.name), + }))), + + html.tag('ul', + stitchArrays(({ + infoLink: relations.groupInfoLinks, + galleryLink: relations.groupGalleryLinks, + })).map(({infoLink, galleryLink}, index) => + html.tag('li', + index === data.currentGroupIndex && + {class: 'current'}, + + language.$(capsule, 'item', { + group: + (slots.currentExtra === 'gallery' + ? galleryLink ?? infoLink + : infoLink), + })))), + ])), }; diff --git a/src/content/dependencies/generateListAllAdditionalFilesChunk.js b/src/content/dependencies/generateListAllAdditionalFilesChunk.js index 43a78cb3..659cf4e5 100644 --- a/src/content/dependencies/generateListAllAdditionalFilesChunk.js +++ b/src/content/dependencies/generateListAllAdditionalFilesChunk.js @@ -42,49 +42,50 @@ export default { additionalFileLinks, additionalFileFiles, }) => - (additionalFileLinks.length === 1 - ? html.tag('li', - additionalFileLinks[0].slots({ - content: - language.$('listingPage', slots.stringsKey, 'file', { - title: additionalFileTitle, - }), - })) + language.encapsulate('listingPage', slots.stringsKey, 'file', capsule => + (additionalFileLinks.length === 1 + ? html.tag('li', + additionalFileLinks[0].slots({ + content: + language.$(capsule, { + title: additionalFileTitle, + }), + })) - : additionalFileLinks.length === 0 - ? html.tag('li', - language.$('listingPage', slots.stringsKey, 'file.withNoFiles', { - title: additionalFileTitle, - })) + : additionalFileLinks.length === 0 + ? html.tag('li', + language.$(capsule, 'withNoFiles', { + title: additionalFileTitle, + })) - : html.tag('li', {class: 'has-details'}, - html.tag('details', [ - html.tag('summary', - html.tag('span', - language.$('listingPage', slots.stringsKey, 'file.withMultipleFiles', { - title: - html.tag('span', {class: 'group-name'}, - additionalFileTitle), + : html.tag('li', {class: 'has-details'}, + html.tag('details', [ + html.tag('summary', + html.tag('span', + language.$(capsule, 'withMultipleFiles', { + title: + html.tag('span', {class: 'group-name'}, + additionalFileTitle), - files: - language.countAdditionalFiles( - additionalFileLinks.length, - {unit: true}), - }))), + files: + language.countAdditionalFiles( + additionalFileLinks.length, + {unit: true}), + }))), - html.tag('ul', - stitchArrays({ - additionalFileLink: additionalFileLinks, - additionalFileFile: additionalFileFiles, - }).map(({additionalFileLink, additionalFileFile}) => - html.tag('li', - additionalFileLink.slots({ - content: - language.$('listingPage', slots.stringsKey, 'file', { - title: additionalFileFile, - }), - })))), - ])))))), + html.tag('ul', + stitchArrays({ + additionalFileLink: additionalFileLinks, + additionalFileFile: additionalFileFiles, + }).map(({additionalFileLink, additionalFileFile}) => + html.tag('li', + additionalFileLink.slots({ + content: + language.$(capsule, { + title: additionalFileFile, + }), + })))), + ]))))))), ]); }, }; diff --git a/src/content/dependencies/generateListingPage.js b/src/content/dependencies/generateListingPage.js index 23377afb..5f9a99a9 100644 --- a/src/content/dependencies/generateListingPage.js +++ b/src/content/dependencies/generateListingPage.js @@ -34,13 +34,15 @@ export default { relations.sameTargetListingLinks = listing.target.listings .map(listing => relation('linkListing', listing)); + } else { + relations.sameTargetListingLinks = []; } - if (!empty(listing.seeAlso)) { - relations.seeAlsoLinks = - listing.seeAlso - .map(listing => relation('linkListing', listing)); - } + relations.seeAlsoLinks = + (!empty(listing.seeAlso) + ? listing.seeAlso + .map(listing => relation('linkListing', listing)) + : []); return relations; }, @@ -167,33 +169,37 @@ export default { headingMode: 'sticky', mainContent: [ - relations.sameTargetListingLinks && - html.tag('p', - language.$('listingPage.listingsFor', { - target: - language.$('listingPage.target', data.targetStringsKey), - - listings: - language.formatUnitList( - stitchArrays({ - link: relations.sameTargetListingLinks, - stringsKey: data.sameTargetListingStringsKeys, - }).map(({link, stringsKey}, index) => - html.tag('span', - index === data.sameTargetListingsCurrentIndex && - {class: 'current'}, - - link.slots({ - attributes: {class: 'nowrap'}, - content: language.$('listingPage', stringsKey, 'title.short'), - })))), - })), - - relations.seeAlsoLinks && - html.tag('p', - language.$('listingPage.seeAlso', { - listings: language.formatUnitList(relations.seeAlsoLinks), - })), + html.tag('p', + {[html.onlyIfContent]: true}, + language.$('listingPage.listingsFor', { + [language.onlyIfOptions]: ['listings'], + + target: + language.$('listingPage.target', data.targetStringsKey), + + listings: + language.formatUnitList( + stitchArrays({ + link: relations.sameTargetListingLinks, + stringsKey: data.sameTargetListingStringsKeys, + }).map(({link, stringsKey}, index) => + html.tag('span', + index === data.sameTargetListingsCurrentIndex && + {class: 'current'}, + + link.slots({ + attributes: {class: 'nowrap'}, + content: language.$('listingPage', stringsKey, 'title.short'), + })))), + })), + + html.tag('p', + {[html.onlyIfContent]: true}, + language.$('listingPage.seeAlso', { + [language.onlyIfOptions]: ['listings'], + listings: + language.formatUnitList(relations.seeAlsoLinks), + })), slots.content, @@ -243,7 +249,7 @@ export default { .clone() .slots({ tag: 'dt', - id, + attributes: [id && {id}], title: formatListingString({ diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js index bcba7194..2c382cfa 100644 --- a/src/content/dependencies/generateNewsEntryPage.js +++ b/src/content/dependencies/generateNewsEntryPage.js @@ -91,41 +91,41 @@ export default { }; }, - generate(data, relations, {html, language}) { - return relations.layout.slots({ - title: - language.$('newsEntryPage.title', { - entry: data.name, - }), - - headingMode: 'sticky', - - mainClasses: ['long-content'], - mainContent: [ - html.tag('p', - language.$('newsEntryPage.published', { - date: language.formatDate(data.date), - })), - - relations.content, - relations.readAnotherLinks, - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {html: relations.newsIndexLink}, - { - auto: 'current', - accent: - (relations.previousNextLinks - ? `(${language.formatUnitList(relations.previousNextLinks.slots({ - previousLink: relations.previousEntryNavLink ?? null, - nextLink: relations.nextEntryNavLink ?? null, - }).content)})` - : null), - }, - ], - }); - }, + generate: (data, relations, {html, language}) => + language.encapsulate('newsEntryPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + entry: data.name, + }), + + headingMode: 'sticky', + + mainClasses: ['long-content'], + mainContent: [ + html.tag('p', + language.$(pageCapsule, 'published', { + date: language.formatDate(data.date), + })), + + relations.content, + relations.readAnotherLinks, + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {html: relations.newsIndexLink}, + { + auto: 'current', + accent: + (relations.previousNextLinks + ? `(${language.formatUnitList(relations.previousNextLinks.slots({ + previousLink: relations.previousEntryNavLink ?? null, + nextLink: relations.nextEntryNavLink ?? null, + }).content)})` + : null), + }, + ], + })), }; diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js index 539af804..02964ce8 100644 --- a/src/content/dependencies/generateNewsIndexPage.js +++ b/src/content/dependencies/generateNewsIndexPage.js @@ -57,37 +57,38 @@ export default { }; }, - generate(data, relations, {html, language}) { - return relations.layout.slots({ - title: language.$('newsIndex.title'), - headingMode: 'sticky', - - mainClasses: ['long-content', 'news-index'], - mainContent: - stitchArrays({ - entryLink: relations.entryLinks, - viewRestLink: relations.viewRestLinks, - content: relations.entryContents, - date: data.entryDates, - directory: data.entryDirectories, - }).map(({entryLink, viewRestLink, content, date, directory}) => - html.tag('article', {id: directory}, [ - html.tag('h2', [ - html.tag('time', language.formatDate(date)), - entryLink, - ]), - - content, - - viewRestLink - ?.slot('content', language.$('newsIndex.entry.viewRest')), - ])), - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {auto: 'current'}, - ], - }); - }, + generate: (data, relations, {html, language}) => + language.encapsulate('newsIndex', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title'), + headingMode: 'sticky', + + mainClasses: ['long-content', 'news-index'], + mainContent: + stitchArrays({ + entryLink: relations.entryLinks, + viewRestLink: relations.viewRestLinks, + content: relations.entryContents, + date: data.entryDates, + directory: data.entryDirectories, + }).map(({entryLink, viewRestLink, content, date, directory}) => + language.encapsulate(pageCapsule, 'entry', entryCapsule => + html.tag('article', {id: directory}, [ + html.tag('h2', [ + html.tag('time', language.formatDate(date)), + entryLink, + ]), + + content, + + viewRestLink + ?.slot('content', language.$(entryCapsule, 'viewRest')), + ]))), + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {auto: 'current'}, + ], + })), }; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 51f9057b..7e9e49a0 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -5,12 +5,13 @@ export default { contentDependencies: [ 'generateColorStyleRules', 'generateFooterLocalizationLinks', + 'generatePageSidebar', + 'generateSearchSidebarBox', 'generateStickyHeadingContainer', 'transformContent', ], extraDependencies: [ - 'cachebust', 'getColors', 'html', 'language', @@ -21,6 +22,7 @@ export default { sprawl({wikiInfo}) { return { + enableSearch: wikiInfo.enableSearch, footerContent: wikiInfo.footerContent, wikiColor: wikiInfo.color, wikiName: wikiInfo.nameShort, @@ -43,6 +45,14 @@ export default { relations.stickyHeadingContainer = relation('generateStickyHeadingContainer'); + relations.sidebar = + relation('generatePageSidebar'); + + if (sprawl.enableSearch) { + relations.searchBox = + relation('generateSearchSidebarBox'); + } + if (sprawl.footerContent) { relations.defaultFooterContent = relation('transformContent', sprawl.footerContent); @@ -65,6 +75,11 @@ export default { default: true, }, + showSearch: { + type: 'boolean', + default: true, + }, + additionalNames: { type: 'html', mutable: false, @@ -209,7 +224,6 @@ export default { }, generate(data, relations, slots, { - cachebust, getColors, html, language, @@ -374,30 +388,66 @@ export default { slots.navContent), ]); - const getSidebar = (side, id) => - (html.isBlank(slots[side]) - ? html.blank() - : slots[side].slots({ - attributes: - slots[side] - .getSlotValue('attributes') - .with({id}), - })); + const getSidebar = (side, id, needed) => { + const sidebar = + (html.isBlank(slots[side]) + ? (needed + ? relations.sidebar.clone() + : html.blank()) + : slots[side]); + + if (html.isBlank(sidebar) && !needed) { + return sidebar; + } + + return sidebar.slots({ + attributes: + sidebar + .getSlotValue('attributes') + .with({id}), + }); + } + + const willShowSearch = + slots.showSearch && relations.searchBox; - const leftSidebar = getSidebar('leftSidebar', 'sidebar-left'); - const rightSidebar = getSidebar('rightSidebar', 'sidebar-right'); + let showingSidebarLeft; + let showingSidebarRight; + + const leftSidebar = getSidebar('leftSidebar', 'sidebar-left', willShowSearch); + const rightSidebar = getSidebar('rightSidebar', 'sidebar-right', false); + + if (willShowSearch) { + if (html.isBlank(leftSidebar)) { + leftSidebar.setSlot('initiallyHidden', true); + showingSidebarLeft = false; + } + + leftSidebar.setSlot( + 'boxes', + html.tags([ + relations.searchBox, + leftSidebar.getSlotValue('boxes'), + ])); + } const hasSidebarLeft = !html.isBlank(html.resolve(leftSidebar)); const hasSidebarRight = !html.isBlank(html.resolve(rightSidebar)); + showingSidebarLeft ??= hasSidebarLeft; + showingSidebarRight ??= hasSidebarRight; + const processSkippers = skipperList => skipperList .filter(({condition, id}) => (condition === undefined ? hasID(id) : condition)) + .map(({id, string}) => html.tag('span', {class: 'skipper'}, + {'data-for': id}, + html.tag('a', {href: `#${id}`}, language.$('misc.skippers', string)))); @@ -456,41 +506,43 @@ export default { html.tag('img', {id: 'image-overlay-image'}), html.tag('img', {id: 'image-overlay-image-thumb'}), ]), - html.tag('div', {id: 'image-overlay-action-container'}, [ - html.tag('div', {id: 'image-overlay-action-content-without-size'}, - language.$('releaseInfo.viewOriginalFile', { - link: html.tag('a', {class: 'image-overlay-view-original'}, - language.$('releaseInfo.viewOriginalFile.link')), - })), - - html.tag('div', {id: 'image-overlay-action-content-with-size'}, [ - language.$('releaseInfo.viewOriginalFile.withSize', { - link: - html.tag('a', {class: 'image-overlay-view-original'}, - language.$('releaseInfo.viewOriginalFile.link')), - - size: - html.tag('span', - {[html.joinChildren]: ''}, - [ - html.tag('span', {id: 'image-overlay-file-size-kilobytes'}, - language.$('count.fileSize.kilobytes', { - kilobytes: - html.tag('span', {class: 'image-overlay-file-size-count'}), - })), - - html.tag('span', {id: 'image-overlay-file-size-megabytes'}, - language.$('count.fileSize.megabytes', { - megabytes: - html.tag('span', {class: 'image-overlay-file-size-count'}), - })), - ]), - }), - html.tag('span', {id: 'image-overlay-file-size-warning'}, - language.$('releaseInfo.viewOriginalFile.sizeWarning')), - ]), - ]), + html.tag('div', {id: 'image-overlay-action-container'}, + language.encapsulate('releaseInfo.viewOriginalFile', capsule => [ + html.tag('div', {id: 'image-overlay-action-content-without-size'}, + language.$(capsule, { + link: html.tag('a', {class: 'image-overlay-view-original'}, + language.$(capsule, 'link')), + })), + + html.tag('div', {id: 'image-overlay-action-content-with-size'}, [ + language.$(capsule, 'withSize', { + link: + html.tag('a', {class: 'image-overlay-view-original'}, + language.$(capsule, 'link')), + + size: + html.tag('span', + {[html.joinChildren]: ''}, + [ + html.tag('span', {id: 'image-overlay-file-size-kilobytes'}, + language.$('count.fileSize.kilobytes', { + kilobytes: + html.tag('span', {class: 'image-overlay-file-size-count'}), + })), + + html.tag('span', {id: 'image-overlay-file-size-megabytes'}, + language.$('count.fileSize.megabytes', { + megabytes: + html.tag('span', {class: 'image-overlay-file-size-count'}), + })), + ]), + }), + + html.tag('span', {id: 'image-overlay-file-size-warning'}, + language.$(capsule, 'sizeWarning')), + ]), + ])), ])); const layoutHTML = [ @@ -528,6 +580,8 @@ export default { {'data-rebase-localized': to('localized.root')}, {'data-rebase-shared': to('shared.root')}, {'data-rebase-media': to('media.root')}, + {'data-rebase-thumb': to('thumb.root')}, + {'data-rebase-lib': to('staticLib.root')}, {'data-rebase-data': to('data.root')}, [ @@ -598,7 +652,7 @@ export default { html.tag('link', { rel: 'stylesheet', - href: to('shared.staticFile', 'site7.css', cachebust), + href: to('staticCSS.path', 'site.css'), }), html.tag('style', [ @@ -608,25 +662,29 @@ export default { ]), html.tag('script', { - src: to('shared.staticFile', 'lazy-loading.js', cachebust), + src: to('staticLib.path', 'chroma-js/chroma.min.js'), + }), + + html.tag('script', { + blocking: 'render', + src: to('staticJS.path', 'lazy-loading.js'), + }), + + html.tag('script', { + blocking: 'render', + type: 'module', + src: to('staticJS.path', 'client.js'), }), ]), html.tag('body', [ html.tag('div', {id: 'page-container'}, - (hasSidebarLeft || hasSidebarRight - ? {class: 'has-one-sidebar'} - : {class: 'has-zero-sidebars'}), + showingSidebarLeft && + {class: 'showing-sidebar-left'}, - hasSidebarLeft && hasSidebarRight && - {class: 'has-two-sidebars'}, - - hasSidebarLeft && - {class: 'has-sidebar-left'}, - - hasSidebarRight && - {class: 'has-sidebar-right'}, + showingSidebarRight && + {class: 'showing-sidebar-right'}, [ skippersHTML, @@ -635,11 +693,6 @@ export default { // infoCardHTML, imageOverlayHTML, - - html.tag('script', { - type: 'module', - src: to('shared.staticFile', 'client4.js', cachebust), - }), ]), ]) ]).toString(); diff --git a/src/content/dependencies/generatePageSidebar.js b/src/content/dependencies/generatePageSidebar.js index 43015aa3..d3b55580 100644 --- a/src/content/dependencies/generatePageSidebar.js +++ b/src/content/dependencies/generatePageSidebar.js @@ -19,14 +19,13 @@ export default { // Sticky mode controls which sidebar sections, if any, follow the // scroll position, "sticking" to the top of the browser viewport. // - // 'last' - last or only sidebar box is sticky // 'column' - entire column, incl. multiple boxes from top, is sticky // 'static' - sidebar not sticky at all, stays at top of page // // Note: This doesn't affect the content of any sidebar section, only // the whole section's containing box (or the sidebar column as a whole). stickyMode: { - validate: v => v.is('last', 'column', 'static'), + validate: v => v.is('column', 'static'), default: 'static', }, @@ -37,6 +36,16 @@ export default { type: 'boolean', default: false, }, + + // Provide to include all the HTML for the sidebar in place as usual, + // but start it out totally invisible. This is mainly so client-side + // JavaScript can show the sidebar if it needs to (and has a target + // to slot its own content into). If there are no boxes and this + // option *isn't* provided, then the sidebar will just be blank. + initiallyHidden: { + type: 'boolean', + default: false, + }, }, generate(slots, {html}) { @@ -68,7 +77,11 @@ export default { attributes.add('class', 'all-boxes-collapsible'); } - if (html.isBlank(slots.boxes)) { + if (slots.initiallyHidden) { + attributes.add('class', 'initially-hidden'); + } + + if (html.isBlank(slots.boxes) && !slots.initiallyHidden) { return html.blank(); } else { return html.tag('div', attributes, slots.boxes); diff --git a/src/content/dependencies/generatePageSidebarBox.js b/src/content/dependencies/generatePageSidebarBox.js index e11efc3f..26b30494 100644 --- a/src/content/dependencies/generatePageSidebarBox.js +++ b/src/content/dependencies/generatePageSidebarBox.js @@ -20,6 +20,8 @@ export default { generate: (slots, {html}) => html.tag('div', {class: 'sidebar'}, + {[html.onlyIfContent]: true}, + slots.collapsible && {class: 'collapsible'}, diff --git a/src/content/dependencies/generateQuickDescription.js b/src/content/dependencies/generateQuickDescription.js new file mode 100644 index 00000000..4c7c944a --- /dev/null +++ b/src/content/dependencies/generateQuickDescription.js @@ -0,0 +1,134 @@ +export default { + contentDependencies: ['transformContent'], + extraDependencies: ['html', 'language'], + + query: (thing) => ({ + hasDescription: + !!thing.description, + + hasLongerDescription: + thing.description && + thing.descriptionShort && + thing.descriptionShort !== thing.description, + }), + + relations: (relation, query, thing) => ({ + description: + (query.hasLongerDescription || !thing.description + ? null + : relation('transformContent', thing.description)), + + descriptionShort: + (query.hasLongerDescription + ? relation('transformContent', thing.descriptionShort) + : null), + + descriptionLong: + (query.hasLongerDescription + ? relation('transformContent', thing.description) + : null), + }), + + data: (query) => ({ + hasDescription: query.hasDescription, + hasLongerDescription: query.hasLongerDescription, + }), + + slots: { + extraReadingLinks: { + validate: v => v.sparseArrayOf(v.isHTML), + }, + }, + + generate(data, relations, slots, {html, language}) { + const prefix = 'misc.quickDescription'; + + const actionsWithoutLongerDescription = + (data.hasLongerDescription + ? null + : slots.extraReadingLinks + ? language.$(prefix, 'readMore', { + links: + language.formatDisjunctionList(slots.extraReadingLinks), + }) + : null); + + const wrapExpandCollapseLink = (expandCollapse, content) => + html.tag('a', {class: `${expandCollapse}-link`}, + {href: '#'}, + content); + + const actionsWhenCollapsed = + (data.hasLongerDescription && slots.extraReadingLinks + ? language.$(prefix, 'expandDescription.orReadMore', { + links: + language.formatDisjunctionList(slots.extraReadingLinks), + expand: + wrapExpandCollapseLink('expand', + language.$(prefix, 'expandDescription.orReadMore.expand')), + }) + : data.hasLongerDescription + ? language.$(prefix, 'expandDescription', { + expand: + wrapExpandCollapseLink('expand', + language.$(prefix, 'expandDescription.expand')), + }) + : null); + + const actionsWhenExpanded = + (data.hasLongerDescription && slots.extraReadingLinks + ? language.$(prefix, 'collapseDescription.orReadMore', { + links: + language.formatDisjunctionList(slots.extraReadingLinks), + collapse: + wrapExpandCollapseLink('collapse', + language.$(prefix, 'collapseDescription.orReadMore.collapse')), + }) + : data.hasLongerDescription + ? language.$(prefix, 'collapseDescription', { + collapse: + wrapExpandCollapseLink('collapse', + language.$(prefix, 'collapseDescription.collapse')), + }) + : null); + + const wrapActions = (attributes, children) => + html.tag('p', {class: 'quick-description-actions'}, + {[html.onlyIfContent]: true}, + attributes, + + children); + + const wrapContent = (attributes, content) => + html.tag('div', {class: 'description-content'}, + {[html.onlyIfContent]: true}, + attributes, + + content?.slot('mode', 'multiline')); + + return ( + html.tag('div', {class: 'quick-description'}, + {[html.onlyIfContent]: true}, + + data.hasLongerDescription && + {class: 'collapsed'}, + + !data.hasLongerDescription && + !slots.extraReadingLinks && + {class: 'has-content-only'}, + + !data.hasDescription && + slots.extraReadingLinks && + {class: 'has-external-links-only'}, + + [ + wrapContent(null, relations.description), + wrapContent({class: 'short'}, relations.descriptionShort), + wrapContent({class: 'long'}, relations.descriptionLong), + + wrapActions(null, actionsWithoutLongerDescription), + wrapActions({class: 'when-collapsed'}, actionsWhenCollapsed), + wrapActions({class: 'when-expanded'}, actionsWhenExpanded), + ])); + }, +}; diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js index 2e6c4709..3e96ed44 100644 --- a/src/content/dependencies/generateReleaseInfoContributionsLine.js +++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js @@ -17,10 +17,12 @@ export default { }, slots: { - stringKey: {type: 'string'}, - showContribution: {type: 'boolean', default: true}, - showIcons: {type: 'boolean', default: true}, + showExternalLinks: {type: 'boolean', default: true}, + showChronology: {type: 'boolean', default: true}, + + stringKey: {type: 'string'}, + chronologyKind: {type: 'string'}, }, generate(relations, slots, {html, language}) { @@ -34,8 +36,9 @@ export default { relations.contributionLinks.map(link => link.slots({ showContribution: slots.showContribution, - showIcons: slots.showIcons, - iconMode: 'tooltip', + showExternalLinks: slots.showExternalLinks, + showChronology: slots.showChronology, + chronologyKind: slots.chronologyKind, }))), }); }, diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js new file mode 100644 index 00000000..188a678f --- /dev/null +++ b/src/content/dependencies/generateSearchSidebarBox.js @@ -0,0 +1,62 @@ +export default { + contentDependencies: ['generatePageSidebarBox'], + extraDependencies: ['html', 'language'], + + relations: (relation) => ({ + sidebarBox: + relation('generatePageSidebarBox'), + }), + + generate: (relations, {html, language}) => + language.encapsulate('misc.search', capsule => + relations.sidebarBox.slots({ + attributes: {class: 'wiki-search-sidebar-box'}, + collapsible: false, + + content: [ + html.tag('label', {class: 'wiki-search-label'}, + html.tag('input', {class: 'wiki-search-input'}, + {type: 'search'}, + + { + placeholder: + language.$(capsule, 'placeholder').toString(), + })), + + html.tag('template', {class: 'wiki-search-preparing-string'}, + language.$(capsule, 'preparing')), + + html.tag('template', {class: 'wiki-search-loading-data-string'}, + language.$(capsule, 'loadingData')), + + html.tag('template', {class: 'wiki-search-searching-string'}, + language.$(capsule, 'searching')), + + html.tag('template', {class: 'wiki-search-failed-string'}, + language.$(capsule, 'failed')), + + html.tag('template', {class: 'wiki-search-no-results-string'}, + language.$(capsule, 'noResults')), + + html.tag('template', {class: 'wiki-search-current-result-string'}, + language.$(capsule, 'currentResult')), + + html.tag('template', {class: 'wiki-search-end-search-string'}, + language.$(capsule, 'endSearch')), + + language.encapsulate(capsule, 'resultKind', capsule => [ + html.tag('template', {class: 'wiki-search-album-result-kind-string'}, + language.$(capsule, 'album')), + + html.tag('template', {class: 'wiki-search-artist-result-kind-string'}, + language.$(capsule, 'artist')), + + html.tag('template', {class: 'wiki-search-group-result-kind-string'}, + language.$(capsule, 'group')), + + html.tag('template', {class: 'wiki-search-tag-result-kind-string'}, + language.$(capsule, 'artTag')), + ]), + ], + })), +}; diff --git a/src/content/dependencies/generateStickyHeadingContainer.js b/src/content/dependencies/generateStickyHeadingContainer.js index 9becfb26..7f271715 100644 --- a/src/content/dependencies/generateStickyHeadingContainer.js +++ b/src/content/dependencies/generateStickyHeadingContainer.js @@ -22,10 +22,17 @@ export default { html.tag('div', {class: 'content-sticky-heading-row'}, [ html.tag('h1', slots.title), - !html.isBlank(slots.cover) && - html.tag('div', {class: 'content-sticky-heading-cover-container'}, - html.tag('div', {class: 'content-sticky-heading-cover'}, - slots.cover.slot('mode', 'thumbnail'))), + html.tag('div', {class: 'content-sticky-heading-cover-container'}, + {[html.onlyIfContent]: true}, + + html.tag('div', {class: 'content-sticky-heading-cover'}, + {[html.onlyIfContent]: true}, + + // TODO: We shouldn't need to do an isBlank check here, + // but a live blank value doesn't have a slot functions, so. + (html.isBlank(slots.cover) + ? html.blank() + : slots.cover.slot('mode', 'thumbnail')))), ]), html.tag('div', {class: 'content-sticky-subheading-row'}, diff --git a/src/content/dependencies/generateTooltip.js b/src/content/dependencies/generateTooltip.js index 81f74aec..8314d33c 100644 --- a/src/content/dependencies/generateTooltip.js +++ b/src/content/dependencies/generateTooltip.js @@ -21,10 +21,13 @@ export default { generate: (slots, {html}) => html.tag('span', {class: 'tooltip'}, {[html.noEdgeWhitespace]: true}, + {[html.onlyIfContent]: true}, slots.attributes, html.tag('span', {class: 'tooltip-content'}, {[html.noEdgeWhitespace]: true}, + {[html.onlyIfContent]: true}, slots.contentAttributes, + slots.content)), }; diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index f5324519..64ed0cb4 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -1,333 +1,142 @@ -import {sortAlbumsTracksChronologically, sortFlashesChronologically} - from '#sort'; -import {empty, stitchArrays} from '#sugar'; - -import getChronologyRelations from '../util/getChronologyRelations.js'; - export default { contentDependencies: [ - 'generateAbsoluteDatetimestamp', 'generateAlbumAdditionalFilesList', 'generateAlbumNavAccent', 'generateAlbumSecondaryNav', 'generateAlbumSidebar', 'generateAlbumStyleRules', - 'generateChronologyLinks', - 'generateColorStyleAttribute', 'generateCommentarySection', 'generateContentHeading', 'generateContributionList', 'generatePageLayout', - 'generateRelativeDatetimestamp', 'generateTrackAdditionalNamesBox', 'generateTrackCoverArtwork', + 'generateTrackInfoPageFeaturedByFlashesList', + 'generateTrackInfoPageOtherReleasesList', 'generateTrackList', 'generateTrackListDividedByGroups', 'generateTrackReleaseInfo', 'generateTrackSocialEmbed', 'linkAlbum', - 'linkArtist', - 'linkFlash', 'linkTrack', 'transformContent', ], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({wikiInfo}) { - return { - divideTrackListsByGroups: wikiInfo.divideTrackListsByGroups, - enableFlashesAndGames: wikiInfo.enableFlashesAndGames, - }; - }, - - relations(relation, sprawl, track) { - const relations = {}; - const sections = relations.sections = {}; - const {album} = track; - - relations.layout = - relation('generatePageLayout'); - - relations.albumStyleRules = - relation('generateAlbumStyleRules', track.album, track); - - relations.socialEmbed = - relation('generateTrackSocialEmbed', track); - - relations.artistChronologyContributions = - getChronologyRelations(track, { - contributions: [ - ...track.artistContribs ?? [], - ...track.contributorContribs ?? [], - ], - - linkArtist: artist => relation('linkArtist', artist), - linkThing: track => relation('linkTrack', track), - - getThings(artist) { - const getDate = thing => thing.date; - - const things = [ - ...artist.tracksAsArtist, - ...artist.tracksAsContributor, - ].filter(getDate); - - return sortAlbumsTracksChronologically(things, {getDate}); - }, - }); - - relations.coverArtistChronologyContributions = - getChronologyRelations(track, { - contributions: track.coverArtistContribs ?? [], - - linkArtist: artist => relation('linkArtist', artist), - - linkThing: trackOrAlbum => - (trackOrAlbum.album - ? relation('linkTrack', trackOrAlbum) - : relation('linkAlbum', trackOrAlbum)), - - getThings(artist) { - const getDate = thing => thing.coverArtDate ?? thing.date; - - const things = [ - ...artist.albumsAsCoverArtist, - ...artist.tracksAsCoverArtist, - ].filter(getDate); - - return sortAlbumsTracksChronologically(things, {getDate}); - }, - }), - - relations.albumLink = - relation('linkAlbum', track.album); - - relations.trackLink = - relation('linkTrack', track); - - relations.albumNavAccent = - relation('generateAlbumNavAccent', track.album, track); - - relations.chronologyLinks = - relation('generateChronologyLinks'); - - relations.secondaryNav = - relation('generateAlbumSecondaryNav', track.album); - - relations.sidebar = - relation('generateAlbumSidebar', track.album, track); - - const additionalFilesSection = additionalFiles => ({ - heading: relation('generateContentHeading'), - list: relation('generateAlbumAdditionalFilesList', album, additionalFiles), - }); + sprawl: ({wikiInfo}) => ({ + divideTrackListsByGroups: + wikiInfo.divideTrackListsByGroups, + }), - // This'll take care of itself being blank if there's nothing to show here. - relations.additionalNamesBox = - relation('generateTrackAdditionalNamesBox', track); + relations: (relation, sprawl, track) => ({ + layout: + relation('generatePageLayout'), - if (track.hasUniqueCoverArt || album.hasCoverArt) { - relations.cover = - relation('generateTrackCoverArtwork', track); - } + albumStyleRules: + relation('generateAlbumStyleRules', track.album, track), - // Section: Release info + socialEmbed: + relation('generateTrackSocialEmbed', track), - relations.releaseInfo = - relation('generateTrackReleaseInfo', track); + albumLink: + relation('linkAlbum', track.album), - // Section: Other releases + trackLink: + relation('linkTrack', track), - if (!empty(track.otherReleases)) { - const otherReleases = sections.otherReleases = {}; + albumNavAccent: + relation('generateAlbumNavAccent', track.album, track), - otherReleases.heading = - relation('generateContentHeading'); + secondaryNav: + relation('generateAlbumSecondaryNav', track.album), - otherReleases.colorStyles = - track.otherReleases - .map(track => relation('generateColorStyleAttribute', track.color)); + sidebar: + relation('generateAlbumSidebar', track.album, track), - otherReleases.trackLinks = - track.otherReleases - .map(track => relation('linkTrack', track)); + additionalNamesBox: + relation('generateTrackAdditionalNamesBox', track), - otherReleases.albumLinks = - track.otherReleases - .map(track => relation('linkAlbum', track.album)); + cover: + (track.hasUniqueCoverArt || track.album.hasCoverArt + ? relation('generateTrackCoverArtwork', track) + : null), - otherReleases.datetimestamps = - track.otherReleases.map(track2 => - (track2.date - ? (track.date - ? relation('generateRelativeDatetimestamp', - track2.date, - track.date) - : relation('generateAbsoluteDatetimestamp', - track2.date)) - : null)); + contentHeading: + relation('generateContentHeading'), - otherReleases.items = - track.otherReleases.map(track => ({ - trackLink: relation('linkTrack', track), - albumLink: relation('linkAlbum', track.album), - })); - } + releaseInfo: + relation('generateTrackReleaseInfo', track), - // Section: Contributors + otherReleasesList: + relation('generateTrackInfoPageOtherReleasesList', track), - if (!empty(track.contributorContribs)) { - const contributors = sections.contributors = {}; + contributorContributionList: + relation('generateContributionList', track.contributorContribs), - contributors.heading = - relation('generateContentHeading'); + referencedTracksList: + relation('generateTrackList', track.referencedTracks), - contributors.list = - relation('generateContributionList', track.contributorContribs); - } + sampledTracksList: + relation('generateTrackList', track.sampledTracks), - // Section: Referenced tracks + referencedByTracksList: + relation('generateTrackListDividedByGroups', + track.referencedByTracks, + sprawl.divideTrackListsByGroups), - if (!empty(track.referencedTracks)) { - const references = sections.references = {}; + sampledByTracksList: + relation('generateTrackListDividedByGroups', + track.sampledByTracks, + sprawl.divideTrackListsByGroups), - references.heading = - relation('generateContentHeading'); + flashesThatFeatureList: + relation('generateTrackInfoPageFeaturedByFlashesList', track), - references.list = - relation('generateTrackList', track.referencedTracks); - } + lyrics: + relation('transformContent', track.lyrics), - // Section: Sampled tracks + sheetMusicFilesList: + relation('generateAlbumAdditionalFilesList', + track.album, + track.sheetMusicFiles), - if (!empty(track.sampledTracks)) { - const samples = sections.samples = {}; + midiProjectFilesList: + relation('generateAlbumAdditionalFilesList', + track.album, + track.midiProjectFiles), - samples.heading = - relation('generateContentHeading'); + additionalFilesList: + relation('generateAlbumAdditionalFilesList', + track.album, + track.additionalFiles), - samples.list = - relation('generateTrackList', track.sampledTracks); - } + artistCommentarySection: + relation('generateCommentarySection', track.commentary), + }), - // Section: Tracks that reference + data: (sprawl, track) => ({ + name: + track.name, - if (!empty(track.referencedByTracks)) { - const referencedBy = sections.referencedBy = {}; + color: + track.color, - referencedBy.heading = - relation('generateContentHeading'); + hasTrackNumbers: + track.album.hasTrackNumbers, - referencedBy.list = - relation('generateTrackListDividedByGroups', - track.referencedByTracks, - sprawl.divideTrackListsByGroups); - } - - // Section: Tracks that sample - - if (!empty(track.sampledByTracks)) { - const sampledBy = sections.sampledBy = {}; - - sampledBy.heading = - relation('generateContentHeading'); - - sampledBy.list = - relation('generateTrackListDividedByGroups', - track.sampledByTracks, - sprawl.divideTrackListsByGroups); - } - - // Section: Flashes that feature - - if (sprawl.enableFlashesAndGames) { - const sortedFeatures = - sortFlashesChronologically( - [track, ...track.otherReleases].flatMap(track => - track.featuredInFlashes.map(flash => ({ - // These aren't going to be exposed directly, they're processed - // into the appropriate relations after this sort. - flash, track, - - // These properties are only used for the sort. - act: flash.act, - date: flash.date, - })))); - - if (!empty(sortedFeatures)) { - const flashesThatFeature = sections.flashesThatFeature = {}; - - flashesThatFeature.heading = - relation('generateContentHeading'); - - flashesThatFeature.entries = - sortedFeatures.map(({flash, track: directlyFeaturedTrack}) => - (directlyFeaturedTrack === track - ? { - flashLink: relation('linkFlash', flash), - } - : { - flashLink: relation('linkFlash', flash), - trackLink: relation('linkTrack', directlyFeaturedTrack), - })); - } - } - - // Section: Lyrics - - if (track.lyrics) { - const lyrics = sections.lyrics = {}; - - lyrics.heading = - relation('generateContentHeading'); - - lyrics.content = - relation('transformContent', track.lyrics); - } - - // Sections: Sheet music files, MIDI/proejct files, additional files - - if (!empty(track.sheetMusicFiles)) { - sections.sheetMusicFiles = additionalFilesSection(track.sheetMusicFiles); - } - - if (!empty(track.midiProjectFiles)) { - sections.midiProjectFiles = additionalFilesSection(track.midiProjectFiles); - } - - if (!empty(track.additionalFiles)) { - sections.additionalFiles = additionalFilesSection(track.additionalFiles); - } - - // Section: Artist commentary - - if (track.commentary) { - sections.artistCommentary = - relation('generateCommentarySection', track.commentary); - } - - return relations; - }, - - data(sprawl, track) { - return { - name: track.name, - color: track.color, + trackNumber: + track.album.tracks.indexOf(track) + 1, + }), - hasTrackNumbers: track.album.hasTrackNumbers, - trackNumber: track.album.tracks.indexOf(track) + 1, - - numAdditionalFiles: track.additionalFiles.length, - }; - }, - - generate(data, relations, {html, language}) { - const {sections: sec} = relations; + generate: (data, relations, {html, language}) => + language.encapsulate('trackPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + track: data.name, + }), - return relations.layout - .slots({ - title: language.$('trackPage.title', {track: data.name}), headingMode: 'sticky', additionalNames: relations.additionalNamesBox, @@ -349,227 +158,232 @@ export default { {[html.onlyIfContent]: true}, {[html.joinChildren]: html.tag('br')}, - [ - sec.sheetMusicFiles && - language.$('releaseInfo.sheetMusicFiles.shortcut', { - link: html.tag('a', - {href: '#sheet-music-files'}, - language.$('releaseInfo.sheetMusicFiles.shortcut.link')), - }), + language.encapsulate('releaseInfo', capsule => [ + !html.isBlank(relations.sheetMusicFilesList) && + language.encapsulate(capsule, 'sheetMusicFiles.shortcut', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#sheet-music-files'}, + language.$(capsule, 'link')), + })), - sec.midiProjectFiles && - language.$('releaseInfo.midiProjectFiles.shortcut', { - link: html.tag('a', - {href: '#midi-project-files'}, - language.$('releaseInfo.midiProjectFiles.shortcut.link')), - }), + !html.isBlank(relations.midiProjectFilesList) && + language.encapsulate(capsule, 'midiProjectFiles.shortcut', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#midi-project-files'}, + language.$(capsule, 'link')), + })), - sec.additionalFiles && - language.$('releaseInfo.additionalFiles.shortcut', { - link: html.tag('a', - {href: '#midi-project-files'}, - language.$('releaseInfo.additionalFiles.shortcut.link')), - }), + !html.isBlank(relations.additionalFilesList) && + language.encapsulate(capsule, 'additionalFiles.shortcut', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#midi-project-files'}, + language.$(capsule, 'link')), + })), - sec.artistCommentary && - language.$('releaseInfo.readCommentary', { - link: html.tag('a', - {href: '#artist-commentary'}, - language.$('releaseInfo.readCommentary.link')), - }), - ]), + !html.isBlank(relations.artistCommentarySection) && + language.encapsulate(capsule, 'readCommentary', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#artist-commentary'}, + language.$(capsule, 'link')), + })), + ])), - sec.otherReleases && [ - sec.otherReleases.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'also-released-as', + attributes: {id: 'also-released-as'}, title: language.$('releaseInfo.alsoReleasedAs'), }), - html.tag('ul', - stitchArrays({ - trackLink: sec.otherReleases.trackLinks, - albumLink: sec.otherReleases.albumLinks, - datetimestamp: sec.otherReleases.datetimestamps, - colorStyle: sec.otherReleases.colorStyles, - }).map(({ - trackLink, - albumLink, - datetimestamp, - colorStyle, - }) => { - const parts = ['releaseInfo.alsoReleasedAs.item']; - const options = {}; - - options.track = trackLink.slot('color', false); - options.album = albumLink; - - if (datetimestamp) { - parts.push('withYear'); - options.year = - datetimestamp.slots({ - style: 'year', - tooltip: true, - }); - } - - return ( - html.tag('li', - colorStyle, - language.$(...parts, options))); - })), - ], + relations.otherReleasesList, + ]), - sec.contributors && [ - sec.contributors.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'contributors', + attributes: {id: 'contributors'}, title: language.$('releaseInfo.contributors'), }), - sec.contributors.list, - ], + relations.contributorContributionList.slots({ + chronologyKind: 'trackContribution', + }), + ]), + + html.tags([ + language.encapsulate('releaseInfo.tracksReferenced', capsule => + relations.contentHeading.clone() + .slots({ + attributes: {id: 'references'}, + + title: + language.$(capsule, { + track: + html.tag('i', data.name), + }), + + stickyTitle: + language.$(capsule, 'sticky'), + })), - sec.references && [ - sec.references.heading - .slots({ - id: 'references', - title: - language.$('releaseInfo.tracksReferenced', { - track: html.tag('i', data.name), - }), - }), + relations.referencedTracksList, + ]), - sec.references.list, - ], + html.tags([ + language.encapsulate('releaseInfo.tracksSampled', capsule => + relations.contentHeading.clone() + .slots({ + attributes: {id: 'samples'}, - sec.samples && [ - sec.samples.heading - .slots({ - id: 'samples', - title: - language.$('releaseInfo.tracksSampled', { - track: html.tag('i', data.name), - }), - }), + title: + language.$(capsule, { + track: + html.tag('i', data.name), + }), - sec.samples.list, - ], + stickyTitle: + language.$(capsule, 'sticky'), + })), - sec.referencedBy && [ - sec.referencedBy.heading - .slots({ - id: 'referenced-by', - title: - language.$('releaseInfo.tracksThatReference', { - track: html.tag('i', data.name), - }), - }), + relations.sampledTracksList, + ]), - sec.referencedBy.list, - ], + language.encapsulate('releaseInfo.tracksThatReference', capsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'referenced-by'}, - sec.sampledBy && [ - sec.sampledBy.heading - .slots({ - id: 'referenced-by', - title: - language.$('releaseInfo.tracksThatSample', { - track: html.tag('i', data.name), - }), - }), + title: + language.$(capsule, { + track: html.tag('i', data.name), + }), - sec.sampledBy.list, - ], + stickyTitle: + language.$(capsule, 'sticky'), + }), - sec.flashesThatFeature && [ - sec.flashesThatFeature.heading - .slots({ - id: 'featured-in', - title: - language.$('releaseInfo.flashesThatFeature', { - track: html.tag('i', data.name), - }), - }), + relations.referencedByTracksList + .slots({ + headingString: capsule, + }), + ])), + + language.encapsulate('releaseInfo.tracksThatSample', capsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'sampled-by'}, + + title: + language.$(capsule, { + track: html.tag('i', data.name), + }), + + stickyTitle: + language.$(capsule, 'sticky'), + }), + + relations.sampledByTracksList + .slots({ + headingString: capsule, + }), + ])), + + html.tags([ + language.encapsulate('releaseInfo.flashesThatFeature', capsule => + relations.contentHeading.clone() + .slots({ + attributes: {id: 'featured-in'}, + + title: + language.$(capsule, { + track: html.tag('i', data.name), + }), + + stickyTitle: + language.$(capsule, 'sticky'), + })), + + relations.flashesThatFeatureList, + ]), - html.tag('ul', sec.flashesThatFeature.entries.map(({flashLink, trackLink}) => - (trackLink - ? html.tag('li', {class: 'rerelease'}, - language.$('releaseInfo.flashesThatFeature.item.asDifferentRelease', { - flash: flashLink, - track: trackLink, - })) - : html.tag('li', - language.$('releaseInfo.flashesThatFeature.item', { - flash: flashLink, - }))))), - ], - - sec.lyrics && [ - sec.lyrics.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'lyrics', + attributes: {id: 'lyrics'}, title: language.$('releaseInfo.lyrics'), }), html.tag('blockquote', - sec.lyrics.content - .slot('mode', 'lyrics')), - ], + {[html.onlyIfContent]: true}, + relations.lyrics.slot('mode', 'lyrics')), + ]), - sec.sheetMusicFiles && [ - sec.sheetMusicFiles.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'sheet-music-files', + attributes: {id: 'sheet-music-files'}, title: language.$('releaseInfo.sheetMusicFiles.heading'), }), - sec.sheetMusicFiles.list, - ], + relations.sheetMusicFilesList, + ]), - sec.midiProjectFiles && [ - sec.midiProjectFiles.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'midi-project-files', + attributes: {id: 'midi-project-files'}, title: language.$('releaseInfo.midiProjectFiles.heading'), }), - sec.midiProjectFiles.list, - ], + relations.midiProjectFilesList, + ]), - sec.additionalFiles && [ - sec.additionalFiles.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'additional-files', - title: - language.$('releaseInfo.additionalFiles.heading', { - additionalFiles: - language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}), - }), + attributes: {id: 'additional-files'}, + title: language.$('releaseInfo.additionalFiles.heading'), }), - sec.additionalFiles.list, - ], + relations.additionalFilesList, + ]), - sec.artistCommentary, + relations.artistCommentarySection, ], navLinkStyle: 'hierarchical', + navLinks: [ {auto: 'home'}, + {html: relations.albumLink.slot('color', false)}, + { html: - (data.hasTrackNumbers - ? language.$('trackPage.nav.track.withNumber', { - number: data.trackNumber, - track: relations.trackLink - .slot('attributes', {class: 'current'}), - }) - : language.$('trackPage.nav.track', { - track: relations.trackLink - .slot('attributes', {class: 'current'}), - })), + language.encapsulate(pageCapsule, 'nav.track', workingCapsule => { + const workingOptions = {}; + + workingOptions.track = + relations.trackLink + .slot('attributes', {class: 'current'}); + + if (data.hasTrackNumbers) { + workingCapsule += '.withNumber'; + workingOptions.number = data.trackNumber; + } + + return language.$(workingCapsule, workingOptions); + }), }, ], @@ -579,20 +393,6 @@ export default { showExtraLinks: false, }), - navContent: - relations.chronologyLinks.slots({ - chronologyInfoSets: [ - { - headingString: 'misc.chronology.heading.track', - contributions: relations.artistChronologyContributions, - }, - { - headingString: 'misc.chronology.heading.coverArt', - contributions: relations.coverArtistChronologyContributions, - }, - ], - }), - secondaryNav: relations.secondaryNav .slot('mode', 'track'), @@ -600,8 +400,7 @@ export default { leftSidebar: relations.sidebar, socialEmbed: relations.socialEmbed, - }); - }, + })), }; /* diff --git a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js new file mode 100644 index 00000000..5958be9a --- /dev/null +++ b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js @@ -0,0 +1,62 @@ +import {sortFlashesChronologically} from '#sort'; +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['linkFlash', 'linkTrack'], + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({wikiInfo}) => ({ + enableFlashesAndGames: + wikiInfo.enableFlashesAndGames, + }), + + query: (sprawl, track) => ({ + sortedFeatures: + (sprawl.enableFlashesAndGames + ? sortFlashesChronologically( + [track, ...track.otherReleases].flatMap(track => + track.featuredInFlashes.map(flash => ({ + flash, + track, + + // These properties are only used for the sort. + act: flash.act, + date: flash.date, + })))) + : []), + }), + + relations: (relation, query, _sprawl, track) => ({ + flashLinks: + query.sortedFeatures + .map(({flash}) => relation('linkFlash', flash)), + + trackLinks: + query.sortedFeatures + .map(({track: directlyFeaturedTrack}) => + (directlyFeaturedTrack === track + ? null + : relation('linkTrack', directlyFeaturedTrack))), + }), + + generate: (relations, {html, language}) => + html.tag('ul', + {[html.onlyIfContent]: true}, + + stitchArrays({ + flashLink: relations.flashLinks, + trackLink: relations.trackLinks, + }).map(({flashLink, trackLink}) => { + const attributes = html.attributes(); + const parts = ['releaseInfo.flashesThatFeature.item']; + const options = {flash: flashLink}; + + if (trackLink) { + attributes.add('class', 'rerelease'); + parts.push('asDifferentRelease'); + options.track = trackLink; + } + + return html.tag('li', attributes, language.$(...parts, options)); + })), +}; diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js new file mode 100644 index 00000000..004bba6d --- /dev/null +++ b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js @@ -0,0 +1,80 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateAbsoluteDatetimestamp', + 'generateColorStyleAttribute', + 'generateRelativeDatetimestamp', + 'linkAlbum', + 'linkTrack', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, track) => ({ + colorStyles: + track.otherReleases + .map(track => relation('generateColorStyleAttribute', track.color)), + + trackLinks: + track.otherReleases + .map(track => relation('linkTrack', track)), + + albumLinks: + track.otherReleases + .map(track => relation('linkAlbum', track.album)), + + datetimestamps: + track.otherReleases.map(track2 => + (track2.date + ? (track.date + ? relation('generateRelativeDatetimestamp', + track2.date, + track.date) + : relation('generateAbsoluteDatetimestamp', + track2.date)) + : null)), + + items: + track.otherReleases.map(track => ({ + trackLink: relation('linkTrack', track), + albumLink: relation('linkAlbum', track.album), + })), + }), + + generate: (relations, {html, language}) => + html.tag('ul', + {[html.onlyIfContent]: true}, + + stitchArrays({ + trackLink: relations.trackLinks, + albumLink: relations.albumLinks, + datetimestamp: relations.datetimestamps, + colorStyle: relations.colorStyles, + }).map(({ + trackLink, + albumLink, + datetimestamp, + colorStyle, + }) => { + const parts = ['releaseInfo.alsoReleasedAs.item']; + const options = {}; + + options.track = trackLink.slot('color', false); + options.album = albumLink; + + if (datetimestamp) { + parts.push('withYear'); + options.year = + datetimestamp.slots({ + style: 'year', + tooltip: true, + }); + } + + return ( + html.tag('li', + colorStyle, + language.$(...parts, options))); + })), +}; diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js index 3c36d248..7c3b11c1 100644 --- a/src/content/dependencies/generateTrackList.js +++ b/src/content/dependencies/generateTrackList.js @@ -5,55 +5,42 @@ export default { extraDependencies: ['html', 'language'], - relations(relation, tracks) { - if (empty(tracks)) { - return {}; - } - - return { - trackLinks: - tracks - .map(track => relation('linkTrack', track)), - - contributionLinks: - tracks - .map(track => - (empty(track.artistContribs) - ? null - : track.artistContribs - .map(contrib => relation('linkContribution', contrib)))), - }; - }, - - slots: { - showContribution: {type: 'boolean', default: false}, - showIcons: {type: 'boolean', default: false}, - }, - - generate(relations, slots, {html, language}) { - return ( - html.tag('ul', - stitchArrays({ - trackLink: relations.trackLinks, - contributionLinks: relations.contributionLinks, - }).map(({trackLink, contributionLinks}) => - html.tag('li', - (empty(contributionLinks) - ? trackLink - : language.$('trackList.item.withArtists', { - track: trackLink, - by: - html.tag('span', {class: 'by'}, - html.metatag('chunkwrap', {split: ','}, - language.$('trackList.item.withArtists.by', { - artists: - language.formatConjunctionList( - contributionLinks.map(link => - link.slots({ - showContribution: slots.showContribution, - showIcons: slots.showIcons, - }))), - }))), - })))))); - }, + relations: (relation, tracks) => ({ + trackLinks: + tracks + .map(track => relation('linkTrack', track)), + + contributionLinks: + tracks + .map(track => + track.artistContribs + .map(contrib => relation('linkContribution', contrib))), + }), + + generate: (relations, {html, language}) => + html.tag('ul', + {[html.onlyIfContent]: true}, + + stitchArrays({ + trackLink: relations.trackLinks, + contributionLinks: relations.contributionLinks, + }).map(({trackLink, contributionLinks}) => + html.tag('li', + language.encapsulate('trackList.item', itemCapsule => + language.encapsulate(itemCapsule, workingCapsule => { + const workingOptions = {track: trackLink}; + + if (!empty(contributionLinks)) { + workingCapsule += '.withArtists'; + workingOptions.by = + html.tag('span', {class: 'by'}, + html.metatag('chunkwrap', {split: ','}, + language.$(itemCapsule, 'withArtists.by', { + artists: + language.formatConjunctionList(contributionLinks), + }))); + } + + return language.$(workingCapsule, workingOptions); + }))))), }; diff --git a/src/content/dependencies/generateTrackListDividedByGroups.js b/src/content/dependencies/generateTrackListDividedByGroups.js index e070ac35..3cba479e 100644 --- a/src/content/dependencies/generateTrackListDividedByGroups.js +++ b/src/content/dependencies/generateTrackListDividedByGroups.js @@ -1,53 +1,138 @@ -import {empty} from '#sugar'; - -import groupTracksByGroup from '../util/groupTracksByGroup.js'; +import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateTrackList', 'linkGroup'], + contentDependencies: [ + 'generateContentHeading', + 'generateTrackList', + 'linkGroup', + ], + extraDependencies: ['html', 'language'], - relations(relation, tracks, groups) { - if (empty(tracks)) { - return {}; + query(tracks, dividingGroups) { + const groupings = new Map(); + const ungroupedTracks = []; + + // Entry order matters! Add blank lists for each group + // in the order that those groups are provided. + for (const group of dividingGroups) { + groupings.set(group, []); } - if (empty(groups)) { - return { - flatList: - relation('generateTrackList', tracks), - }; + for (const track of tracks) { + const firstMatchingGroup = + dividingGroups.find(group => group.albums.includes(track.album)); + + if (firstMatchingGroup) { + groupings.get(firstMatchingGroup).push(track); + } else { + ungroupedTracks.push(track); + } } - const lists = groupTracksByGroup(tracks, groups); + const groups = Array.from(groupings.keys()); + const groupedTracks = Array.from(groupings.values()); - return { - groupedLists: - Array.from(lists.entries()).map(([groupOrOther, tracks]) => ({ - ...(groupOrOther === 'other' - ? {other: true} - : {groupLink: relation('linkGroup', groupOrOther)}), + // Drop the empty lists, so just the groups which + // at least a single track matched are left. + filterMultipleArrays( + groups, + groupedTracks, + (_group, tracks) => !empty(tracks)); - list: - relation('generateTrackList', tracks), - })), - }; + return {groups, groupedTracks, ungroupedTracks}; }, - generate(relations, {html, language}) { - if (relations.flatList) { - return relations.flatList; - } + relations: (relation, query, tracks, groups) => ({ + flatList: + (empty(groups) + ? relation('generateTrackList', tracks) + : null), + + contentHeading: + relation('generateContentHeading'), + + groupLinks: + query.groups + .map(group => relation('linkGroup', group)), + + groupedTrackLists: + query.groupedTracks + .map(tracks => relation('generateTrackList', tracks)), + + ungroupedTrackList: + (empty(query.ungroupedTracks) + ? null + : relation('generateTrackList', query.ungroupedTracks)), + }), + + data: (query) => ({ + groupNames: + query.groups + .map(group => group.name), + }), - return html.tag('dl', - relations.groupedLists.map(({other, groupLink, list}) => [ - html.tag('dt', - (other - ? language.$('trackList.group.fromOther') - : language.$('trackList.group', { - group: groupLink - }))), - - html.tag('dd', list), - ])); + slots: { + headingString: { + type: 'string', + }, }, + + generate: (data, relations, slots, {html, language}) => + relations.flatList ?? + + html.tag('dl', + {[html.onlyIfContent]: true}, + + language.encapsulate('trackList', listCapsule => [ + stitchArrays({ + groupName: data.groupNames, + groupLink: relations.groupLinks, + trackList: relations.groupedTrackLists, + }).map(({ + groupName, + groupLink, + trackList, + }) => [ + language.encapsulate(listCapsule, 'fromGroup', capsule => + (slots.headingString + ? relations.contentHeading.clone().slots({ + tag: 'dt', + + title: + language.$(capsule, { + group: groupLink + }), + + stickyTitle: + language.$(slots.headingString, 'sticky', 'fromGroup', { + group: groupName, + }), + }) + : html.tag('dt', + language.$(capsule, { + group: groupLink + })))), + + html.tag('dd', trackList), + ]), + + relations.ungroupedTrackList && [ + language.encapsulate(listCapsule, 'fromOther', capsule => + (slots.headingString + ? relations.contentHeading.clone().slots({ + tag: 'dt', + + title: + language.$(capsule), + + stickyTitle: + language.$(slots.headingString, 'sticky', 'fromOther'), + }) + : html.tag('dt', + language.$(capsule)))), + + html.tag('dd', relations.ungroupedTrackList), + ], + ])), }; diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js index 3bdeaa4f..8a081046 100644 --- a/src/content/dependencies/generateTrackReleaseInfo.js +++ b/src/content/dependencies/generateTrackReleaseInfo.js @@ -47,44 +47,51 @@ export default { }, generate: (data, relations, {html, language}) => - html.tags([ - html.tag('p', - {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, - - [ - relations.artistContributionLinks - .slots({stringKey: 'releaseInfo.by'}), + language.encapsulate('releaseInfo', capsule => + html.tags([ + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + relations.artistContributionLinks.slots({ + stringKey: capsule + '.by', + chronologyKind: 'track', + }), - relations.coverArtistContributionsLine - ?.slots({stringKey: 'releaseInfo.coverArtBy'}), + relations.coverArtistContributionsLine?.slots({ + stringKey: capsule + '.coverArtBy', + chronologyKind: 'trackArt', + }), - data.date && - language.$('releaseInfo.released', { + language.$(capsule, 'released', { + [language.onlyIfOptions]: ['date'], date: language.formatDate(data.date), }), - data.coverArtDate && - language.$('releaseInfo.artReleased', { + language.$(capsule, 'artReleased', { + [language.onlyIfOptions]: ['date'], date: language.formatDate(data.coverArtDate), }), - data.duration && - language.$('releaseInfo.duration', { + language.$(capsule, 'duration', { + [language.onlyIfOptions]: ['duration'], duration: language.formatDuration(data.duration), }), - ]), - - html.tag('p', - (relations.externalLinks - ? language.$('releaseInfo.listenOn', { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => link.slot('context', 'track'))), - }) - : language.$('releaseInfo.listenOn.noLinks', { - name: html.tag('i', data.name), - }))), - ]), + ]), + + 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), + })))), + ])), }; diff --git a/src/content/dependencies/generateTrackSocialEmbed.js b/src/content/dependencies/generateTrackSocialEmbed.js index 0337fc46..9868f0e2 100644 --- a/src/content/dependencies/generateTrackSocialEmbed.js +++ b/src/content/dependencies/generateTrackSocialEmbed.js @@ -39,35 +39,35 @@ export default { return data; }, - generate(data, relations, {absoluteTo, language, urls}) { - return relations.socialEmbed.slots({ - title: - language.$('trackPage.socialEmbed.title', { - track: data.trackName, - }), + generate: (data, relations, {absoluteTo, language, urls}) => + language.encapsulate('trackPage.socialEmbed', embedCapsule => + relations.socialEmbed.slots({ + title: + language.$(embedCapsule, 'title', { + track: data.trackName, + }), - headingContent: - language.$('trackPage.socialEmbed.heading', { - album: data.albumName, - }), + headingContent: + language.$(embedCapsule, 'heading', { + album: data.albumName, + }), - headingLink: - absoluteTo('localized.album', data.albumDirectory), + headingLink: + absoluteTo('localized.album', data.albumDirectory), - imagePath: - (data.imageSource === 'album' - ? '/' + - urls - .from('shared.root') - .to('media.albumCover', data.albumDirectory, data.coverArtFileExtension) - : data.imageSource === 'track' - ? '/' + - urls - .from('shared.root') - .to('media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension) - : null), - }); - }, + imagePath: + (data.imageSource === 'album' + ? '/' + + urls + .from('shared.root') + .to('media.albumCover', data.albumDirectory, data.coverArtFileExtension) + : data.imageSource === 'track' + ? '/' + + urls + .from('shared.root') + .to('media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension) + : null), + })), }; /* diff --git a/src/content/dependencies/generateWikiHomeAlbumsRow.js b/src/content/dependencies/generateWikiHomeAlbumsRow.js index a19f104c..16c22bb3 100644 --- a/src/content/dependencies/generateWikiHomeAlbumsRow.js +++ b/src/content/dependencies/generateWikiHomeAlbumsRow.js @@ -113,10 +113,10 @@ export default { image.slots({ path, missingSourceContent: - name && - language.$('misc.albumGrid.noCoverArt', { - album: name, - }), + language.$('misc.albumGrid.noCoverArt', { + [language.onlyIfOptions]: ['album'], + album: name, + }), })); commonSlots.actionLinks = diff --git a/src/content/dependencies/generateWikiHomeNewsBox.js b/src/content/dependencies/generateWikiHomeNewsBox.js index bd0e4797..83a27695 100644 --- a/src/content/dependencies/generateWikiHomeNewsBox.js +++ b/src/content/dependencies/generateWikiHomeNewsBox.js @@ -1,4 +1,4 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { contentDependencies: [ @@ -39,49 +39,48 @@ export default { .map(entry => entry.date), }), - generate(data, relations, {html, language}) { - if (empty(relations.entryContents)) { - return html.blank(); - } + generate: (data, relations, {html, language}) => + language.encapsulate('homepage.news', boxCapsule => + relations.box.slots({ + attributes: {class: 'latest-news-sidebar-box'}, + collapsible: false, - return relations.box.slots({ - attributes: {class: 'latest-news-sidebar-box'}, - collapsible: false, + content: [ + html.tag('h1', + {[html.onlyIfSiblings]: true}, + language.$(boxCapsule, 'title')), - content: [ - html.tag('h1', language.$('homepage.news.title')), + stitchArrays({ + date: data.entryDates, + content: relations.entryContents, + mainLink: relations.entryMainLinks, + readMoreLink: relations.entryReadMoreLinks, + }).map(({ + date, + content, + mainLink, + readMoreLink, + }, index) => + language.encapsulate(boxCapsule, 'entry', entryCapsule => + html.tag('article', {class: 'news-entry'}, + index === 0 && + {class: 'first-news-entry'}, - stitchArrays({ - date: data.entryDates, - content: relations.entryContents, - mainLink: relations.entryMainLinks, - readMoreLink: relations.entryReadMoreLinks, - }).map(({ - date, - content, - mainLink, - readMoreLink, - }, index) => - html.tag('article', {class: 'news-entry'}, - index === 0 && - {class: 'first-news-entry'}, + [ + html.tag('h2', [ + html.tag('time', language.formatDate(date)), + mainLink, + ]), - [ - html.tag('h2', [ - html.tag('time', language.formatDate(date)), - mainLink, - ]), + content.slot('thumb', 'medium'), - content.slot('thumb', 'medium'), - - html.tag('p', - {[html.onlyIfContent]: true}, - readMoreLink - ?.slots({ - content: language.$('homepage.news.entry.viewRest'), - })), - ])), - ], - }); - }, + html.tag('p', + {[html.onlyIfContent]: true}, + readMoreLink + ?.slots({ + content: language.$(entryCapsule, 'viewRest'), + })), + ]))), + ], + })), }; diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 822efe3f..b1f02819 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -3,7 +3,6 @@ import {empty} from '#sugar'; export default { extraDependencies: [ - 'cachebust', 'checkIfImagePathHasCachedThumbnails', 'getDimensionsOfImagePath', 'getSizeOfImagePath', @@ -82,7 +81,6 @@ export default { }, generate(data, relations, slots, { - cachebust, checkIfImagePathHasCachedThumbnails, getDimensionsOfImagePath, getSizeOfImagePath, @@ -133,15 +131,8 @@ export default { !isMissingImageFile && !empty(contentWarnings); - const hasBothDimensions = - !!(slots.dimensions && - slots.dimensions[0] !== null && - slots.dimensions[1] !== null); - const willSquare = - (hasBothDimensions - ? slots.dimensions[0] === slots.dimensions[1] - : slots.square); + slots.square; const imgAttributes = html.attributes([ {class: 'image'}, @@ -152,7 +143,7 @@ export default { {width: slots.dimensions[0]}, slots.dimensions?.[1] && - {width: slots.dimensions[1]}, + {height: slots.dimensions[1]}, ]); const isPlaceholder = @@ -172,7 +163,7 @@ export default { if (willReveal) { reveal = [ html.tag('img', {class: 'reveal-symbol'}, - {src: to('shared.staticFile', 'warning.svg', cachebust)}), + {src: to('staticMisc.path', 'warning.svg')}), html.tag('br'), diff --git a/src/content/dependencies/linkAnythingMan.js b/src/content/dependencies/linkAnythingMan.js new file mode 100644 index 00000000..d4697403 --- /dev/null +++ b/src/content/dependencies/linkAnythingMan.js @@ -0,0 +1,25 @@ +export default { + contentDependencies: [ + 'linkAlbum', + 'linkFlash', + 'linkTrack', + ], + + query: (thing) => ({ + referenceType: thing.constructor[Symbol.for('Thing.referenceType')], + }), + + relations: (relation, query, thing) => ({ + link: + (query.referenceType === 'album' + ? relation('linkAlbum', thing) + : query.referenceType === 'flash' + ? relation('linkFlash', thing) + : query.referenceType === 'track' + ? relation('linkTrack', thing) + : null), + }), + + generate: (relations) => + relations.link, +}; diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js index 1a51c387..26f0b2d7 100644 --- a/src/content/dependencies/linkContribution.js +++ b/src/content/dependencies/linkContribution.js @@ -1,145 +1,78 @@ -import {empty, stitchArrays} from '#sugar'; - export default { contentDependencies: [ + 'generateContributionTooltip', 'generateTextWithTooltip', - 'generateTooltip', 'linkArtist', - 'linkExternalAsIcon', ], extraDependencies: ['html', 'language'], - relations(relation, contribution) { - const relations = {}; - - relations.artistLink = - relation('linkArtist', contribution.artist); + relations: (relation, contribution) => ({ + artistLink: + relation('linkArtist', contribution.artist), - relations.textWithTooltip = - relation('generateTextWithTooltip'); + textWithTooltip: + relation('generateTextWithTooltip'), - relations.tooltip = - relation('generateTooltip'); + tooltip: + relation('generateContributionTooltip', contribution), + }), - if (!empty(contribution.artist.urls)) { - relations.artistIcons = - contribution.artist.urls - .map(url => relation('linkExternalAsIcon', url)); - } - - return relations; - }, - - data(contribution) { - return { - contribution: contribution.annotation, - urls: contribution.artist.urls, - }; - }, + data: (contribution) => ({ + contribution: contribution.annotation, + urls: contribution.artist.urls, + }), slots: { showContribution: {type: 'boolean', default: false}, - showIcons: {type: 'boolean', default: false}, - preventWrapping: {type: 'boolean', default: true}, + showExternalLinks: {type: 'boolean', default: false}, + showChronology: {type: 'boolean', default: false}, - iconMode: { - validate: v => v.is('inline', 'tooltip'), - default: 'inline' - }, + preventWrapping: {type: 'boolean', default: true}, + chronologyKind: {type: 'string'}, }, - generate(data, relations, slots, {html, language}) { - const hasContribution = !!(slots.showContribution && data.contribution); - const hasExternalIcons = !!(slots.showIcons && relations.artistIcons); - - const parts = ['misc.artistLink']; - const options = {}; - - options.artist = - (hasExternalIcons && slots.iconMode === 'tooltip' - ? relations.textWithTooltip.slots({ - customInteractionCue: true, - - text: - relations.artistLink.slots({ - attributes: {class: 'text-with-tooltip-interaction-cue'}, - }), - - tooltip: - relations.tooltip.slots({ - attributes: - {class: ['icons', 'icons-tooltip']}, - - contentAttributes: - {[html.joinChildren]: ''}, - - content: - stitchArrays({ - icon: relations.artistIcons, - url: data.urls, - }).map(({icon, url}) => { - icon.setSlots({ - context: 'artist', - withText: true, - }); - - let platformText = - language.formatExternalLink(url, { - context: 'artist', - style: 'platform', - }); - - // This is a pretty ridiculous hack, but we currently - // don't have a way of telling formatExternalLink to *not* - // use the fallback string, which just formats the URL as - // its host/domain... so is technically detectable. - if (platformText.toString() === (new URL(url)).host) { - platformText = - language.$('misc.artistLink.noExternalLinkPlatformName'); - } - - const platformSpan = - html.tag('span', {class: 'icon-platform'}, - platformText); - - return [icon, platformSpan]; - }), - }), - }) - : relations.artistLink); - - if (hasContribution) { - parts.push('withContribution'); - options.contrib = data.contribution; - } - - if (hasExternalIcons && slots.iconMode === 'inline') { - parts.push('withExternalLinks'); - options.links = - html.tag('span', {class: ['icons', 'icons-inline']}, - {[html.noEdgeWhitespace]: true}, - language.formatUnitList( - relations.artistIcons - .slice(0, 4) - .map(icon => icon.slot('context', 'artist')))); - } - - const contributionPart = - language.formatString(...parts, options); - - if (!hasContribution && !hasExternalIcons) { - return contributionPart; - } - - return ( - html.tag('span', {class: 'contribution'}, - {[html.noEdgeWhitespace]: true}, - - parts.length > 1 && - slots.preventWrapping && - {class: 'nowrap'}, - - contributionPart)); - }, + generate: (data, relations, slots, {html, language}) => + html.tag('span', {class: 'contribution'}, + {[html.noEdgeWhitespace]: true}, + + slots.preventWrapping && + {class: 'nowrap'}, + + language.encapsulate('misc.artistLink', workingCapsule => { + const workingOptions = {}; + + relations.tooltip.setSlots({ + showExternalLinks: slots.showExternalLinks, + showChronology: slots.showChronology, + chronologyKind: slots.chronologyKind, + }); + + workingOptions.artist = + (html.isBlank(relations.tooltip) + ? relations.artistLink + : relations.textWithTooltip.slots({ + customInteractionCue: true, + + text: + relations.artistLink.slots({ + attributes: {class: 'text-with-tooltip-interaction-cue'}, + }), + + tooltip: + relations.tooltip.slots({ + showExternalLinks: slots.showExternalLinks, + showChronology: slots.showChronology, + chronologyKind: slots.chronologyKind, + }), + })); + + if (slots.showContribution && data.contribution) { + workingCapsule += '.withContribution'; + workingOptions.contrib = + data.contribution; + } + + return language.formatString(workingCapsule, workingOptions); + })), }; diff --git a/src/content/dependencies/linkExternalAsIcon.js b/src/content/dependencies/linkExternalAsIcon.js deleted file mode 100644 index 6f37529e..00000000 --- a/src/content/dependencies/linkExternalAsIcon.js +++ /dev/null @@ -1,51 +0,0 @@ -import {isExternalLinkContext} from '#external-links'; - -export default { - extraDependencies: ['html', 'language', 'to'], - - data: (url) => ({url}), - - slots: { - context: { - // This awkward syntax is because the slot descriptor validator can't - // differentiate between a function that returns a validator (the usual - // syntax) and a function that is itself a validator. - validate: () => isExternalLinkContext, - default: 'generic', - }, - - withText: {type: 'boolean'}, - }, - - generate(data, slots, {html, language, to}) { - const format = style => - language.formatExternalLink(data.url, {style, context: slots.context}); - - const platformText = format('platform'); - const handleText = format('handle'); - const iconId = format('icon-id'); - - return html.tag('a', {class: 'icon'}, - {href: data.url}, - - slots.withText && - {class: 'has-text'}, - - [ - html.tag('svg', [ - !slots.withText && - html.tag('title', platformText), - - html.tag('use', { - href: to('shared.staticIcon', iconId), - }), - ]), - - slots.withText && - html.tag('span', {class: 'icon-text'}, - (html.isBlank(handleText) - ? platformText - : handleText)), - ]); - }, -}; diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js index 0af586cd..41944959 100644 --- a/src/content/dependencies/listArtistsByContributions.js +++ b/src/content/dependencies/listArtistsByContributions.js @@ -1,6 +1,13 @@ import {sortAlphabetically, sortByCount} from '#sort'; -import {empty, filterByCount, filterMultipleArrays, stitchArrays, unique} - from '#sugar'; + +import { + accumulateSum, + empty, + filterByCount, + filterMultipleArrays, + stitchArrays, + unique, +} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], @@ -38,26 +45,33 @@ export default { 'artistsByTrackContributions', 'countsByTrackContributions', artist => - unique([ - ...artist.tracksAsContributor, - ...artist.tracksAsArtist, - ]).length); + (unique( + ([ + artist.trackArtistContributions, + artist.trackContributorContributions, + ]).flat() + .map(({thing}) => thing) + )).length); queryContributionInfo( 'artistsByArtworkContributions', 'countsByArtworkContributions', artist => - artist.tracksAsCoverArtist.length + - artist.albumsAsCoverArtist.length + - artist.albumsAsWallpaperArtist.length + - artist.albumsAsBannerArtist.length); + accumulateSum( + [ + artist.albumCoverArtistContributions, + artist.albumWallpaperArtistContributions, + artist.albumBannerArtistContributions, + artist.trackCoverArtistContributions, + ], + contribs => contribs.length)); if (sprawl.enableFlashesAndGames) { queryContributionInfo( 'artistsByFlashContributions', 'countsByFlashContributions', artist => - artist.flashesAsContributor.length); + artist.flashContributorContributions.length); } return query; diff --git a/src/content/dependencies/listArtistsByDuration.js b/src/content/dependencies/listArtistsByDuration.js index f677d82c..6b2a18a0 100644 --- a/src/content/dependencies/listArtistsByDuration.js +++ b/src/content/dependencies/listArtistsByDuration.js @@ -1,6 +1,5 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; -import {getTotalDuration} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], @@ -16,11 +15,7 @@ export default { artistData.filter(artist => !artist.isAlias)); const durations = - artists.map(artist => - getTotalDuration([ - ...(artist.tracksAsArtist ?? []), - ...(artist.tracksAsContributor ?? []), - ], {originalReleasesOnly: true})); + artists.map(artist => artist.totalDuration); filterByCount(artists, durations); sortByCount(artists, durations, {greatestFirst: true}); diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js index 30884d24..0bf9dd2d 100644 --- a/src/content/dependencies/listArtistsByGroup.js +++ b/src/content/dependencies/listArtistsByGroup.js @@ -1,6 +1,13 @@ import {sortAlphabetically} from '#sort'; -import {empty, filterMultipleArrays, stitchArrays, unique} from '#sugar'; -import {getArtistNumContributions} from '#wiki-data'; + +import { + empty, + filterByCount, + filterMultipleArrays, + stitchArrays, + transposeArrays, + unique, +} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], @@ -15,29 +22,69 @@ export default { sortAlphabetically( sprawl.artistData.filter(artist => !artist.isAlias)); - const groups = + const interestingGroups = sprawl.wikiInfo.divideTrackListsByGroups; - if (empty(groups)) { - return {spec, artists}; + if (empty(interestingGroups)) { + return {spec}; } - const artistGroups = + // We don't actually care about *which* things belong to each group, only + // how many belong to each group. So we'll just compute a list of all the + // (interesting) groups that each of each artists' things belongs to. + const artistThingGroups = artists.map(artist => - unique( - unique([ - ...artist.albumsAsAny, - ...artist.tracksAsAny.map(track => track.album), - ]).flatMap(album => album.groups))) - - const artistsByGroup = - groups.map(group => - artists.filter((artist, index) => artistGroups[index].includes(group))); - - filterMultipleArrays(groups, artistsByGroup, - (group, artists) => !empty(artists)); - - return {spec, groups, artistsByGroup}; + ([ + (unique( + ([ + artist.albumArtistContributions, + artist.albumCoverArtistContributions, + artist.albumWallpaperArtistContributions, + artist.albumBannerArtistContributions, + ]).flat() + .map(({thing}) => thing) + )).map(album => album.groups), + (unique( + ([ + artist.trackArtistContributions, + artist.trackContributorContributions, + artist.trackCoverArtistContributions, + ]).flat() + .map(({thing}) => thing) + )).map(track => track.album.groups), + ]).flat() + .map(groups => groups + .filter(group => interestingGroups.includes(group)))); + + const [artistsByGroup, countsByGroup] = + transposeArrays(interestingGroups.map(group => { + const counts = + artistThingGroups + .map(thingGroups => thingGroups + .filter(thingGroups => thingGroups.includes(group)) + .length); + + const filteredArtists = artists.slice(); + + filterByCount(filteredArtists, counts); + + return [filteredArtists, counts]; + })); + + const groups = interestingGroups; + + filterMultipleArrays( + groups, + artistsByGroup, + countsByGroup, + (_group, artists, _counts) => !empty(artists)); + + return { + spec, + groups, + artistsByGroup, + countsByGroup, + }; }, relations(relation, query) { @@ -46,12 +93,6 @@ export default { relations.page = relation('generateListingPage', query.spec); - if (query.artists) { - relations.artistLinks = - query.artists - .map(artist => relation('linkArtist', artist)); - } - if (query.artistsByGroup) { relations.groupLinks = query.groups @@ -69,65 +110,43 @@ export default { data(query) { const data = {}; - if (query.artists) { - data.counts = - query.artists - .map(artist => getArtistNumContributions(artist)); - } - if (query.artistsByGroup) { data.groupDirectories = query.groups .map(group => group.directory); data.countsByGroup = - query.artistsByGroup - .map(artists => artists - .map(artist => getArtistNumContributions(artist))); + query.countsByGroup; } return data; }, - generate(data, relations, {language}) { - return ( - (relations.artistLinksByGroup - ? relations.page.slots({ - type: 'chunks', - - showSkipToSection: true, - chunkIDs: - data.groupDirectories - .map(directory => `contributed-to-${directory}`), - - chunkTitles: - relations.groupLinks.map(groupLink => ({ - group: groupLink, - })), - - chunkRows: - stitchArrays({ - artistLinks: relations.artistLinksByGroup, - counts: data.countsByGroup, - }).map(({artistLinks, counts}) => - stitchArrays({ - link: artistLinks, - count: counts, - }).map(({link, count}) => ({ - artist: link, - contributions: language.countContributions(count, {unit: true}), - }))), - }) - : relations.page.slots({ - type: 'rows', - rows: - stitchArrays({ - link: relations.artistLinks, - count: data.counts, - }).map(({link, count}) => ({ - artist: link, - contributions: language.countContributions(count, {unit: true}), - })), - }))); - }, + generate: (data, relations, {language}) => + relations.page.slots({ + type: 'chunks', + + showSkipToSection: true, + chunkIDs: + data.groupDirectories + .map(directory => `contributed-to-${directory}`), + + chunkTitles: + relations.groupLinks.map(groupLink => ({ + group: groupLink, + })), + + chunkRows: + stitchArrays({ + artistLinks: relations.artistLinksByGroup, + counts: data.countsByGroup, + }).map(({artistLinks, counts}) => + stitchArrays({ + link: artistLinks, + count: counts, + }).map(({link, count}) => ({ + artist: link, + contributions: language.countContributions(count, {unit: true}), + }))), + }), }; diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js index ab2eca93..79bba441 100644 --- a/src/content/dependencies/listRandomPageLinks.js +++ b/src/content/dependencies/listRandomPageLinks.js @@ -74,20 +74,22 @@ export default { }, generate(data, relations, {html, language}) { + const capsule = language.encapsulate('listingPage.other.randomPages'); + const miscellaneousChunkRows = [ - { + language.encapsulate(capsule, 'chunk.item.randomArtist', capsule => ({ stringsKey: 'randomArtist', mainLink: html.tag('a', {href: '#', 'data-random': 'artist'}, - language.$('listingPage.other.randomPages.chunk.item.randomArtist.mainLink')), + language.$(capsule, 'mainLink')), atLeastTwoContributions: html.tag('a', {href: '#', 'data-random': 'artist-more-than-one-contrib'}, - language.$('listingPage.other.randomPages.chunk.item.randomArtist.atLeastTwoContributions')), - }, + language.$(capsule, 'atLeastTwoContributions')), + })), {stringsKey: 'randomAlbumWholeSite'}, {stringsKey: 'randomTrackWholeSite'}, @@ -104,24 +106,25 @@ export default { content: [ html.tag('p', - language.$('listingPage.other.randomPages.chooseLinkLine', { - fromPart: - (relations.groupLinks - ? language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.dividedByGroups') - : language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.notDividedByGroups')), + language.encapsulate(capsule, 'chooseLinkLine', capsule => + language.$(capsule, { + fromPart: + (relations.groupLinks + ? language.$(capsule, 'fromPart.dividedByGroups') + : language.$(capsule, 'fromPart.notDividedByGroups')), - browserSupportPart: - language.$('listingPage.other.randomPages.chooseLinkLine.browserSupportPart'), - })), + browserSupportPart: + language.$(capsule, 'browserSupportPart'), + }))), html.tag('p', {id: 'data-loading-line'}, - language.$('listingPage.other.randomPages.dataLoadingLine')), + language.$(capsule, 'dataLoadingLine')), html.tag('p', {id: 'data-loaded-line'}, - language.$('listingPage.other.randomPages.dataLoadedLine')), + language.$(capsule, 'dataLoadedLine')), html.tag('p', {id: 'data-error-line'}, - language.$('listingPage.other.randomPages.dataErrorLine')), + language.$(capsule, 'dataErrorLine')), ], showSkipToSection: true, @@ -148,17 +151,18 @@ export default { ... (relations.groupLinks - ? relations.groupLinks.map(() => ({ - randomAlbum: - html.tag('a', - {href: '#', 'data-random': 'album-in-group-dl'}, - language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomAlbum')), - - randomTrack: - html.tag('a', - {href: '#', 'data-random': 'track-in-group-dl'}, - language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomTrack')), - })) + ? relations.groupLinks.map(() => + language.encapsulate(capsule, 'chunk.title.fromGroup.accent', capsule => ({ + randomAlbum: + html.tag('a', + {href: '#', 'data-random': 'album-in-group-dl'}, + language.$(capsule, 'randomAlbum')), + + randomTrack: + html.tag('a', + {href: '#', 'data-random': 'track-in-group-dl'}, + language.$(capsule, 'randomTrack')), + }))) : [null]), ], diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js index 01ce4e2d..0a2bfd6c 100644 --- a/src/content/dependencies/listTracksByDate.js +++ b/src/content/dependencies/listTracksByDate.js @@ -15,7 +15,8 @@ export default { chunks: chunkByProperties( - sortAlbumsTracksChronologically(trackData.slice()), + sortAlbumsTracksChronologically( + trackData.filter(track => track.date)), ['album', 'date']), }; }, diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 0904cde6..5f803a3b 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -45,7 +45,7 @@ export default { sprawl(wikiData, content) { const find = bindFind(wikiData); - const parsedNodes = parseInput(content); + const parsedNodes = parseInput(content ?? ''); return { nodes: parsedNodes @@ -262,6 +262,10 @@ export default { height && {height}, style && {style}, + align === 'center' && + !link && + {class: 'align-center'}, + pixelate && {class: 'pixelate'}); @@ -271,16 +275,20 @@ export default { {href: link}, {target: '_blank'}, + align === 'center' && + {class: 'align-center'}, + {title: - language.$('misc.external.opensInNewTab', { - link: - language.formatExternalLink(link, { - style: 'platform', - }), + language.encapsulate('misc.external.opensInNewTab', capsule => + language.$(capsule, { + link: + language.formatExternalLink(link, { + style: 'platform', + }), - annotation: - language.$('misc.external.opensInNewTab.annotation'), - }).toString()}, + annotation: + language.$(capsule, 'annotation'), + }).toString())}, content); } @@ -505,7 +513,11 @@ export default { addText(markedOutput.slice(parseFrom)); } - return html.tags(tags, {[html.joinChildren]: ''}); + return ( + html.tags(tags, { + [html.joinChildren]: '', + [html.onlyIfContent]: true, + })); }; if (slots.mode === 'inline') { @@ -530,9 +542,9 @@ export default { // Expand line breaks which don't follow a list, quote, // or <br> / " ", and which don't precede or follow // indented text (by at least two spaces). - .replace(/(?<!^ *-.*|^>.*|^ .*\n*| $|<br>$)\n+(?! |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */ + .replace(/(?<!^ *(?:-|\d\.).*|^>.*|^ .*\n*| $|<br>$)\n+(?! |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */ // Expand line breaks which are at the end of a list. - .replace(/(?<=^ *-.*)\n+(?!^ *-)/gm, '\n\n') + .replace(/(?<=^ *(?:-|\d\.).*)\n+(?!^ *(?:-|\d\.))/gm, '\n\n') // Expand line breaks which are at the end of a quote. .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n'); |