diff options
Diffstat (limited to 'src/content/dependencies')
203 files changed, 9630 insertions, 3872 deletions
diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js index 68120b23..7e05b5b5 100644 --- a/src/content/dependencies/generateAdditionalFilesList.js +++ b/src/content/dependencies/generateAdditionalFilesList.js @@ -1,26 +1,22 @@ -import {stitchArrays} from '#sugar'; - export default { + contentDependencies: ['generateAdditionalFilesListChunk'], extraDependencies: ['html'], - slots: { - chunks: { - validate: v => v.strictArrayOf(v.isHTML), - }, + relations: (relation, additionalFiles) => ({ + chunks: + additionalFiles + .map(file => relation('generateAdditionalFilesListChunk', file)), + }), - chunkItems: { - validate: v => v.strictArrayOf(v.isHTML), - }, + slots: { + showFileSizes: {type: 'boolean', default: true}, }, - generate: (slots, {html}) => + generate: (relations, slots, {html}) => html.tag('ul', {class: 'additional-files-list'}, {[html.onlyIfContent]: true}, - stitchArrays({ - chunk: slots.chunks, - items: slots.chunkItems, - }).map(({chunk, items}) => - chunk.clone() - .slot('items', items))), + relations.chunks.map(chunk => chunk.slots({ + showFileSizes: slots.showFileSizes, + }))), }; diff --git a/src/content/dependencies/generateAdditionalFilesListChunk.js b/src/content/dependencies/generateAdditionalFilesListChunk.js index e66560fc..3cac851b 100644 --- a/src/content/dependencies/generateAdditionalFilesListChunk.js +++ b/src/content/dependencies/generateAdditionalFilesListChunk.js @@ -1,47 +1,81 @@ +import {stitchArrays} from '#sugar'; + export default { - extraDependencies: ['html', 'language'], + contentDependencies: ['linkAdditionalFile', 'transformContent'], + extraDependencies: ['getSizeOfMediaFile', 'html', 'language', 'urls'], - slots: { - title: { - type: 'html', - mutable: false, - }, + relations: (relation, file) => ({ + description: + relation('transformContent', file.description), - description: { - type: 'html', - mutable: false, - }, + links: + file.filenames + .map(filename => relation('linkAdditionalFile', file, filename)), + }), + + data: (file) => ({ + title: + file.title, + + paths: + file.paths, + }), - items: { - validate: v => v.looseArrayOf(v.isHTML), + slots: { + showFileSizes: { + type: 'boolean', }, }, - generate: (slots, {html, language}) => - language.encapsulate('releaseInfo.additionalFiles.entry', capsule => + generate: (data, relations, slots, {getSizeOfMediaFile, html, language, urls}) => + language.encapsulate('releaseInfo.additionalFiles', capsule => html.tag('li', html.tag('details', - html.isBlank(slots.items) && + html.isBlank(relations.links) && {open: true}, [ html.tag('summary', html.tag('span', - language.$(capsule, { + language.$(capsule, 'entry', { title: - html.tag('span', {class: 'group-name'}, - slots.title), + html.tag('b', data.title), }))), html.tag('ul', [ html.tag('li', {class: 'entry-description'}, {[html.onlyIfContent]: true}, - slots.description), - (html.isBlank(slots.items) + relations.description.slot('mode', 'inline')), + + (html.isBlank(relations.links) ? html.tag('li', - language.$(capsule, 'noFilesAvailable')) - : slots.items), + language.$(capsule, 'entry.noFilesAvailable')) + + : stitchArrays({ + link: relations.links, + path: data.paths, + }).map(({link, path}) => + html.tag('li', + language.encapsulate(capsule, 'file', workingCapsule => { + const workingOptions = {file: link}; + + if (slots.showFileSizes) { + const fileSize = + getSizeOfMediaFile( + urls + .from('media.root') + .to(...path)); + + if (fileSize) { + workingCapsule += '.withSize'; + workingOptions.size = + language.formatFileSize(fileSize); + } + } + + return language.$(workingCapsule, workingOptions); + })))), ]), ]))), }; diff --git a/src/content/dependencies/generateAdditionalFilesListChunkItem.js b/src/content/dependencies/generateAdditionalFilesListChunkItem.js deleted file mode 100644 index c37d6bb2..00000000 --- a/src/content/dependencies/generateAdditionalFilesListChunkItem.js +++ /dev/null @@ -1,30 +0,0 @@ -export default { - extraDependencies: ['html', 'language'], - - slots: { - fileLink: { - type: 'html', - mutable: false, - }, - - fileSize: { - validate: v => v.isWholeNumber, - }, - }, - - generate(slots, {html, language}) { - const itemParts = ['releaseInfo.additionalFiles.file']; - const itemOptions = {file: slots.fileLink}; - - if (slots.fileSize) { - itemParts.push('withSize'); - itemOptions.size = language.formatFileSize(slots.fileSize); - } - - const li = - html.tag('li', - language.$(...itemParts, itemOptions)); - - return li; - }, -}; diff --git a/src/content/dependencies/generateAdditionalNamesBox.js b/src/content/dependencies/generateAdditionalNamesBox.js index 63427c58..b7392dfd 100644 --- a/src/content/dependencies/generateAdditionalNamesBox.js +++ b/src/content/dependencies/generateAdditionalNamesBox.js @@ -9,12 +9,20 @@ export default { }), generate: (relations, {html, language}) => - html.tag('div', {id: 'additional-names-box'}, [ - html.tag('p', - language.$('misc.additionalNames.title')), + html.tag('div', {id: 'additional-names-box'}, + {class: 'drop'}, + {[html.onlyIfContent]: true}, - html.tag('ul', - relations.items - .map(item => html.tag('li', item))), - ]), + [ + html.tag('p', + {[html.onlyIfSiblings]: true}, + + language.$('misc.additionalNames.title')), + + html.tag('ul', + {[html.onlyIfContent]: true}, + + relations.items + .map(item => html.tag('li', item))), + ]), }; diff --git a/src/content/dependencies/generateAdditionalNamesBoxItem.js b/src/content/dependencies/generateAdditionalNamesBoxItem.js index 7515b5b0..e3e59a34 100644 --- a/src/content/dependencies/generateAdditionalNamesBoxItem.js +++ b/src/content/dependencies/generateAdditionalNamesBoxItem.js @@ -1,7 +1,5 @@ -import {stitchArrays} from '#sugar'; - export default { - contentDependencies: ['linkTrack', 'transformContent'], + contentDependencies: ['transformContent'], extraDependencies: ['html', 'language'], relations: (relation, entry) => ({ @@ -12,21 +10,9 @@ export default { (entry.annotation ? relation('transformContent', entry.annotation) : null), - - trackLinks: - (entry.from - ? entry.from.map(track => relation('linkTrack', track)) - : null), - }), - - data: (entry) => ({ - albumNames: - (entry.from - ? entry.from.map(track => track.album.name) - : null), }), - generate: (data, relations, {html, language}) => { + generate: (relations, {html, language}) => { const prefix = 'misc.additionalNames.item'; const itemParts = [prefix]; @@ -42,19 +28,10 @@ export default { if (relations.annotationContent) { accentParts.push('withAnnotation'); accentOptions.annotation = - relations.annotationContent.slot('mode', 'inline'); - } - - if (relations.trackLinks) { - accentParts.push('withAlbums'); - accentOptions.albums = - language.formatConjunctionList( - stitchArrays({ - trackLink: relations.trackLinks, - albumName: data.albumNames, - }).map(({trackLink, albumName}) => - trackLink.slot('content', - language.sanitize(albumName)))); + relations.annotationContent.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }); } if (accentParts.length > 2) { diff --git a/src/content/dependencies/generateAlbumAdditionalFilesList.js b/src/content/dependencies/generateAlbumAdditionalFilesList.js deleted file mode 100644 index 9818a43c..00000000 --- a/src/content/dependencies/generateAlbumAdditionalFilesList.js +++ /dev/null @@ -1,96 +0,0 @@ -import {stitchArrays} from '#sugar'; - -export default { - contentDependencies: [ - 'generateAdditionalFilesList', - 'generateAdditionalFilesListChunk', - 'generateAdditionalFilesListChunkItem', - 'linkAlbumAdditionalFile', - 'transformContent', - ], - - extraDependencies: ['getSizeOfAdditionalFile', 'html', 'urls'], - - relations: (relation, album, additionalFiles) => ({ - list: - relation('generateAdditionalFilesList', additionalFiles), - - chunks: - additionalFiles - .map(() => relation('generateAdditionalFilesListChunk')), - - chunkDescriptions: - additionalFiles - .map(({description}) => - (description - ? relation('transformContent', description) - : null)), - - chunkItems: - additionalFiles - .map(({files}) => - (files ?? []) - .map(() => relation('generateAdditionalFilesListChunkItem'))), - - chunkItemFileLinks: - additionalFiles - .map(({files}) => - (files ?? []) - .map(file => relation('linkAlbumAdditionalFile', album, file))), - }), - - data: (album, additionalFiles) => ({ - albumDirectory: album.directory, - - chunkTitles: - additionalFiles - .map(({title}) => title), - - chunkItemLocations: - additionalFiles - .map(({files}) => files ?? []), - }), - - slots: { - showFileSizes: {type: 'boolean', default: true}, - }, - - generate: (data, relations, slots, {getSizeOfAdditionalFile, urls}) => - relations.list.slots({ - chunks: - stitchArrays({ - chunk: relations.chunks, - description: relations.chunkDescriptions, - title: data.chunkTitles, - }).map(({chunk, title, description}) => - chunk.slots({ - title, - description: - (description - ? description.slot('mode', 'inline') - : null), - })), - - chunkItems: - stitchArrays({ - items: relations.chunkItems, - fileLinks: relations.chunkItemFileLinks, - locations: data.chunkItemLocations, - }).map(({items, fileLinks, locations}) => - stitchArrays({ - item: items, - fileLink: fileLinks, - location: locations, - }).map(({item, fileLink, location}) => - item.slots({ - fileLink: fileLink, - fileSize: - (slots.showFileSizes - ? getSizeOfAdditionalFile( - urls - .from('media.root') - .to('media.albumAdditionalFile', data.albumDirectory, location)) - : 0), - }))), - }), -}; diff --git a/src/content/dependencies/generateAlbumArtInfoBox.js b/src/content/dependencies/generateAlbumArtInfoBox.js new file mode 100644 index 00000000..8c44c930 --- /dev/null +++ b/src/content/dependencies/generateAlbumArtInfoBox.js @@ -0,0 +1,39 @@ +export default { + contentDependencies: ['generateReleaseInfoContributionsLine'], + extraDependencies: ['html', 'language'], + + relations: (relation, album) => ({ + wallpaperArtistContributionsLine: + (album.wallpaperArtwork + ? relation('generateReleaseInfoContributionsLine', + album.wallpaperArtwork.artistContribs) + : null), + + bannerArtistContributionsLine: + (album.bannerArtwork + ? relation('generateReleaseInfoContributionsLine', + album.bannerArtwork.artistContribs) + : null), + }), + + generate: (relations, {html, language}) => + language.encapsulate('releaseInfo', capsule => + html.tag('div', {class: 'album-art-info'}, + {[html.onlyIfContent]: true}, + + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + relations.wallpaperArtistContributionsLine?.slots({ + stringKey: capsule + '.wallpaperArtBy', + chronologyKind: 'wallpaperArt', + }), + + relations.bannerArtistContributionsLine?.slots({ + stringKey: capsule + '.bannerArtBy', + chronologyKind: 'bannerArt', + }), + ]))), +}; diff --git a/src/content/dependencies/generateAlbumArtworkColumn.js b/src/content/dependencies/generateAlbumArtworkColumn.js new file mode 100644 index 00000000..e6762463 --- /dev/null +++ b/src/content/dependencies/generateAlbumArtworkColumn.js @@ -0,0 +1,38 @@ +export default { + contentDependencies: ['generateAlbumArtInfoBox', 'generateCoverArtwork'], + extraDependencies: ['html'], + + relations: (relation, album) => ({ + firstCover: + (album.hasCoverArt + ? relation('generateCoverArtwork', album.coverArtworks[0]) + : null), + + restCovers: + (album.hasCoverArt + ? album.coverArtworks.slice(1).map(artwork => + relation('generateCoverArtwork', artwork)) + : []), + + albumArtInfoBox: + relation('generateAlbumArtInfoBox', album), + }), + + generate: (relations, {html}) => + html.tags([ + relations.firstCover?.slots({ + showOriginDetails: true, + showArtTagDetails: true, + showReferenceDetails: true, + }), + + relations.albumArtInfoBox, + + relations.restCovers.map(cover => + cover.slots({ + showOriginDetails: true, + showArtTagDetails: true, + showReferenceDetails: true, + })), + ]), +}; diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index c14640af..3529c4dc 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -1,14 +1,14 @@ -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; export default { contentDependencies: [ 'generateAlbumCommentarySidebar', - 'generateAlbumCoverArtwork', 'generateAlbumNavAccent', - 'generateAlbumStyleRules', + 'generateAlbumSecondaryNav', + 'generateAlbumStyleTags', 'generateCommentaryEntry', 'generateContentHeading', - 'generateTrackCoverArtwork', + 'generateCoverArtwork', 'generatePageLayout', 'linkAlbum', 'linkExternal', @@ -17,17 +17,35 @@ export default { extraDependencies: ['html', 'language'], - relations(relation, album) { + query(album) { + const query = {}; + + query.tracksWithCommentary = + album.tracks + .filter(({commentary}) => !empty(commentary)); + + query.thingsWithCommentary = + (empty(album.commentary) + ? query.tracksWithCommentary + : [album, ...query.tracksWithCommentary]); + + return query; + }, + + relations(relation, query, album) { const relations = {}; relations.layout = relation('generatePageLayout'); + relations.secondaryNav = + relation('generateAlbumSecondaryNav', album); + relations.sidebar = relation('generateAlbumCommentarySidebar', album); - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); + relations.albumStyleTags = + relation('generateAlbumStyleTags', album, null); relations.albumLink = relation('linkAlbum', album); @@ -35,7 +53,7 @@ export default { relations.albumNavAccent = relation('generateAlbumNavAccent', album, null); - if (album.commentary) { + if (!empty(album.commentary)) { relations.albumCommentaryHeading = relation('generateContentHeading'); @@ -47,7 +65,7 @@ export default { if (album.hasCoverArt) { relations.albumCommentaryCover = - relation('generateAlbumCoverArtwork', album); + relation('generateCoverArtwork', album.coverArtworks[0]); } relations.albumCommentaryEntries = @@ -55,32 +73,28 @@ export default { .map(entry => relation('generateCommentaryEntry', entry)); } - const tracksWithCommentary = - album.tracks - .filter(({commentary}) => commentary); - relations.trackCommentaryHeadings = - tracksWithCommentary + query.tracksWithCommentary .map(() => relation('generateContentHeading')); relations.trackCommentaryLinks = - tracksWithCommentary + query.tracksWithCommentary .map(track => relation('linkTrack', track)); relations.trackCommentaryListeningLinks = - tracksWithCommentary + query.tracksWithCommentary .map(track => track.urls.map(url => relation('linkExternal', url))); relations.trackCommentaryCovers = - tracksWithCommentary + query.tracksWithCommentary .map(track => (track.hasUniqueCoverArt - ? relation('generateTrackCoverArtwork', track) + ? relation('generateCoverArtwork', track.trackArtworks[0]) : null)); relations.trackCommentaryEntries = - tracksWithCommentary + query.tracksWithCommentary .map(track => track.commentary .map(entry => relation('generateCommentaryEntry', entry))); @@ -88,40 +102,36 @@ export default { return relations; }, - data(album) { + data(query, album) { const data = {}; data.name = album.name; data.color = album.color; - - const tracksWithCommentary = - album.tracks - .filter(({commentary}) => commentary); - - const thingsWithCommentary = - (album.commentary - ? [album, ...tracksWithCommentary] - : tracksWithCommentary); + data.date = album.date; data.entryCount = - thingsWithCommentary + query.thingsWithCommentary .flatMap(({commentary}) => commentary) .length; data.wordCount = - thingsWithCommentary + query.thingsWithCommentary .flatMap(({commentary}) => commentary) .map(({body}) => body) .join(' ') .split(' ') .length; + data.trackCommentaryTrackDates = + query.tracksWithCommentary + .map(track => track.dateFirstReleased); + data.trackCommentaryDirectories = - tracksWithCommentary + query.tracksWithCommentary .map(track => track.directory); data.trackCommentaryColors = - tracksWithCommentary + query.tracksWithCommentary .map(track => (track.color === album.color ? null @@ -141,26 +151,49 @@ export default { headingMode: 'sticky', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['long-content'], mainContent: [ html.tag('p', - language.$(pageCapsule, 'infoLine', { - words: - html.tag('b', - language.formatWordCount(data.wordCount, {unit: true})), - - entries: - html.tag('b', - language.countCommentaryEntries(data.entryCount, {unit: true})), - })), + {[html.joinChildren]: html.tag('br')}, + + [ + data.date && + data.entryCount >= 1 && + language.$('releaseInfo.albumReleased', { + date: + html.tag('b', + language.formatDate(data.date)), + }), + + language.encapsulate(pageCapsule, 'infoLine', workingCapsule => { + const workingOptions = {}; + + if (data.entryCount >= 1) { + workingOptions.words = + html.tag('b', + language.formatWordCount(data.wordCount, {unit: true})); + + workingOptions.entries = + html.tag('b', + language.countCommentaryEntries(data.entryCount, {unit: true})); + } + + if (data.entryCount === 0) { + workingCapsule += '.withoutCommentary'; + } + + return language.$(workingCapsule, workingOptions); + }) + ]), relations.albumCommentaryEntries && language.encapsulate(pageCapsule, 'entry', entryCapsule => [ language.encapsulate(entryCapsule, 'title.albumCommentary', titleCapsule => relations.albumCommentaryHeading.slots({ tag: 'h3', + attributes: {id: 'album-commentary'}, color: data.color, title: @@ -200,6 +233,7 @@ export default { cover: relations.trackCommentaryCovers, entries: relations.trackCommentaryEntries, color: data.trackCommentaryColors, + trackDate: data.trackCommentaryTrackDates, }).map(({ heading, link, @@ -208,6 +242,7 @@ export default { cover, entries, color, + trackDate, }) => language.encapsulate(pageCapsule, 'entry', entryCapsule => [ language.encapsulate(entryCapsule, 'title.trackCommentary', titleCapsule => @@ -231,7 +266,17 @@ export default { }), })), - cover?.slots({mode: 'commentary'}), + cover?.slots({ + mode: 'commentary', + color: true, + }), + + trackDate && + trackDate !== data.date && + html.tag('p', {class: 'track-info'}, + language.$('releaseInfo.trackReleased', { + date: language.formatDate(trackDate), + })), entries.map(entry => entry.slot('color', color)), ])), @@ -254,6 +299,11 @@ export default { }, ], + secondaryNav: + relations.secondaryNav.slots({ + alwaysVisible: true, + }), + leftSidebar: relations.sidebar, })), }; diff --git a/src/content/dependencies/generateAlbumCommentarySidebar.js b/src/content/dependencies/generateAlbumCommentarySidebar.js index 435860cb..9ecec66d 100644 --- a/src/content/dependencies/generateAlbumCommentarySidebar.js +++ b/src/content/dependencies/generateAlbumCommentarySidebar.js @@ -1,3 +1,5 @@ +import {empty} from '#sugar'; + export default { contentDependencies: [ 'generateAlbumSidebarTrackSection', @@ -6,7 +8,7 @@ export default { 'linkAlbum', ], - extraDependencies: ['html'], + extraDependencies: ['html', 'language'], relations: (relation, album) => ({ sidebar: @@ -26,22 +28,46 @@ export default { trackSection)), }), - generate: (relations, {html}) => - relations.sidebar.slots({ - stickyMode: 'column', - boxes: [ - relations.sidebarBox.slots({ - attributes: {class: 'commentary-track-list-sidebar-box'}, - content: [ - html.tag('h1', relations.albumLink), - relations.trackSections.map(section => - section.slots({ - anchor: true, - open: true, - mode: 'commentary', - })), - ], - }), - ] - }), + data: (album) => ({ + albumHasCommentary: + !empty(album.commentary), + + anyTrackHasCommentary: + album.tracks.some(track => !empty(track.commentary)), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('albumCommentaryPage', pageCapsule => + relations.sidebar.slots({ + stickyMode: 'column', + boxes: [ + relations.sidebarBox.slots({ + attributes: {class: 'commentary-track-list-sidebar-box'}, + content: [ + html.tag('h1', relations.albumLink), + + html.tag('p', {[html.onlyIfContent]: true}, + language.encapsulate(pageCapsule, 'sidebar', workingCapsule => { + if (data.anyTrackHasCommentary) return html.blank(); + + if (data.albumHasCommentary) { + workingCapsule += '.noTrackCommentary'; + } else { + workingCapsule += '.noCommentary'; + } + + return language.$(workingCapsule); + })), + + data.anyTrackHasCommentary && + relations.trackSections.map(section => + section.slots({ + anchor: true, + open: true, + mode: 'commentary', + })), + ], + }), + ] + })), } diff --git a/src/content/dependencies/generateAlbumCoverArtwork.js b/src/content/dependencies/generateAlbumCoverArtwork.js deleted file mode 100644 index dbb22fe7..00000000 --- a/src/content/dependencies/generateAlbumCoverArtwork.js +++ /dev/null @@ -1,26 +0,0 @@ -export default { - contentDependencies: ['generateCoverArtwork'], - - relations: (relation, album) => ({ - coverArtwork: - relation('generateCoverArtwork', album.artTags), - }), - - data: (album) => ({ - path: - ['media.albumCover', album.directory, album.coverArtFileExtension], - - color: - album.color, - - dimensions: - album.coverArtDimensions, - }), - - generate: (data, relations) => - relations.coverArtwork.slots({ - path: data.path, - color: data.color, - dimensions: data.dimensions, - }), -}; diff --git a/src/content/dependencies/generateAlbumGalleryAlbumGrid.js b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js new file mode 100644 index 00000000..7f152871 --- /dev/null +++ b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js @@ -0,0 +1,90 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateCoverGrid', + 'image', + 'linkAlbum', + ], + + extraDependencies: ['html', 'language'], + + query: (album) => ({ + artworks: + (album.hasCoverArt + ? album.coverArtworks + : []), + }), + + relations: (relation, query, album) => ({ + coverGrid: + relation('generateCoverGrid'), + + albumLinks: + query.artworks.map(_artwork => + relation('linkAlbum', album)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + }), + + data: (query, album) => ({ + albumName: + album.name, + + artworkLabels: + query.artworks + .map(artwork => artwork.label), + + artworkArtists: + query.artworks + .map(artwork => artwork.artistContribs + .map(contrib => contrib.artist.name)), + }), + + slots: { + attributes: {type: 'attributes', mutable: false}, + }, + + generate: (data, relations, slots, {html, language}) => + html.tag('div', + {[html.onlyIfContent]: true}, + + slots.attributes, + + [ + relations.coverArtistsLine, + + relations.coverGrid.slots({ + links: + relations.albumLinks, + + names: + data.artworkLabels + .map(label => label ?? data.albumName), + + images: + stitchArrays({ + image: relations.images, + label: data.artworkLabels, + }).map(({image, label}) => + image.slots({ + missingSourceContent: + language.$('misc.albumGalleryGrid.noCoverArt', { + name: + label ?? data.albumName, + }), + })), + + info: + data.artworkArtists.map(artists => + language.$('misc.coverGrid.details.coverArtists', { + [language.onlyIfOptions]: ['artists'], + + artists: + language.formatUnitList(artists), + })), + }), + ]), +}; diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index 44d49c54..516a7ca8 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -1,18 +1,18 @@ -import {compareArrays, stitchArrays} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; +import {getKebabCase} from '#wiki-data'; export default { contentDependencies: [ - 'generateAlbumGalleryCoverArtistsLine', + 'generateAlbumGalleryAlbumGrid', 'generateAlbumGalleryNoTrackArtworksLine', 'generateAlbumGalleryStatsLine', + 'generateAlbumGalleryTrackGrid', 'generateAlbumNavAccent', 'generateAlbumSecondaryNav', - 'generateAlbumStyleRules', - 'generateCoverGrid', + 'generateAlbumStyleTags', + 'generateIntrapageDotSwitcher', 'generatePageLayout', - 'image', 'linkAlbum', - 'linkTrack', ], extraDependencies: ['html', 'language'], @@ -20,147 +20,82 @@ export default { query(album) { const query = {}; - const tracksWithUniqueCoverArt = + const trackArtworkLabels = album.tracks - .filter(track => track.hasUniqueCoverArt); - - // Don't display "all artwork by..." for albums where there's - // only one unique artwork in the first place. - if (tracksWithUniqueCoverArt.length > 1) { - const allCoverArtistArrays = - tracksWithUniqueCoverArt - .map(track => track.coverArtistContribs) - .map(contribs => contribs.map(contrib => contrib.artist)); - - const allSameCoverArtists = - allCoverArtistArrays - .slice(1) - .every(artists => compareArrays(artists, allCoverArtistArrays[0])); - - if (allSameCoverArtists) { - query.coverArtistsForAllTracks = - allCoverArtistArrays[0]; - } - } + .map(track => track.trackArtworks + .map(artwork => artwork.label)); + + const recurranceThreshold = 2; + + // This list may include null, if some artworks are not labelled! + // That's expected. + query.recurringTrackArtworkLabels = + unique(trackArtworkLabels.flat()) + .filter(label => + trackArtworkLabels + .filter(labels => labels.includes(label)) + .length >= + (label === null + ? 1 + : recurranceThreshold)); return query; }, - relations(relation, query, album) { - const relations = {}; + relations: (relation, query, album) => ({ + layout: + relation('generatePageLayout'), - relations.layout = - relation('generatePageLayout'); + albumStyleTags: + relation('generateAlbumStyleTags', album, null), - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); - - relations.albumLink = - relation('linkAlbum', album); - - relations.albumNavAccent = - relation('generateAlbumNavAccent', album, null); - - relations.secondaryNav = - relation('generateAlbumSecondaryNav', album); - - relations.statsLine = - relation('generateAlbumGalleryStatsLine', album); - - if (album.tracks.every(track => !track.hasUniqueCoverArt)) { - relations.noTrackArtworksLine = - relation('generateAlbumGalleryNoTrackArtworksLine'); - } - - if (query.coverArtistsForAllTracks) { - relations.coverArtistsLine = - relation('generateAlbumGalleryCoverArtistsLine', query.coverArtistsForAllTracks); - } - - relations.coverGrid = - relation('generateCoverGrid'); - - relations.links = [ + albumLink: relation('linkAlbum', album), - ... - album.tracks - .map(track => relation('linkTrack', track)), - ]; - - relations.images = [ - (album.hasCoverArt - ? relation('image', album.artTags) - : relation('image')), + albumNavAccent: + relation('generateAlbumNavAccent', album, null), - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? relation('image', track.artTags) - : relation('image'))), - ]; - - return relations; - }, + secondaryNav: + relation('generateAlbumSecondaryNav', album), - data(query, album) { - const data = {}; + statsLine: + relation('generateAlbumGalleryStatsLine', album), - data.name = album.name; - data.color = album.color; - - data.names = [ - album.name, - ...album.tracks.map(track => track.name), - ]; - - data.coverArtists = [ - (album.hasCoverArt - ? album.coverArtistContribs.map(({artist}) => artist.name) + noTrackArtworksLine: + (album.tracks.every(track => !track.hasUniqueCoverArt) + ? relation('generateAlbumGalleryNoTrackArtworksLine') : null), - ... - album.tracks.map(track => { - if (query.coverArtistsForAllTracks) { - return null; - } + setSwitcher: + relation('generateIntrapageDotSwitcher'), - if (track.hasUniqueCoverArt) { - return track.coverArtistContribs.map(({artist}) => artist.name); - } + albumGrid: + relation('generateAlbumGalleryAlbumGrid', album), - return null; - }), - ]; - - data.paths = [ - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null), + trackGrids: + query.recurringTrackArtworkLabels.map(label => + relation('generateAlbumGalleryTrackGrid', album, label)), + }), - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : null)), - ]; + data: (query, album) => ({ + trackGridLabels: + query.recurringTrackArtworkLabels, - data.dimensions = [ - (album.hasCoverArt - ? album.coverArtDimensions - : null), + trackGridIDs: + query.recurringTrackArtworkLabels.map(label => + 'track-grid-' + + (label + ? getKebabCase(label) + : 'no-label')), - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? track.coverArtDimensions - : null)), - ]; + name: + album.name, - return data; - }, + color: + album.color, + }), - generate: (data, relations, {language}) => + generate: (data, relations, {html, language}) => language.encapsulate('albumGalleryPage', pageCapsule => relations.layout.slots({ title: @@ -171,39 +106,44 @@ export default { headingMode: 'static', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['top-index'], mainContent: [ relations.statsLine, - relations.coverArtistsLine, + + relations.albumGrid, + relations.noTrackArtworksLine, - relations.coverGrid - .slots({ - links: relations.links, - names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - name: data.names, - }).map(({image, path, dimensions, name}) => - image.slots({ - path, - dimensions, - missingSourceContent: - language.$('misc.albumGalleryGrid.noCoverArt', {name}), - })), - info: - data.coverArtists.map(names => - (names === null - ? null - : language.$('misc.albumGrid.details.coverArtists', { - artists: language.formatUnitList(names), - }))), - }), + data.trackGridLabels.some(value => value !== null) && + html.tag('p', {class: 'gallery-set-switcher'}, + language.encapsulate(pageCapsule, 'setSwitcher', switcherCapsule => + language.$(switcherCapsule, { + sets: + relations.setSwitcher.slots({ + initialOptionIndex: 0, + + titles: + data.trackGridLabels.map(label => + label ?? + language.$(switcherCapsule, 'unlabeledSet')), + + targetIDs: + data.trackGridIDs, + }), + }))), + + stitchArrays({ + grid: relations.trackGrids, + id: data.trackGridIDs, + }).map(({grid, id}, index) => + grid.slots({ + attributes: [ + {id}, + index >= 1 && {style: 'display: none'}, + ], + })), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateAlbumGalleryTrackGrid.js b/src/content/dependencies/generateAlbumGalleryTrackGrid.js new file mode 100644 index 00000000..fb5ed7ea --- /dev/null +++ b/src/content/dependencies/generateAlbumGalleryTrackGrid.js @@ -0,0 +1,122 @@ +import {compareArrays, stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateAlbumGalleryCoverArtistsLine', + 'generateCoverGrid', + 'image', + 'linkAlbum', + 'linkTrack', + ], + + extraDependencies: ['html', 'language'], + + query(album, label) { + const query = {}; + + query.artworks = + album.tracks.map(track => + track.trackArtworks.find(artwork => artwork.label === label) ?? + null); + + const presentArtworks = + query.artworks.filter(Boolean); + + if (presentArtworks.length > 1) { + const allArtistArrays = + presentArtworks + .map(artwork => artwork.artistContribs + .map(contrib => contrib.artist)); + + const allSameArtists = + allArtistArrays + .slice(1) + .every(artists => compareArrays(artists, allArtistArrays[0])); + + if (allSameArtists) { + query.artistsForAllTrackArtworks = + allArtistArrays[0]; + } + } + + return query; + }, + + relations: (relation, query, album, _label) => ({ + coverArtistsLine: + (query.artistsForAllTrackArtworks + ? relation('generateAlbumGalleryCoverArtistsLine', + query.artistsForAllTrackArtworks) + : null), + + coverGrid: + relation('generateCoverGrid'), + + albumLink: + relation('linkAlbum', album), + + trackLinks: + album.tracks + .map(track => relation('linkTrack', track)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + }), + + data: (query, album, _label) => ({ + trackNames: + album.tracks + .map(track => track.name), + + artworkArtists: + query.artworks.map(artwork => + (query.artistsForAllTrackArtworks + ? null + : artwork + ? artwork.artistContribs + .map(contrib => contrib.artist.name) + : null)), + }), + + slots: { + attributes: {type: 'attributes', mutable: false}, + }, + + generate: (data, relations, slots, {html, language}) => + html.tag('div', + {[html.onlyIfContent]: true}, + + slots.attributes, + + [ + relations.coverArtistsLine, + + relations.coverGrid.slots({ + links: + relations.trackLinks, + + names: + data.trackNames, + + images: + stitchArrays({ + image: relations.images, + name: data.trackNames, + }).map(({image, name}) => + image.slots({ + missingSourceContent: + language.$('misc.albumGalleryGrid.noCoverArt', {name}), + })), + + info: + data.artworkArtists.map(artists => + language.$('misc.coverGrid.details.coverArtists', { + [language.onlyIfOptions]: ['artists'], + + artists: + language.formatUnitList(artists), + })), + }), + ]), +}; diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index 1bffe2d0..1664c788 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -1,16 +1,20 @@ +import {empty} from '#sugar'; + export default { contentDependencies: [ - 'generateAlbumAdditionalFilesList', + 'generateAdditionalFilesList', + 'generateAdditionalNamesBox', + 'generateAlbumArtworkColumn', 'generateAlbumBanner', - 'generateAlbumCoverArtwork', 'generateAlbumNavAccent', 'generateAlbumReleaseInfo', 'generateAlbumSecondaryNav', 'generateAlbumSidebar', 'generateAlbumSocialEmbed', - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateAlbumTrackList', - 'generateCommentarySection', + 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generatePageLayout', 'linkAlbumCommentary', @@ -23,8 +27,8 @@ export default { layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), socialEmbed: relation('generateAlbumSocialEmbed', album), @@ -38,10 +42,11 @@ export default { sidebar: relation('generateAlbumSidebar', album, null), - cover: - (album.hasCoverArt - ? relation('generateAlbumCoverArtwork', album) - : null), + additionalNamesBox: + relation('generateAdditionalNamesBox', album.additionalNames), + + artworkColumn: + relation('generateAlbumArtworkColumn', album), banner: (album.hasBannerArt @@ -51,6 +56,9 @@ export default { contentHeading: relation('generateContentHeading'), + contentContentHeading: + relation('generateContentContentHeading', album), + releaseInfo: relation('generateAlbumReleaseInfo', album), @@ -60,7 +68,7 @@ export default { : null), commentaryLink: - (album.commentary || album.tracks.some(t => t.commentary) + ([album, ...album.tracks].some(({commentary}) => !empty(commentary)) ? relation('linkAlbumCommentary', album) : null), @@ -68,12 +76,15 @@ export default { relation('generateAlbumTrackList', album), additionalFilesList: - relation('generateAlbumAdditionalFilesList', - album, - album.additionalFiles), + relation('generateAdditionalFilesList', album.additionalFiles), - artistCommentarySection: - relation('generateCommentarySection', album.commentary), + artistCommentaryEntries: + album.commentary + .map(entry => relation('generateCommentaryEntry', entry)), + + creditSourceEntries: + album.creditingSources + .map(entry => relation('generateCommentaryEntry', entry)), }), data: (album) => ({ @@ -97,14 +108,12 @@ export default { color: data.color, headingMode: 'sticky', - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, + + additionalNames: relations.additionalNamesBox, - cover: - relations.cover - ?.slots({ - alt: language.$('misc.alt.albumCover'), - }) - ?? null, + artworkColumnContent: + relations.artworkColumn, mainContent: [ relations.releaseInfo, @@ -150,6 +159,15 @@ export default { })) : html.blank()), + + !html.isBlank(relations.creditSourceEntries) && + language.encapsulate(capsule, 'readCreditingSources', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#crediting-sources'}, + language.$(capsule, 'link')), + })), ])), relations.trackList, @@ -165,6 +183,11 @@ export default { }), ])), + (!html.isBlank(relations.artistCommentaryEntries) || + !html.isBlank(relations.creditSourceEntries)) + && + html.tag('hr', {class: 'main-separator'}), + language.encapsulate('releaseInfo.additionalFiles', capsule => html.tags([ relations.contentHeading.clone() @@ -176,7 +199,25 @@ export default { relations.additionalFilesList, ])), - relations.artistCommentarySection, + html.tags([ + relations.contentContentHeading.clone() + .slots({ + attributes: {id: 'artist-commentary'}, + string: 'misc.artistCommentary', + }), + + relations.artistCommentaryEntries, + ]), + + html.tags([ + relations.contentContentHeading.clone() + .slots({ + attributes: {id: 'crediting-sources'}, + string: 'misc.creditingSources', + }), + + relations.creditSourceEntries, + ]), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index 4b6fb062..432c5f3d 100644 --- a/src/content/dependencies/generateAlbumNavAccent.js +++ b/src/content/dependencies/generateAlbumNavAccent.js @@ -1,8 +1,10 @@ -import {empty} from '#sugar'; +import {atOffset, empty} from '#sugar'; export default { contentDependencies: [ - 'generatePreviousNextLinks', + 'generateInterpageDotSwitcher', + 'generateNextLink', + 'generatePreviousLink', 'linkTrack', 'linkAlbumCommentary', 'linkAlbumGallery', @@ -10,47 +12,68 @@ export default { extraDependencies: ['html', 'language'], - relations(relation, album, track) { - const relations = {}; + query(album, track) { + const query = {}; - relations.previousNextLinks = - relation('generatePreviousNextLinks'); + const index = + (track + ? album.tracks.indexOf(track) + : null); - relations.previousTrackLink = null; - relations.nextTrackLink = null; + query.previousTrack = + (track + ? atOffset(album.tracks, index, -1) + : null); - if (track) { - const index = album.tracks.indexOf(track); + query.nextTrack = + (track + ? atOffset(album.tracks, index, +1) + : null); - if (index > 0) { - relations.previousTrackLink = - relation('linkTrack', album.tracks[index - 1]); - } + return query; + }, - if (index < album.tracks.length - 1) { - relations.nextTrackLink = - relation('linkTrack', album.tracks[index + 1]); - } - } + relations: (relation, query, album, _track) => ({ + switcher: + relation('generateInterpageDotSwitcher'), - relations.albumGalleryLink = - relation('linkAlbumGallery', album); + previousLink: + relation('generatePreviousLink'), - if (album.commentary || album.tracks.some(t => t.commentary)) { - relations.albumCommentaryLink = - relation('linkAlbumCommentary', album); - } + nextLink: + relation('generateNextLink'), - return relations; - }, + previousTrackLink: + (query.previousTrack + ? relation('linkTrack', query.previousTrack) + : null), - data(album, track) { - return { - hasMultipleTracks: album.tracks.length > 1, - galleryIsStub: album.tracks.every(t => !t.hasUniqueCoverArt), - isTrackPage: !!track, - }; - }, + nextTrackLink: + (query.nextTrack + ? relation('linkTrack', query.nextTrack) + : null), + + albumGalleryLink: + relation('linkAlbumGallery', album), + + albumCommentaryLink: + relation('linkAlbumCommentary', album), + }), + + data: (query, album, track) => ({ + hasMultipleTracks: + album.tracks.length > 1, + + commentaryPageIsStub: + [album, ...album.tracks] + .every(({commentary}) => empty(commentary)), + + galleryIsStub: + album.tracks.every(t => !t.hasUniqueCoverArt), + + isTrackPage: + !!track, + }), slots: { showTrackNavigation: {type: 'boolean', default: false}, @@ -65,32 +88,29 @@ export default { 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.$(albumNavCapsule, 'gallery'), - }), - - relations.albumCommentaryLink?.slots({ - attributes: {class: slots.currentExtra === 'commentary' && 'current'}, - content: language.$(albumNavCapsule, 'commentary'), - }), - ]}; - - const {content: previousNextLinks = []} = - slots.showTrackNavigation && + const previousLink = data.isTrackPage && - data.hasMultipleTracks && - relations.previousNextLinks.slots({ - previousLink: relations.previousTrackLink, - nextLink: relations.nextTrackLink, + relations.previousLink.slot('link', relations.previousTrackLink); + + const nextLink = + data.isTrackPage && + relations.nextLink.slot('link', relations.nextTrackLink); + + const galleryLink = + (!data.galleryIsStub || slots.currentExtra === 'gallery') && + relations.albumGalleryLink.slots({ + attributes: {class: slots.currentExtra === 'gallery' && 'current'}, + content: language.$(albumNavCapsule, 'gallery'), + }); + + const commentaryLink = + (!data.commentaryPageIsStub || slots.currentExtra === 'commentary') && + relations.albumCommentaryLink.slots({ + attributes: {class: slots.currentExtra === 'commentary' && 'current'}, + content: language.$(albumNavCapsule, 'commentary'), }); const randomLink = - slots.showTrackNavigation && data.hasMultipleTracks && html.tag('a', {id: 'random-button'}, @@ -100,16 +120,23 @@ export default { ? language.$(trackNavCapsule, 'random') : language.$(albumNavCapsule, 'randomTrack'))); - const allLinks = [ - ...previousNextLinks, - ...extraLinks, - randomLink, - ].filter(Boolean); + return relations.switcher.slots({ + links: [ + slots.showTrackNavigation && + previousLink, + + slots.showTrackNavigation && + nextLink, + + slots.showExtraLinks && + galleryLink, - if (empty(allLinks)) { - return html.blank(); - } + slots.showExtraLinks && + commentaryLink, - return `(${language.formatUnitList(allLinks)})`; + slots.showTrackNavigation && + randomLink, + ], + }); }, }; diff --git a/src/content/dependencies/generateAlbumReferencedArtworksPage.js b/src/content/dependencies/generateAlbumReferencedArtworksPage.js new file mode 100644 index 00000000..52c78dc2 --- /dev/null +++ b/src/content/dependencies/generateAlbumReferencedArtworksPage.js @@ -0,0 +1,58 @@ +export default { + contentDependencies: [ + 'generateAlbumStyleTags', + 'generateBackToAlbumLink', + 'generateReferencedArtworksPage', + 'linkAlbum', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, album) => ({ + page: + relation('generateReferencedArtworksPage', album.coverArtworks[0]), + + albumStyleTags: + relation('generateAlbumStyleTags', album, null), + + albumLink: + relation('linkAlbum', album), + + backToAlbumLink: + relation('generateBackToAlbumLink', album), + }), + + data: (album) => ({ + name: + album.name, + }), + + generate: (data, relations, {html, language}) => + relations.page.slots({ + title: + language.$('albumPage.title', { + album: + data.name, + }), + + styleTags: relations.albumStyleTags, + + navLinks: [ + {auto: 'home'}, + + { + html: + relations.albumLink + .slot('attributes', {class: 'current'}), + + accent: + html.tag('a', {href: ''}, + {class: 'current'}, + + language.$('referencedArtworksPage.subtitle')), + }, + ], + + navBottomRowContent: relations.backToAlbumLink, + }), +}; diff --git a/src/content/dependencies/generateAlbumReferencingArtworksPage.js b/src/content/dependencies/generateAlbumReferencingArtworksPage.js new file mode 100644 index 00000000..bc36ae06 --- /dev/null +++ b/src/content/dependencies/generateAlbumReferencingArtworksPage.js @@ -0,0 +1,58 @@ +export default { + contentDependencies: [ + 'generateAlbumStyleTags', + 'generateBackToAlbumLink', + 'generateReferencingArtworksPage', + 'linkAlbum', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, album) => ({ + page: + relation('generateReferencingArtworksPage', album.coverArtworks[0]), + + albumStyleTags: + relation('generateAlbumStyleTags', album, null), + + albumLink: + relation('linkAlbum', album), + + backToAlbumLink: + relation('generateBackToAlbumLink', album), + }), + + data: (album) => ({ + name: + album.name, + }), + + generate: (data, relations, {html, language}) => + relations.page.slots({ + title: + language.$('albumPage.title', { + album: + data.name, + }), + + styleTags: relations.albumStyleTags, + + navLinks: [ + {auto: 'home'}, + + { + html: + relations.albumLink + .slot('attributes', {class: 'current'}), + + accent: + html.tag('a', {href: ''}, + {class: 'current'}, + + language.$('referencingArtworksPage.subtitle')), + }, + ], + + navBottomRowContent: relations.backToAlbumLink, + }), +}; diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js index 28227f45..2a958244 100644 --- a/src/content/dependencies/generateAlbumReleaseInfo.js +++ b/src/content/dependencies/generateAlbumReleaseInfo.js @@ -3,7 +3,7 @@ import {accumulateSum, empty} from '#sugar'; export default { contentDependencies: [ 'generateReleaseInfoContributionsLine', - 'linkExternal', + 'generateReleaseInfoListenLine', ], extraDependencies: ['html', 'language'], @@ -14,18 +14,8 @@ export default { relations.artistContributionsLine = relation('generateReleaseInfoContributionsLine', album.artistContribs); - relations.coverArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.coverArtistContribs); - - relations.wallpaperArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.wallpaperArtistContribs); - - relations.bannerArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs); - - relations.externalLinks = - album.urls.map(url => - relation('linkExternal', url)); + relations.listenLine = + relation('generateReleaseInfoListenLine', album); return relations; }, @@ -69,34 +59,15 @@ export default { [ relations.artistContributionsLine.slots({ stringKey: capsule + '.by', + featuringStringKey: capsule + '.by.featuring', chronologyKind: 'album', }), - relations.coverArtistContributionsLine.slots({ - stringKey: capsule + '.coverArtBy', - chronologyKind: 'coverArt', - }), - - relations.wallpaperArtistContributionsLine.slots({ - stringKey: capsule + '.wallpaperArtBy', - chronologyKind: 'wallpaperArt', - }), - - relations.bannerArtistContributionsLine.slots({ - stringKey: capsule + '.bannerArtBy', - chronologyKind: 'bannerArt', - }), - language.$(capsule, 'released', { [language.onlyIfOptions]: ['date'], date: language.formatDate(data.date), }), - language.$(capsule, 'artReleased', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.coverArtDate), - }), - language.$(capsule, 'duration', { [language.onlyIfOptions]: ['duration'], duration: @@ -109,21 +80,16 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, - language.$(capsule, 'listenOn', { - [language.onlyIfOptions]: ['links'], - - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => - link.slot('context', [ - 'album', - (data.numTracks === 0 - ? 'albumNoTracks' - : data.numTracks === 1 - ? 'albumOneTrack' - : 'albumMultipleTracks'), - ]))), + relations.listenLine.slots({ + context: [ + 'album', + + (data.numTracks === 0 + ? 'albumNoTracks' + : data.numTracks === 1 + ? 'albumOneTrack' + : 'albumMultipleTracks'), + ], })), ])), }; diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js index d6ff8a0a..bfa48f03 100644 --- a/src/content/dependencies/generateAlbumSecondaryNav.js +++ b/src/content/dependencies/generateAlbumSecondaryNav.js @@ -1,165 +1,127 @@ -import {sortChronologically} from '#sort'; -import {atOffset, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { contentDependencies: [ - 'generateColorStyleAttribute', - 'generatePreviousNextLinks', + 'generateAlbumSecondaryNavGroupPart', + 'generateAlbumSecondaryNavSeriesPart', + 'generateDotSwitcherTemplate', 'generateSecondaryNav', - 'linkAlbumDynamically', - 'linkGroup', - 'linkTrack', ], - extraDependencies: ['html', 'language'], + extraDependencies: ['html', 'wikiData'], - query(album) { + sprawl: ({groupData}) => ({ + // TODO: Series aren't their own things, so we access them weirdly. + seriesData: + groupData.flatMap(group => group.serieses), + }), + + query(sprawl, album) { const query = {}; query.groups = album.groups; - if (album.date) { - // Sort by latest first. This matches the sorting order used on group - // gallery pages, ensuring that previous/next matches moving up/down - // the gallery. Note that this makes the index offsets "backwards" - // compared to how latest-last chronological lists are accessed. - const groupAlbums = - query.groups.map(group => - sortChronologically( - group.albums.filter(album => album.date), - {latestFirst: true})); - - const groupCurrentIndex = - groupAlbums.map(albums => - albums.indexOf(album)); - - query.groupPreviousAlbum = - stitchArrays({ - albums: groupAlbums, - index: groupCurrentIndex, - }).map(({albums, index}) => - atOffset(albums, index, +1)); - - query.groupNextAlbum = - stitchArrays({ - albums: groupAlbums, - index: groupCurrentIndex, - }).map(({albums, index}) => - atOffset(albums, index, -1)); - } + query.groupSerieses = + query.groups + .map(group => + group.serieses + .filter(series => series.albums.includes(album))); + + query.disconnectedSerieses = + sprawl.seriesData + .filter(series => + series.albums.includes(album) && + !query.groups.includes(series.group)); return query; }, - relations(relation, query, album) { - const relations = {}; + relations: (relation, query, _sprawl, album) => ({ + secondaryNav: + relation('generateSecondaryNav'), - relations.secondaryNav = - relation('generateSecondaryNav'); + // Just use a generic dot switcher here. We want the common behavior, + // but the "options" may each contain multiple links (group + series), + // so this is a different use than typical interpage dot switchers. + switcher: + relation('generateDotSwitcherTemplate'), - relations.groupLinks = + groupParts: query.groups - .map(group => relation('linkGroup', group)); - - relations.colorStyles = - query.groups - .map(group => relation('generateColorStyleAttribute', group.color)); - - if (album.date) { - relations.previousNextLinks = - stitchArrays({ - previousAlbum: query.groupPreviousAlbum, - nextAlbum: query.groupNextAlbum - }).map(({previousAlbum, nextAlbum}) => - (previousAlbum || nextAlbum - ? relation('generatePreviousNextLinks') - : null)); - - relations.previousAlbumLinks = - query.groupPreviousAlbum.map(previousAlbum => - (previousAlbum - ? relation('linkAlbumDynamically', previousAlbum) - : null)); - - relations.nextAlbumLinks = - query.groupNextAlbum.map(nextAlbum => - (nextAlbum - ? relation('linkAlbumDynamically', nextAlbum) - : null)); - } - - return relations; - }, + .map(group => + relation('generateAlbumSecondaryNavGroupPart', + group, + album)), + + seriesParts: + query.groupSerieses + .map(serieses => serieses + .map(series => + relation('generateAlbumSecondaryNavSeriesPart', + series, + album))), + + disconnectedSeriesParts: + query.disconnectedSerieses + .map(series => + relation('generateAlbumSecondaryNavSeriesPart', + series, + album)), + }), slots: { mode: { validate: v => v.is('album', 'track'), default: 'album', }, + + alwaysVisible: { + type: 'boolean', + default: false, + }, }, - generate(relations, slots, {html, language}) { - const navLinksShouldShowPreviousNext = - (slots.mode === 'track' - ? Array.from(relations.previousNextLinks ?? [], () => false) - : stitchArrays({ - previousAlbumLink: relations.previousAlbumLinks ?? null, - nextAlbumLink: relations.nextAlbumLinks ?? null, - }).map(({previousAlbumLink, nextAlbumLink}) => - previousAlbumLink || - nextAlbumLink)); - - const navLinkPreviousNextLinks = - stitchArrays({ - showPreviousNext: navLinksShouldShowPreviousNext, - previousNextLinks: relations.previousNextLinks ?? null, - previousAlbumLink: relations.previousAlbumLinks ?? null, - nextAlbumLink: relations.nextAlbumLinks ?? null, - }).map(({ - showPreviousNext, - previousNextLinks, - previousAlbumLink, - nextAlbumLink, - }) => - (showPreviousNext - ? previousNextLinks.slots({ - previousLink: previousAlbumLink, - nextLink: nextAlbumLink, - id: false, - }) - : null)); - - for (const groupLink of relations.groupLinks) { - groupLink.setSlot('color', false); - } - - const navLinkContents = + generate(relations, slots, {html}) { + const groupConnectedParts = stitchArrays({ - groupLink: relations.groupLinks, - previousNextLinks: navLinkPreviousNextLinks, - }).map(({groupLink, previousNextLinks}) => [ - language.$('albumSidebar.groupBox.title', { - group: groupLink, - }), - - previousNextLinks && - `(${language.formatUnitList(previousNextLinks.content)})`, - ]); - - const navLinks = - stitchArrays({ - content: navLinkContents, - colorStyle: relations.colorStyles, - }).map(({content, colorStyle}) => - html.tag('span', {class: 'nav-link'}, - colorStyle.slot('context', 'primary-only'), - - content)); + groupPart: relations.groupParts, + seriesParts: relations.seriesParts, + }).map(({groupPart, seriesParts}) => { + for (const part of [groupPart, ...seriesParts]) { + part.setSlot('mode', slots.mode); + } + + if (html.isBlank(seriesParts)) { + return groupPart; + } else { + return ( + html.tag('span', {class: 'group-with-series'}, + {[html.joinChildren]: ''}, + + [groupPart, ...seriesParts])); + } + }); + + const allParts = [ + ...relations.disconnectedSeriesParts, + ...groupConnectedParts, + ]; return relations.secondaryNav.slots({ - class: 'nav-links-groups', - content: navLinks, + alwaysVisible: slots.alwaysVisible, + + attributes: [ + {class: 'album-secondary-nav'}, + + slots.mode === 'album' && + {class: 'with-previous-next'}, + ], + + content: + (slots.mode === 'album' + ? allParts + : relations.switcher.slot('options', allParts)), }); }, }; diff --git a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js new file mode 100644 index 00000000..22dfa51c --- /dev/null +++ b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js @@ -0,0 +1,94 @@ +import {sortChronologically} from '#sort'; +import {atOffset} from '#sugar'; + +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generateSecondaryNavParentSiblingsPart', + 'linkAlbumDynamically', + 'linkGroup', + ], + + extraDependencies: ['html'], + + query(group, album) { + const query = {}; + + if (album.date) { + // Sort by latest first. This matches the sorting order used on group + // gallery pages, ensuring that previous/next matches moving up/down + // the gallery. Note that this makes the index offsets "backwards" + // compared to how latest-last chronological lists are accessed. + const albums = + sortChronologically( + group.albums.filter(album => album.date), + {latestFirst: true}); + + const currentIndex = + albums.indexOf(album); + + query.previousAlbum = + atOffset(albums, currentIndex, +1); + + query.nextAlbum = + atOffset(albums, currentIndex, -1); + } + + return query; + }, + + relations: (relation, query, group, _album) => ({ + parentSiblingsPart: + relation('generateSecondaryNavParentSiblingsPart'), + + groupLink: + relation('linkGroup', group), + + colorStyle: + relation('generateColorStyleAttribute', group.color), + + previousAlbumLink: + (query.previousAlbum + ? relation('linkAlbumDynamically', query.previousAlbum) + : null), + + nextAlbumLink: + (query.nextAlbum + ? relation('linkAlbumDynamically', query.nextAlbum) + : null), + }), + + slots: { + mode: { + validate: v => v.is('album', 'track'), + default: 'album', + }, + }, + + generate: (relations, slots) => + relations.parentSiblingsPart.slots({ + attributes: {class: 'group-nav-links'}, + + showPreviousNext: slots.mode === 'album', + + colorStyle: relations.colorStyle, + mainLink: relations.groupLink, + + previousLink: + (relations.previousAlbumLink + ? relations.previousAlbumLink.slots({ + linkCommentaryPages: true, + }) + : null), + + nextLink: + (relations.nextAlbumLink + ? relations.nextAlbumLink.slots({ + linkCommentaryPages: true, + }) + : null), + + stringsKey: 'albumSecondaryNav.group', + mainLinkOption: 'group', + }), +}; diff --git a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js new file mode 100644 index 00000000..16f205e3 --- /dev/null +++ b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js @@ -0,0 +1,94 @@ +import {atOffset} from '#sugar'; + +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generateSecondaryNavParentSiblingsPart', + 'linkAlbumDynamically', + 'linkGroup', + ], + + extraDependencies: ['html', 'language'], + + query(series, album) { + const query = {}; + + const albums = + series.albums; + + const currentIndex = + albums.indexOf(album); + + query.previousAlbum = + atOffset(albums, currentIndex, -1); + + query.nextAlbum = + atOffset(albums, currentIndex, +1); + + return query; + }, + + relations: (relation, query, series, _album) => ({ + parentSiblingsPart: + relation('generateSecondaryNavParentSiblingsPart'), + + groupLink: + relation('linkGroup', series.group), + + colorStyle: + relation('generateColorStyleAttribute', series.group.color), + + previousAlbumLink: + (query.previousAlbum + ? relation('linkAlbumDynamically', query.previousAlbum) + : null), + + nextAlbumLink: + (query.nextAlbum + ? relation('linkAlbumDynamically', query.nextAlbum) + : null), + }), + + data: (_query, series) => ({ + name: series.name, + }), + + slots: { + mode: { + validate: v => v.is('album', 'track'), + default: 'album', + }, + }, + + generate: (data, relations, slots, {language}) => + relations.parentSiblingsPart.slots({ + attributes: {class: 'series-nav-links'}, + + showPreviousNext: slots.mode === 'album', + + colorStyle: relations.colorStyle, + + mainLink: + relations.groupLink.slots({ + attributes: {class: 'series'}, + content: language.sanitize(data.name), + }), + + previousLink: + (relations.previousAlbumLink + ? relations.previousAlbumLink.slots({ + linkCommentaryPages: true, + }) + : null), + + nextLink: + (relations.nextAlbumLink + ? relations.nextAlbumLink.slots({ + linkCommentaryPages: true, + }) + : null), + + stringsKey: 'albumSecondaryNav.series', + mainLinkOption: 'series', + }), +}; diff --git a/src/content/dependencies/generateAlbumSidebar.js b/src/content/dependencies/generateAlbumSidebar.js index 355a9a9a..7cf689cc 100644 --- a/src/content/dependencies/generateAlbumSidebar.js +++ b/src/content/dependencies/generateAlbumSidebar.js @@ -1,12 +1,73 @@ +import {sortAlbumsTracksChronologically} from '#sort'; +import {stitchArrays, transposeArrays} from '#sugar'; + export default { contentDependencies: [ 'generateAlbumSidebarGroupBox', + 'generateAlbumSidebarSeriesBox', 'generateAlbumSidebarTrackListBox', 'generatePageSidebar', 'generatePageSidebarConjoinedBox', + 'generateTrackReleaseBox', ], - relations: (relation, album, track) => ({ + extraDependencies: ['html', 'wikiData'], + + sprawl: ({groupData}) => ({ + // TODO: Series aren't their own things, so we access them weirdly. + seriesData: + groupData.flatMap(group => group.serieses), + }), + + query(sprawl, album, track) { + const query = {}; + + query.groups = + album.groups; + + query.groupSerieses = + query.groups + .map(group => + group.serieses + .filter(series => series.albums.includes(album))); + + query.disconnectedSerieses = + sprawl.seriesData + .filter(series => + series.albums.includes(album) && + !query.groups.includes(series.group)); + + if (track) { + const albumTrackMap = + new Map(transposeArrays([ + track.allReleases.map(t => t.album), + track.allReleases, + ])); + + const allReleaseAlbums = + sortAlbumsTracksChronologically( + Array.from(albumTrackMap.keys())); + + const currentReleaseIndex = + allReleaseAlbums.indexOf(track.album); + + const earlierReleaseAlbums = + allReleaseAlbums.slice(0, currentReleaseIndex); + + const laterReleaseAlbums = + allReleaseAlbums.slice(currentReleaseIndex + 1); + + query.earlierReleaseTracks = + earlierReleaseAlbums.map(album => albumTrackMap.get(album)); + + query.laterReleaseTracks = + laterReleaseAlbums.map(album => albumTrackMap.get(album)); + } + + return query; + }, + + relations: (relation, query, _sprawl, album, track) => ({ sidebar: relation('generatePageSidebar'), @@ -17,31 +78,94 @@ export default { relation('generateAlbumSidebarTrackListBox', album, track), groupBoxes: - album.groups.map(group => - relation('generateAlbumSidebarGroupBox', album, group)), + query.groups + .map(group => + relation('generateAlbumSidebarGroupBox', album, group)), + + seriesBoxes: + query.groupSerieses + .map(serieses => serieses + .map(series => + relation('generateAlbumSidebarSeriesBox', album, series))), + + disconnectedSeriesBoxes: + query.disconnectedSerieses + .map(series => + relation('generateAlbumSidebarSeriesBox', album, series)), + + earlierTrackReleaseBoxes: + (track + ? query.earlierReleaseTracks + .map(track => + relation('generateTrackReleaseBox', track)) + : null), + + laterTrackReleaseBoxes: + (track + ? query.laterReleaseTracks + .map(track => + relation('generateTrackReleaseBox', track)) + : null), }), - data: (album, track) => ({ + data: (_query, _sprawl, _album, track) => ({ isAlbumPage: !track, + isTrackPage: !!track, }), - generate: (data, relations) => - relations.sidebar.slots({ + generate(data, relations, {html}) { + for (const box of [ + ...relations.groupBoxes, + ...relations.seriesBoxes.flat(), + ...relations.disconnectedSeriesBoxes, + ]) { + box.setSlot('mode', + data.isAlbumPage ? 'album' : 'track'); + } + + return relations.sidebar.slots({ boxes: [ - data.isAlbumPage && - relations.groupBoxes - .map(box => box.slot('mode', 'album')), + data.isAlbumPage && [ + relations.disconnectedSeriesBoxes, + + stitchArrays({ + groupBox: relations.groupBoxes, + seriesBoxes: relations.seriesBoxes, + }).map(({groupBox, seriesBoxes}) => [ + groupBox, + seriesBoxes.map(seriesBox => [ + html.tag('div', + {class: 'sidebar-box-joiner'}, + {class: 'collapsible'}), + seriesBox, + ]), + ]), + ], + + data.isTrackPage && + relations.earlierTrackReleaseBoxes, relations.trackListBox, - !data.isAlbumPage && + data.isTrackPage && + relations.laterTrackReleaseBoxes, + + data.isTrackPage && relations.conjoinedBox.slots({ attributes: {class: 'conjoined-group-sidebar-box'}, boxes: - relations.groupBoxes - .map(box => box.slot('mode', 'track')) + ([relations.disconnectedSeriesBoxes, + stitchArrays({ + groupBox: relations.groupBoxes, + seriesBoxes: relations.seriesBoxes, + }).flatMap(({groupBox, seriesBoxes}) => [ + groupBox, + ...seriesBoxes, + ]), + ]).flat() .map(box => box.content), /* TODO: Kludge. */ }), ], - }), + }); + }, }; diff --git a/src/content/dependencies/generateAlbumSidebarSeriesBox.js b/src/content/dependencies/generateAlbumSidebarSeriesBox.js new file mode 100644 index 00000000..37616cb2 --- /dev/null +++ b/src/content/dependencies/generateAlbumSidebarSeriesBox.js @@ -0,0 +1,102 @@ +import {atOffset} from '#sugar'; + +export default { + contentDependencies: [ + 'generatePageSidebarBox', + 'linkAlbum', + 'linkGroup', + 'transformContent', + ], + + extraDependencies: ['html', 'language'], + + query(album, series) { + const query = {}; + + const albums = + series.albums; + + const index = + albums.indexOf(album); + + query.previousAlbum = + atOffset(albums, index, -1); + + query.nextAlbum = + atOffset(albums, index, +1); + + return query; + }, + + relations: (relation, query, _album, series) => ({ + box: + relation('generatePageSidebarBox'), + + groupLink: + relation('linkGroup', series.group), + + description: + relation('transformContent', series.description), + + previousAlbumLink: + (query.previousAlbum + ? relation('linkAlbum', query.previousAlbum) + : null), + + nextAlbumLink: + (query.nextAlbum + ? relation('linkAlbum', query.nextAlbum) + : null), + }), + + data: (_query, _album, series) => ({ + name: series.name, + }), + + slots: { + mode: { + validate: v => v.is('album', 'track'), + default: 'track', + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('albumSidebar.groupBox', boxCapsule => + relations.box.slots({ + attributes: {class: 'individual-series-sidebar-box'}, + content: [ + html.tag('h1', + language.$(boxCapsule, 'title', { + group: + relations.groupLink.slots({ + attributes: {class: 'series'}, + content: language.sanitize(data.name), + }), + })), + + slots.mode === 'album' && + relations.description + ?.slot('mode', 'multiline'), + + slots.mode === 'album' && + html.tag('p', {class: 'series-chronology-link'}, + {[html.onlyIfContent]: true}, + + language.$(boxCapsule, 'next', { + [language.onlyIfOptions]: ['album'], + + album: relations.nextAlbumLink, + })), + + slots.mode === 'album' && + html.tag('p', {class: 'series-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 d0c46060..dae5fa03 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -1,3 +1,5 @@ +import {empty, stitchArrays} from '#sugar'; + export default { contentDependencies: ['linkTrack'], extraDependencies: ['getColors', 'html', 'language'], @@ -15,23 +17,25 @@ export default { data(album, track, trackSection) { const data = {}; - data.hasTrackNumbers = album.hasTrackNumbers; + data.hasTrackNumbers = + album.hasTrackNumbers && + !empty(trackSection.tracks); + data.isTrackPage = !!track; data.name = trackSection.name; data.color = trackSection.color; data.isDefaultTrackSection = trackSection.isDefaultTrackSection; - data.firstTrackNumber = trackSection.startIndex + 1; - data.lastTrackNumber = trackSection.startIndex + trackSection.tracks.length; + data.firstTrackNumber = + (data.hasTrackNumbers + ? trackSection.tracks.at(0).trackNumber + : null); - if (track) { - const index = trackSection.tracks.indexOf(track); - if (index !== -1) { - data.includesCurrentTrack = true; - data.currentTrackIndex = index; - } - } + data.lastTrackNumber = + (data.hasTrackNumbers + ? trackSection.tracks.at(-1).trackNumber + : null); data.trackDirectories = trackSection.tracks @@ -39,7 +43,14 @@ export default { data.tracksAreMissingCommentary = trackSection.tracks - .map(track => !track.commentary); + .map(track => empty(track.commentary)); + + data.tracksAreCurrentTrack = + trackSection.tracks + .map(traaaaaaaack => traaaaaaaack === track); + + data.includesCurrentTrack = + data.tracksAreCurrentTrack.includes(true); return data; }, @@ -58,7 +69,7 @@ export default { const capsule = language.encapsulate('albumSidebar.trackList'); const sectionName = - html.tag('span', {class: 'group-name'}, + html.tag('b', (data.isDefaultTrackSection ? language.$(capsule, 'fallbackSectionName') : data.name)); @@ -70,29 +81,39 @@ export default { } const trackListItems = - relations.trackLinks.map((trackLink, index) => - html.tag('li', - data.includesCurrentTrack && - index === data.currentTrackIndex && - {class: 'current'}, - - slots.mode === 'commentary' && - data.tracksAreMissingCommentary[index] && - {class: 'no-commentary'}, - - language.$(capsule, 'item', { - track: - (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index] - ? trackLink.slots({ - linkless: true, - }) - : slots.anchor - ? trackLink.slots({ - anchor: true, - hash: data.trackDirectories[index], - }) - : trackLink), - }))); + stitchArrays({ + trackLink: relations.trackLinks, + directory: data.trackDirectories, + isCurrentTrack: data.tracksAreCurrentTrack, + missingCommentary: data.tracksAreMissingCommentary, + }).map(({ + trackLink, + directory, + isCurrentTrack, + missingCommentary, + }) => + html.tag('li', + data.includesCurrentTrack && + isCurrentTrack && + {class: 'current'}, + + slots.mode === 'commentary' && + missingCommentary && + {class: 'no-commentary'}, + + language.$(capsule, 'item', { + track: + (slots.mode === 'commentary' && missingCommentary + ? trackLink.slots({ + linkless: true, + }) + : slots.anchor + ? trackLink.slots({ + anchor: true, + hash: directory, + }) + : trackLink), + }))); return html.tag('details', data.includesCurrentTrack && @@ -119,17 +140,22 @@ export default { colorStyle, html.tag('span', - language.encapsulate(capsule, 'group', workingCapsule => { - const workingOptions = {group: sectionName}; - - if (data.hasTrackNumbers) { - workingCapsule += '.withRange'; - workingOptions.range = - `${data.firstTrackNumber}–${data.lastTrackNumber}`; - } - - return language.$(workingCapsule, workingOptions); - }))), + language.encapsulate(capsule, 'group', groupCapsule => + language.encapsulate(groupCapsule, workingCapsule => { + const workingOptions = {group: sectionName}; + + if (data.hasTrackNumbers) { + workingCapsule += '.withRange'; + workingOptions.rangePart = + html.tag('span', {class: 'track-section-range'}, + language.$(groupCapsule, 'withRange.rangePart', { + 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 7500109e..e28a3fd0 100644 --- a/src/content/dependencies/generateAlbumSocialEmbed.js +++ b/src/content/dependencies/generateAlbumSocialEmbed.js @@ -6,7 +6,7 @@ export default { 'generateAlbumSocialEmbedDescription', ], - extraDependencies: ['absoluteTo', 'language', 'urls'], + extraDependencies: ['absoluteTo', 'language'], relations(relation, album) { return { @@ -25,15 +25,14 @@ export default { if (data.hasHeading) { const firstGroup = album.groups[0]; - data.headingGroupName = firstGroup.directory; + data.headingGroupName = firstGroup.name; data.headingGroupDirectory = firstGroup.directory; } data.hasImage = album.hasCoverArt; if (data.hasImage) { - data.coverArtDirectory = album.directory; - data.coverArtFileExtension = album.coverArtFileExtension; + data.imagePath = album.coverArtworks[0].path; } data.albumName = album.name; @@ -41,7 +40,7 @@ export default { return data; }, - generate: (data, relations, {absoluteTo, language, urls}) => + generate: (data, relations, {absoluteTo, language}) => language.encapsulate('albumPage.socialEmbed', embedCapsule => relations.socialEmbed.slots({ title: @@ -65,10 +64,7 @@ export default { imagePath: (data.hasImage - ? '/' + - urls - .from('shared.root') - .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension) + ? data.imagePath : null), })), }; diff --git a/src/content/dependencies/generateAlbumSocialEmbedDescription.js b/src/content/dependencies/generateAlbumSocialEmbedDescription.js index 7099616a..69c39c3a 100644 --- a/src/content/dependencies/generateAlbumSocialEmbedDescription.js +++ b/src/content/dependencies/generateAlbumSocialEmbedDescription.js @@ -3,46 +3,39 @@ import {accumulateSum} from '#sugar'; export default { extraDependencies: ['language'], - data(album) { - const data = {}; - - const duration = accumulateSum(album.tracks, track => track.duration); - - data.hasDuration = duration > 0; - data.hasTracks = album.tracks.length > 0; - data.hasDate = !!album.date; - data.hasAny = (data.hasDuration || data.hasTracks || data.hasDuration); - - if (!data.hasAny) - return data; - - if (data.hasDuration) - data.duration = duration; - - if (data.hasTracks) - data.tracks = album.tracks.length; - - if (data.hasDate) - data.date = album.date; - - return data; - }, - - generate(data, {language}) { - return language.formatString( - 'albumPage.socialEmbed.body' + [ - data.hasDuration && '.withDuration', - data.hasTracks && '.withTracks', - data.hasDate && '.withReleaseDate', - ].filter(Boolean).join(''), - - Object.fromEntries([ - data.hasDuration && - ['duration', language.formatDuration(data.duration)], - data.hasTracks && - ['tracks', language.countTracks(data.tracks, {unit: true})], - data.hasDate && - ['date', language.formatDate(data.date)], - ].filter(Boolean))); - }, + data: (album) => ({ + duration: + accumulateSum(album.tracks, track => track.duration), + + tracks: + album.tracks.length, + + date: + album.date, + }), + + generate: (data, {language}) => + language.encapsulate('albumPage.socialEmbed.body', workingCapsule => { + const workingOptions = {}; + + if (data.duration > 0) { + workingCapsule += '.withDuration'; + workingOptions.duration = + language.formatDuration(data.duration); + } + + if (data.tracks > 0) { + workingCapsule += '.withTracks'; + workingOptions.tracks = + language.countTracks(data.tracks, {unit: true}); + } + + if (data.date) { + workingCapsule += '.withReleaseDate'; + workingOptions.date = + language.formatDate(data.date); + } + + return language.$(workingCapsule, workingOptions); + }), }; diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js deleted file mode 100644 index c5acf374..00000000 --- a/src/content/dependencies/generateAlbumStyleRules.js +++ /dev/null @@ -1,72 +0,0 @@ -import {empty} from '#sugar'; - -export default { - extraDependencies: ['to'], - - data(album, track) { - const data = {}; - - data.hasWallpaper = !empty(album.wallpaperArtistContribs); - data.hasBanner = !empty(album.bannerArtistContribs); - - if (data.hasWallpaper) { - data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension]; - data.wallpaperStyle = album.wallpaperStyle; - } - - if (data.hasBanner) { - data.hasBannerStyle = !!album.bannerStyle; - data.bannerStyle = album.bannerStyle; - } - - data.albumDirectory = album.directory; - - if (track) { - data.trackDirectory = track.directory; - } - - return data; - }, - - generate(data, {to}) { - const indent = parts => - (parts ?? []) - .filter(Boolean) - .join('\n') - .split('\n') - .map(line => ' '.repeat(4) + line) - .join('\n'); - - const rule = (selector, parts) => - (!empty(parts.filter(Boolean)) - ? [`${selector} {`, indent(parts), `}`] - : []); - - const wallpaperRule = - data.hasWallpaper && - rule(`body::before`, [ - `background-image: url("${to(...data.wallpaperPath)}");`, - data.wallpaperStyle, - ]); - - const bannerRule = - data.hasBanner && - rule(`#banner img`, [ - data.bannerStyle, - ]); - - const dataRule = - rule(`:root`, [ - data.albumDirectory && - `--album-directory: ${data.albumDirectory};`, - data.trackDirectory && - `--track-directory: ${data.trackDirectory};`, - ]); - - return ( - [wallpaperRule, bannerRule, dataRule] - .filter(Boolean) - .flat() - .join('\n')); - }, -}; diff --git a/src/content/dependencies/generateAlbumStyleTags.js b/src/content/dependencies/generateAlbumStyleTags.js new file mode 100644 index 00000000..4cdc6581 --- /dev/null +++ b/src/content/dependencies/generateAlbumStyleTags.js @@ -0,0 +1,65 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: ['generateAlbumWallpaperStyleTag', 'generateStyleTag'], + extraDependencies: ['html'], + + relations: (relation, album, _track) => ({ + styleTag: + relation('generateStyleTag'), + + wallpaperStyleTag: + relation('generateAlbumWallpaperStyleTag', album), + }), + + data(album, track) { + const data = {}; + + data.hasBanner = !empty(album.bannerArtistContribs); + + if (data.hasBanner) { + data.hasBannerStyle = !!album.bannerStyle; + data.bannerStyle = album.bannerStyle; + } + + data.albumDirectory = album.directory; + + if (track) { + data.trackDirectory = track.directory; + } + + return data; + }, + + generate: (data, relations, {html}) => + html.tags([ + relations.wallpaperStyleTag, + + relations.styleTag.clone().slots({ + attributes: {class: 'album-banner-style'}, + + rules: [ + data.hasBanner && { + select: '#banner img', + declare: [data.bannerStyle], + }, + ], + }), + + relations.styleTag.clone().slots({ + attributes: {class: 'album-directory-style'}, + + rules: [ + { + select: ':root', + declare: [ + data.albumDirectory && + `--album-directory: ${data.albumDirectory};`, + data.trackDirectory && + `--track-directory: ${data.trackDirectory};`, + ], + }, + ] + }), + ], {[html.joinChildren]: ''}), +}; diff --git a/src/content/dependencies/generateAlbumTrackList.js b/src/content/dependencies/generateAlbumTrackList.js index a3435bea..0a949ded 100644 --- a/src/content/dependencies/generateAlbumTrackList.js +++ b/src/content/dependencies/generateAlbumTrackList.js @@ -35,7 +35,12 @@ function getDisplayMode(album) { } export default { - contentDependencies: ['generateAlbumTrackListItem', 'generateContentHeading'], + contentDependencies: [ + 'generateAlbumTrackListItem', + 'generateContentHeading', + 'transformContent', + ], + extraDependencies: ['html', 'language'], query(album) { @@ -53,6 +58,10 @@ export default { album.trackSections.map(() => relation('generateContentHeading')); + relations.trackSectionDescriptions = + album.trackSections.map(section => + relation('transformContent', section.description)); + relations.trackSectionItems = album.trackSections.map(section => section.tracks.map(track => @@ -93,11 +102,11 @@ export default { .map(section => section.tracks.length > 1); if (album.hasTrackNumbers) { - data.trackSectionStartIndices = + data.trackSectionsStartCountingFrom = album.trackSections - .map(section => section.startIndex); + .map(section => section.startCountingFrom); } else { - data.trackSectionStartIndices = + data.trackSectionsStartCountingFrom = album.trackSections .map(() => null); } @@ -132,20 +141,22 @@ export default { return html.tag('dl', {class: 'album-group-list'}, stitchArrays({ heading: relations.trackSectionHeadings, + description: relations.trackSectionDescriptions, items: relations.trackSectionItems, name: data.trackSectionNames, duration: data.trackSectionDurations, durationApproximate: data.trackSectionDurationsApproximate, - startIndex: data.trackSectionStartIndices, + startCountingFrom: data.trackSectionsStartCountingFrom, }).map(({ heading, + description, items, name, duration, durationApproximate, - startIndex, + startCountingFrom, }) => [ language.encapsulate('trackList.section', capsule => heading.slots({ @@ -172,12 +183,17 @@ export default { }), })), - html.tag('dd', + html.tag('dd', [ + html.tag('blockquote', + {[html.onlyIfContent]: true}, + description), + html.tag(listTag, data.hasTrackNumbers && - {start: startIndex + 1}, + {start: startCountingFrom}, - slotItems(items))), + slotItems(items)), + ]), ])); case 'tracks': diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js index 7d5d2c6e..44297c15 100644 --- a/src/content/dependencies/generateAlbumTrackListItem.js +++ b/src/content/dependencies/generateAlbumTrackListItem.js @@ -1,75 +1,36 @@ -import {compareArrays, empty} from '#sugar'; - export default { - contentDependencies: [ - 'generateAlbumTrackListMissingDuration', - 'linkContribution', - 'linkTrack', - ], - - extraDependencies: ['getColors', 'html', 'language'], - - query(track, album) { - const query = {}; + contentDependencies: ['generateTrackListItem'], + extraDependencies: ['html'], - query.duration = track.duration ?? 0; + query: (track, album) => ({ + trackHasDuration: + !!track.duration, - query.trackHasDuration = !!track.duration; - - query.sectionHasDuration = + sectionHasDuration: !album.trackSections .some(section => section.tracks.every(track => !track.duration) && - section.tracks.includes(track)); - - query.albumHasDuration = - album.tracks.some(track => track.duration); + section.tracks.includes(track)), - return query; - }, + albumHasDuration: + album.tracks.some(track => track.duration), + }), - relations(relation, query, track) { - const relations = {}; - - if (!empty(track.artistContribs)) { - relations.contributionLinks = - track.artistContribs - .map(contrib => relation('linkContribution', contrib)); - } - - relations.trackLink = - relation('linkTrack', track); - - if (!query.trackHasDuration) { - relations.missingDuration = - relation('generateAlbumTrackListMissingDuration'); - } - - return relations; - }, + relations: (relation, query, track) => ({ + item: + relation('generateTrackListItem', + track, + track.album.artistContribs), + }), - data(query, track, album) { - const data = {}; + data: (query, track, album) => ({ + trackHasDuration: query.trackHasDuration, + sectionHasDuration: query.sectionHasDuration, + albumHasDuration: query.albumHasDuration, - data.duration = query.duration; - data.trackHasDuration = query.trackHasDuration; - data.sectionHasDuration = query.sectionHasDuration; - data.albumHasDuration = query.albumHasDuration; - - if (track.color !== album.color) { - data.color = track.color; - } - - data.showArtists = - !empty(track.artistContribs) && - (empty(album.artistContribs) || - !compareArrays( - track.artistContribs.map(contrib => contrib.artist), - album.artistContribs.map(contrib => contrib.artist), - {checkOrder: false})); - - return data; - }, + colorize: + track.color !== album.color, + }), slots: { collapseDurationScope: { @@ -80,51 +41,22 @@ export default { }, }, - 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); - }))), + generate: (data, relations, slots) => + relations.item.slots({ + showArtists: true, + + showDuration: + (slots.collapseDurationScope === 'track' + ? data.trackHasDuration + : slots.collapseDurationScope === 'section' + ? data.sectionHasDuration + : slots.collapseDurationScope === 'album' + ? data.albumHasDuration + : true), + + colorMode: + (data.colorize + ? 'line' + : 'none'), + }), }; diff --git a/src/content/dependencies/generateAlbumWallpaperStyleTag.js b/src/content/dependencies/generateAlbumWallpaperStyleTag.js new file mode 100644 index 00000000..47864a1d --- /dev/null +++ b/src/content/dependencies/generateAlbumWallpaperStyleTag.js @@ -0,0 +1,38 @@ +export default { + contentDependencies: ['generateWallpaperStyleTag'], + extraDependencies: ['html'], + + relations: (relation, album) => ({ + wallpaperStyleTag: + (album.hasWallpaperArt + ? relation('generateWallpaperStyleTag') + : null), + }), + + data: (album) => ({ + singleWallpaperPath: + ['media.albumWallpaper', album.directory, album.wallpaperFileExtension], + + singleWallpaperStyle: + album.wallpaperStyle, + + wallpaperPartPaths: + album.wallpaperParts.map(part => + (part.asset + ? ['media.albumWallpaperPart', album.directory, part.asset] + : null)), + + wallpaperPartStyles: + album.wallpaperParts.map(part => part.style), + }), + + generate: (data, relations, {html}) => + (relations.wallpaperStyleTag + ? relations.wallpaperStyleTag.slots({ + singleWallpaperPath: data.singleWallpaperPath, + singleWallpaperStyle: data.singleWallpaperStyle, + wallpaperPartPaths: data.wallpaperPartPaths, + wallpaperPartStyles: data.wallpaperPartStyles, + }) + : html.blank()), +}; diff --git a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js new file mode 100644 index 00000000..80d19b5a --- /dev/null +++ b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js @@ -0,0 +1,153 @@ +import { + filterMultipleArrays, + sortMultipleArrays, + stitchArrays, + unique, +} from '#sugar'; + +export default { + contentDependencies: ['linkArtTagDynamically'], + extraDependencies: ['html', 'language'], + + // Recursion ain't too pretty! + + query(ancestorArtTag, targetArtTag) { + const recursive = artTag => { + const artTags = + artTag.directDescendantArtTags.slice(); + + const displayBriefly = + !artTags.includes(targetArtTag) && + artTags.length > 3; + + const artTagsIncludeTargetArtTag = + artTags.map(artTag => artTag.allDescendantArtTags.includes(targetArtTag)); + + const numExemptArtTags = + (displayBriefly + ? artTagsIncludeTargetArtTag + .filter(includesTargetArtTag => !includesTargetArtTag) + .length + : null); + + const artTagsTimesFeaturedTotal = + artTags.map(artTag => + unique([ + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, + ]).length); + + const sublists = + stitchArrays({ + artTag: artTags, + includesTargetArtTag: artTagsIncludeTargetArtTag, + }).map(({artTag, includesTargetArtTag}) => + (includesTargetArtTag + ? recursive(artTag) + : null)); + + if (displayBriefly) { + filterMultipleArrays(artTags, sublists, artTagsTimesFeaturedTotal, + (artTag, sublist) => + artTag === targetArtTag || + sublist !== null); + } else { + sortMultipleArrays(artTags, sublists, artTagsTimesFeaturedTotal, + (artTagA, artTagB, sublistA, sublistB) => + (sublistA && sublistB + ? 0 + : !sublistA && !sublistB + ? 0 + : sublistA + ? 1 + : -1)); + } + + return { + displayBriefly, + numExemptArtTags, + artTags, + artTagsTimesFeaturedTotal, + sublists, + }; + }; + + return {root: recursive(ancestorArtTag)}; + }, + + relations(relation, query, _ancestorArtTag, _targetArtTag) { + const recursive = ({artTags, sublists}) => ({ + artTagLinks: + artTags + .map(artTag => relation('linkArtTagDynamically', artTag)), + + sublists: + sublists + .map(sublist => (sublist ? recursive(sublist) : null)), + }); + + return {root: recursive(query.root)}; + }, + + data(query, _ancestorArtTag, targetArtTag) { + const recursive = ({ + displayBriefly, + numExemptArtTags, + artTags, + artTagsTimesFeaturedTotal, + sublists, + }) => ({ + displayBriefly, + numExemptArtTags, + artTagsTimesFeaturedTotal, + + artTagsAreTargetTag: + artTags + .map(artTag => artTag === targetArtTag), + + sublists: + sublists + .map(sublist => (sublist ? recursive(sublist) : null)), + }); + + return {root: recursive(query.root)}; + }, + + generate(data, relations, {html, language}) { + const recursive = (dataNode, relationsNode) => + html.tag('dl', {class: dataNode === data.root && 'tree-list'}, [ + dataNode.displayBriefly && + html.tag('dt', + language.$('artTagPage.sidebar.otherTagsExempt', { + tags: + language.countArtTags(dataNode.numExemptArtTags, {unit: true}), + })), + + stitchArrays({ + isTargetTag: dataNode.artTagsAreTargetTag, + timesFeaturedTotal: dataNode.artTagsTimesFeaturedTotal, + dataSublist: dataNode.sublists, + + artTagLink: relationsNode.artTagLinks, + relationsSublist: relationsNode.sublists, + }).map(({ + isTargetTag, timesFeaturedTotal, dataSublist, + artTagLink, relationsSublist, + }) => [ + html.tag('dt', + {class: (dataSublist || isTargetTag) && 'current'}, + [ + artTagLink, + html.tag('span', {class: 'times-used'}, + language.countTimesFeatured(timesFeaturedTotal)), + ]), + + dataSublist && + html.tag('dd', + recursive(dataSublist, relationsSublist)), + ]), + ]); + + return recursive(data.root, relations.root); + }, +}; diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js index c51faeba..cfd6d03e 100644 --- a/src/content/dependencies/generateArtTagGalleryPage.js +++ b/src/content/dependencies/generateArtTagGalleryPage.js @@ -1,14 +1,19 @@ -import {sortAlbumsTracksChronologically} from '#sort'; -import {stitchArrays} from '#sugar'; +import {sortArtworksChronologically} from '#sort'; +import {empty, stitchArrays, unique} from '#sugar'; export default { contentDependencies: [ + 'generateAdditionalNamesBox', + 'generateArtTagGalleryPageFeaturedLine', + 'generateArtTagGalleryPageShowingLine', + 'generateArtTagNavLinks', 'generateCoverGrid', 'generatePageLayout', + 'generateQuickDescription', 'image', - 'linkAlbum', - 'linkArtTag', - 'linkTrack', + 'linkAnythingMan', + 'linkArtTagGallery', + 'linkExternal', ], extraDependencies: ['html', 'language', 'wikiData'], @@ -19,74 +24,107 @@ export default { }; }, - query(sprawl, tag) { - const things = tag.taggedInThings.slice(); + query(sprawl, artTag) { + const directArtworks = artTag.directlyFeaturedInArtworks; + const indirectArtworks = artTag.indirectlyFeaturedInArtworks; + const allArtworks = unique([...directArtworks, ...indirectArtworks]); - sortAlbumsTracksChronologically(things, { - getDate: thing => thing.coverArtDate ?? thing.date, - latestFirst: true, - }); + sortArtworksChronologically(allArtworks, {latestFirst: true}); - return {things}; + return {directArtworks, indirectArtworks, allArtworks}; }, - relations(relation, query, sprawl, tag) { + relations(relation, query, sprawl, artTag) { const relations = {}; relations.layout = relation('generatePageLayout'); - relations.artTagMainLink = - relation('linkArtTag', tag); + relations.navLinks = + relation('generateArtTagNavLinks', artTag); + + relations.additionalNamesBox = + relation('generateAdditionalNamesBox', artTag.additionalNames); + + relations.quickDescription = + relation('generateQuickDescription', artTag); + + relations.featuredLine = + relation('generateArtTagGalleryPageFeaturedLine'); + + relations.showingLine = + relation('generateArtTagGalleryPageShowingLine'); + + if (!empty(artTag.extraReadingURLs)) { + relations.extraReadingLinks = + artTag.extraReadingURLs + .map(url => relation('linkExternal', url)); + } + + if (!empty(artTag.directAncestorArtTags)) { + relations.ancestorLinks = + artTag.directAncestorArtTags + .map(artTag => relation('linkArtTagGallery', artTag)); + } + + if (!empty(artTag.directDescendantArtTags)) { + relations.descendantLinks = + artTag.directDescendantArtTags + .map(artTag => relation('linkArtTagGallery', artTag)); + } relations.coverGrid = relation('generateCoverGrid'); relations.links = - query.things.map(thing => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))); + query.allArtworks + .map(artwork => relation('linkAnythingMan', artwork.thing)); relations.images = - query.things.map(thing => - relation('image', thing.artTags)); + query.allArtworks + .map(artwork => relation('image', artwork)); return relations; }, - data(query, sprawl, tag) { + data(query, sprawl, artTag) { const data = {}; data.enableListings = sprawl.enableListings; - data.name = tag.name; - data.color = tag.color; + data.name = artTag.name; + data.color = artTag.color; - data.numArtworks = query.things.length; + data.numArtworksIndirectly = query.indirectArtworks.length; + data.numArtworksDirectly = query.directArtworks.length; + data.numArtworksTotal = query.allArtworks.length; data.names = - query.things.map(thing => thing.name); + query.allArtworks + .map(artwork => artwork.thing.name); - data.paths = - query.things.map(thing => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])); + data.artworkArtists = + query.allArtworks + .map(artwork => artwork.artistContribs + .map(contrib => contrib.artist.name)); - data.dimensions = - query.things.map(thing => thing.coverArtDimensions); + data.artworkLabels = + query.allArtworks + .map(artwork => artwork.label) - data.coverArtists = - query.things.map(thing => - thing.coverArtistContribs - .map(({artist}) => artist.name)); + data.onlyFeaturedIndirectly = + query.allArtworks.map(artwork => + !query.directArtworks.includes(artwork)); + + data.hasMixedDirectIndirect = + data.onlyFeaturedIndirectly.includes(true) && + data.onlyFeaturedIndirectly.includes(false); return data; }, generate: (data, relations, {html, language}) => - language.encapsulate('tagPage', pageCapsule => + language.encapsulate('artTagGalleryPage', pageCapsule => relations.layout.slots({ title: language.$(pageCapsule, 'title', { @@ -94,59 +132,107 @@ export default { }), headingMode: 'static', - color: data.color, + additionalNames: relations.additionalNamesBox, + mainClasses: ['top-index'], mainContent: [ - html.tag('p', {class: 'quick-info'}, - language.$(pageCapsule, 'infoLine', { - coverArts: language.countCoverArts(data.numArtworks, { - unit: true, + relations.quickDescription.slots({ + extraReadingLinks: relations.extraReadingLinks ?? null, + }), + + data.numArtworksTotal === 0 && + html.tag('p', {class: 'quick-info'}, + language.encapsulate(pageCapsule, 'featuredLine.notFeatured', capsule => [ + language.$(capsule), + html.tag('br'), + language.$(capsule, 'callToAction'), + ])), + + data.numArtworksTotal >= 1 && + relations.featuredLine.clone() + .slots({ + showing: 'all', + count: data.numArtworksTotal, + }), + + data.hasMixedDirectIndirect && [ + relations.featuredLine.clone() + .slots({ + showing: 'direct', + count: data.numArtworksDirectly, }), - })), + + relations.featuredLine.clone() + .slots({ + showing: 'indirect', + count: data.numArtworksIndirectly, + }), + ], + + relations.ancestorLinks && + html.tag('p', {id: 'descends-from-line'}, + {class: 'quick-info'}, + language.$(pageCapsule, 'descendsFrom', { + tags: language.formatUnitList(relations.ancestorLinks), + })), + + relations.descendantLinks && + html.tag('p', {id: 'descendants-line'}, + {class: 'quick-info'}, + language.$(pageCapsule, 'descendants', { + tags: language.formatUnitList(relations.descendantLinks), + })), + + data.hasMixedDirectIndirect && [ + relations.showingLine.clone() + .slot('showing', 'all'), + + relations.showingLine.clone() + .slot('showing', 'direct'), + + relations.showingLine.clone() + .slot('showing', 'indirect'), + ], relations.coverGrid .slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, - })), + lazy: 12, + + classes: + data.onlyFeaturedIndirectly.map(onlyFeaturedIndirectly => + (onlyFeaturedIndirectly ? 'featured-indirectly' : '')), info: - data.coverArtists.map(names => - (names === null - ? null - : language.$('misc.albumGrid.details.coverArtists', { - artists: language.formatUnitList(names), - }))), + stitchArrays({ + artists: data.artworkArtists, + label: data.artworkLabels, + }).map(({artists, label}) => + language.encapsulate('misc.coverGrid.details.coverArtists', workingCapsule => { + const workingOptions = {}; + + workingOptions[language.onlyIfOptions] = ['artists']; + workingOptions.artists = + language.formatUnitList(artists); + + if (label) { + workingCapsule += '.customLabel'; + workingOptions.label = label; + } + + return language.$(workingCapsule, workingOptions); + })), }), ], navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - - data.enableListings && - { - path: ['localized.listingIndex'], - title: language.$('listingIndex.title'), - }, - - { - html: - language.$(pageCapsule, 'nav.tag', { - tag: relations.artTagMainLink, - }), - }, - ], + navLinks: + html.resolve( + relations.navLinks + .slot('currentExtra', 'gallery')), })), }; diff --git a/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js new file mode 100644 index 00000000..b4620fa4 --- /dev/null +++ b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js @@ -0,0 +1,23 @@ +export default { + extraDependencies: ['html', 'language'], + + slots: { + showing: { + validate: v => v.is('all', 'direct', 'indirect'), + }, + + count: {type: 'number'}, + }, + + generate: (slots, {html, language}) => + language.encapsulate('artTagGalleryPage', pageCapsule => + html.tag('p', {class: 'quick-info'}, + {id: `featured-${slots.showing}-line`}, + + language.$(pageCapsule, 'featuredLine', slots.showing, { + coverArts: + language.countArtworks(slots.count, { + unit: true, + }), + }))), +}; diff --git a/src/content/dependencies/generateArtTagGalleryPageShowingLine.js b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js new file mode 100644 index 00000000..6df4d0e5 --- /dev/null +++ b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js @@ -0,0 +1,22 @@ +export default { + extraDependencies: ['html', 'language'], + + slots: { + showing: { + validate: v => v.is('all', 'direct', 'indirect'), + }, + + count: {type: 'number'}, + }, + + generate: (slots, {html, language}) => + language.encapsulate('artTagGalleryPage', pageCapsule => + html.tag('p', {class: 'quick-info'}, + {id: `showing-${slots.showing}-line`}, + + language.$(pageCapsule, 'showingLine', { + showing: + html.tag('a', {href: '#'}, + language.$(pageCapsule, 'showingLine', slots.showing)), + }))), +}; diff --git a/src/content/dependencies/generateArtTagInfoPage.js b/src/content/dependencies/generateArtTagInfoPage.js new file mode 100644 index 00000000..9df51b77 --- /dev/null +++ b/src/content/dependencies/generateArtTagInfoPage.js @@ -0,0 +1,281 @@ +import {empty, stitchArrays, unique} from '#sugar'; + +export default { + contentDependencies: [ + 'generateAdditionalNamesBox', + 'generateArtTagNavLinks', + 'generateArtTagSidebar', + 'generateContentHeading', + 'generatePageLayout', + 'linkArtTagGallery', + 'linkArtTagInfo', + 'linkExternal', + 'transformContent', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({wikiInfo}) => ({ + enableListings: wikiInfo.enableListings, + }), + + query(sprawl, artTag) { + const query = {}; + + query.directThings = + artTag.directlyFeaturedInArtworks; + + query.indirectThings = + artTag.indirectlyFeaturedInArtworks; + + query.allThings = + unique([...query.directThings, ...query.indirectThings]); + + query.allDescendantsHaveMoreDescendants = + artTag.directDescendantArtTags + .every(descendant => !empty(descendant.directDescendantArtTags)); + + return query; + }, + + relations: (relation, query, sprawl, artTag) => ({ + layout: + relation('generatePageLayout'), + + navLinks: + relation('generateArtTagNavLinks', artTag), + + sidebar: + relation('generateArtTagSidebar', artTag), + + additionalNamesBox: + relation('generateAdditionalNamesBox', artTag.additionalNames), + + contentHeading: + relation('generateContentHeading'), + + description: + relation('transformContent', artTag.description), + + galleryLink: + (empty(query.allThings) + ? null + : relation('linkArtTagGallery', artTag)), + + extraReadingLinks: + artTag.extraReadingURLs + .map(url => relation('linkExternal', url)), + + relatedArtTagLinks: + artTag.relatedArtTags + .map(({artTag}) => relation('linkArtTagInfo', artTag)), + + directAncestorLinks: + artTag.directAncestorArtTags + .map(artTag => relation('linkArtTagInfo', artTag)), + + directDescendantInfoLinks: + artTag.directDescendantArtTags + .map(artTag => relation('linkArtTagInfo', artTag)), + + directDescendantGalleryLinks: + artTag.directDescendantArtTags.map(artTag => + (query.allDescendantsHaveMoreDescendants + ? null + : relation('linkArtTagGallery', artTag))), + }), + + data: (query, sprawl, artTag) => ({ + enableListings: + sprawl.enableListings, + + name: + artTag.name, + + color: + artTag.color, + + numArtworksIndirectly: + query.indirectThings.length, + + numArtworksDirectly: + query.directThings.length, + + numArtworksTotal: + query.allThings.length, + + relatedArtTagAnnotations: + artTag.relatedArtTags + .map(({annotation}) => annotation), + + directDescendantTimesFeaturedTotal: + artTag.directDescendantArtTags.map(artTag => + unique([ + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, + ]).length), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('artTagInfoPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + tag: language.sanitize(data.name), + }), + + headingMode: 'sticky', + color: data.color, + + additionalNames: relations.additionalNamesBox, + + mainContent: [ + html.tag('p', + language.encapsulate(pageCapsule, 'featuredIn', capsule => + (data.numArtworksTotal === 0 + ? language.$(capsule, 'notFeatured') + + : data.numArtworksDirectly === 0 + ? language.$(capsule, 'indirectlyOnly', { + artworks: + language.countArtworks(data.numArtworksIndirectly, {unit: true}), + }) + + : data.numArtworksIndirectly === 0 + ? language.$(capsule, 'directlyOnly', { + artworks: + language.countArtworks(data.numArtworksDirectly, {unit: true}), + }) + + : language.$(capsule, 'directlyAndIndirectly', { + artworksDirectly: + language.countArtworks(data.numArtworksDirectly, {unit: true}), + + artworksIndirectly: + language.countArtworks(data.numArtworksIndirectly, {unit: false}), + + artworksTotal: + language.countArtworks(data.numArtworksTotal, {unit: false}), + })))), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$(pageCapsule, 'viewArtGallery', { + [language.onlyIfOptions]: ['link'], + + link: + relations.galleryLink + ?.slot('content', language.$(pageCapsule, 'viewArtGallery.link')), + })), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.encapsulate(pageCapsule, 'seeAlso', capsule => + language.$(capsule, { + [language.onlyIfOptions]: ['tags'], + + tags: + language.formatUnitList( + stitchArrays({ + artTagLink: relations.relatedArtTagLinks, + annotation: data.relatedArtTagAnnotations, + }).map(({artTagLink, annotation}) => + (html.isBlank(annotation) + ? artTagLink + : language.$(capsule, 'tagWithAnnotation', { + tag: artTagLink, + annotation, + })))), + }))), + + html.tag('blockquote', + {[html.onlyIfContent]: true}, + + relations.description + .slot('mode', 'multiline')), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$(pageCapsule, 'readMoreOn', { + [language.onlyIfOptions]: ['links'], + + tag: language.sanitize(data.name), + links: language.formatDisjunctionList(relations.extraReadingLinks), + })), + + language.encapsulate(pageCapsule, 'descendsFromTags', listCapsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + title: + language.$(listCapsule, { + tag: language.sanitize(data.name), + }), + }), + + html.tag('ul', + {[html.onlyIfContent]: true}, + + relations.directAncestorLinks + .map(link => + html.tag('li', + language.$(listCapsule, 'item', { + tag: link, + })))), + ])), + + language.encapsulate(pageCapsule, 'descendantTags', listCapsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + title: + language.$(listCapsule, { + tag: language.sanitize(data.name), + }), + }), + + html.tag('ul', + {[html.onlyIfContent]: true}, + + stitchArrays({ + infoLink: relations.directDescendantInfoLinks, + galleryLink: relations.directDescendantGalleryLinks, + timesFeaturedTotal: data.directDescendantTimesFeaturedTotal, + }).map(({infoLink, galleryLink, timesFeaturedTotal}) => + html.tag('li', + language.encapsulate(listCapsule, 'item', itemCapsule => + language.encapsulate(itemCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.tag = infoLink; + + if (!html.isBlank(galleryLink ?? html.blank())) { + workingCapsule += '.withGallery'; + workingOptions.gallery = + galleryLink.slot('content', + language.$(itemCapsule, 'withGallery.gallery')); + } + + if (timesFeaturedTotal >= 1) { + workingCapsule += `.withTimesUsed`; + workingOptions.timesUsed = + language.countTimesFeatured(timesFeaturedTotal, { + unit: true, + }); + } + + return language.$(workingCapsule, workingOptions); + }))))), + ])), + ], + + navLinkStyle: 'hierarchical', + navLinks: relations.navLinks.content, + + leftSidebar: + relations.sidebar, + })), +}; diff --git a/src/content/dependencies/generateArtTagNavLinks.js b/src/content/dependencies/generateArtTagNavLinks.js new file mode 100644 index 00000000..9061a09f --- /dev/null +++ b/src/content/dependencies/generateArtTagNavLinks.js @@ -0,0 +1,81 @@ +export default { + contentDependencies: [ + 'generateInterpageDotSwitcher', + 'linkArtTagInfo', + 'linkArtTagGallery', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({wikiInfo}) => + ({enableListings: wikiInfo.enableListings}), + + relations: (relation, sprawl, tag) => ({ + switcher: + relation('generateInterpageDotSwitcher'), + + mainLink: + relation('linkArtTagInfo', tag), + + infoLink: + relation('linkArtTagInfo', tag), + + galleryLink: + relation('linkArtTagGallery', tag), + }), + + data: (sprawl) => + ({enableListings: sprawl.enableListings}), + + slots: { + currentExtra: { + validate: v => v.is('gallery'), + }, + }, + + generate(data, relations, slots, {language}) { + if (!data.enableListings) { + return [ + {auto: 'home'}, + {auto: 'current'}, + ]; + } + + const infoLink = + relations.infoLink.slots({ + attributes: {class: slots.currentExtra === null && 'current'}, + content: language.$('misc.nav.info'), + }); + + const galleryLink = + relations.galleryLink.slots({ + attributes: {class: slots.currentExtra === 'gallery' && 'current'}, + content: language.$('misc.nav.gallery'), + }); + + return [ + {auto: 'home'}, + + data.enableListings && + { + path: ['localized.listingIndex'], + title: language.$('listingIndex.title'), + }, + + { + html: + language.$('artTagPage.nav.tag', { + tag: relations.mainLink, + }), + + accent: + relations.switcher.slots({ + links: [ + infoLink, + galleryLink, + ], + }), + }, + ].filter(Boolean); + }, +}; diff --git a/src/content/dependencies/generateArtTagSidebar.js b/src/content/dependencies/generateArtTagSidebar.js new file mode 100644 index 00000000..9e2f813c --- /dev/null +++ b/src/content/dependencies/generateArtTagSidebar.js @@ -0,0 +1,124 @@ +import {collectTreeLeaves, empty, stitchArrays, unique} from '#sugar'; + +export default { + contentDependencies: [ + 'generatePageSidebar', + 'generatePageSidebarBox', + 'generateArtTagAncestorDescendantMapList', + 'linkArtTagDynamically', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({artTagData}) => + ({artTagData}), + + query(sprawl, artTag) { + const baobab = artTag.ancestorArtTagBaobabTree; + const uniqueLeaves = new Set(collectTreeLeaves(baobab)); + + // Just match the order in tag data. + const furthestAncestorArtTags = + sprawl.artTagData + .filter(artTag => uniqueLeaves.has(artTag)); + + return {furthestAncestorArtTags}; + }, + + relations: (relation, query, sprawl, artTag) => ({ + sidebar: + relation('generatePageSidebar'), + + sidebarBox: + relation('generatePageSidebarBox'), + + artTagLink: + relation('linkArtTagDynamically', artTag), + + directDescendantArtTagLinks: + artTag.directDescendantArtTags + .map(descendantArtTag => + relation('linkArtTagDynamically', descendantArtTag)), + + furthestAncestorArtTagMapLists: + query.furthestAncestorArtTags + .map(ancestorArtTag => + relation('generateArtTagAncestorDescendantMapList', + ancestorArtTag, + artTag)), + }), + + data: (query, sprawl, artTag) => ({ + name: artTag.name, + + directDescendantTimesFeaturedTotal: + artTag.directDescendantArtTags.map(artTag => + unique([ + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, + ]).length), + + furthestAncestorArtTagNames: + query.furthestAncestorArtTags + .map(ancestorArtTag => ancestorArtTag.name), + }), + + generate(data, relations, {html, language}) { + if ( + empty(relations.directDescendantArtTagLinks) && + empty(relations.furthestAncestorArtTagMapLists) + ) { + return relations.sidebar; + } + + return relations.sidebar.slots({ + boxes: [ + relations.sidebarBox.slots({ + content: [ + html.tag('h1', + relations.artTagLink), + + !empty(relations.directDescendantArtTagLinks) && + html.tag('details', {class: 'current', open: true}, [ + html.tag('summary', + html.tag('span', + html.tag('b', + language.sanitize(data.name)))), + + html.tag('ul', + stitchArrays({ + link: relations.directDescendantArtTagLinks, + timesFeaturedTotal: data.directDescendantTimesFeaturedTotal, + }).map(({link, timesFeaturedTotal}) => + html.tag('li', [ + link, + html.tag('span', {class: 'times-used'}, + language.countTimesFeatured(timesFeaturedTotal)), + ]))), + ]), + + stitchArrays({ + name: data.furthestAncestorArtTagNames, + list: relations.furthestAncestorArtTagMapLists, + }).map(({name, list}) => + html.tag('details', + { + class: 'has-tree-list', + open: + empty(relations.directDescendantArtTagLinks) && + relations.furthestAncestorArtTagMapLists.length === 1, + }, + [ + html.tag('summary', + html.tag('span', + html.tag('b', + language.sanitize(name)))), + + list, + ])), + ], + }), + ], + }); + }, +}; diff --git a/src/content/dependencies/generateArtistArtworkColumn.js b/src/content/dependencies/generateArtistArtworkColumn.js new file mode 100644 index 00000000..a4135489 --- /dev/null +++ b/src/content/dependencies/generateArtistArtworkColumn.js @@ -0,0 +1,13 @@ +export default { + contentDependencies: ['generateCoverArtwork'], + + relations: (relation, artist) => ({ + coverArtwork: + (artist.hasAvatar + ? relation('generateCoverArtwork', artist.avatarArtwork) + : null), + }), + + generate: (relations) => + relations.coverArtwork, +}; diff --git a/src/content/dependencies/generateArtistCredit.js b/src/content/dependencies/generateArtistCredit.js new file mode 100644 index 00000000..bab32f7d --- /dev/null +++ b/src/content/dependencies/generateArtistCredit.js @@ -0,0 +1,194 @@ +import {compareArrays, empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generateArtistCreditWikiEditsPart', + 'linkContribution', + ], + + extraDependencies: ['html', 'language'], + + query: (creditContributions, contextContributions) => { + const query = {}; + + const featuringFilter = contribution => + contribution.annotation === 'featuring'; + + const wikiEditFilter = contribution => + contribution.annotation?.startsWith('edits for wiki'); + + const normalFilter = contribution => + !featuringFilter(contribution) && + !wikiEditFilter(contribution); + + query.normalContributions = + creditContributions.filter(normalFilter); + + query.featuringContributions = + creditContributions.filter(featuringFilter); + + query.wikiEditContributions = + creditContributions.filter(wikiEditFilter); + + const contextNormalContributions = + contextContributions.filter(normalFilter); + + // Note that the normal contributions will implicitly *always* + // "differ from context" if no context contributions are given, + // as in release info lines. + + query.normalContributionArtistsDifferFromContext = + !compareArrays( + query.normalContributions.map(({artist}) => artist), + contextNormalContributions.map(({artist}) => artist), + {checkOrder: true}); + + query.normalContributionAnnotationsDifferFromContext = + !compareArrays( + query.normalContributions.map(({annotation}) => annotation), + contextNormalContributions.map(({annotation}) => annotation), + {checkOrder: true}); + + return query; + }, + + relations: (relation, query, _creditContributions, _contextContributions) => ({ + normalContributionLinks: + query.normalContributions + .map(contrib => relation('linkContribution', contrib)), + + featuringContributionLinks: + query.featuringContributions + .map(contrib => relation('linkContribution', contrib)), + + wikiEditsPart: + relation('generateArtistCreditWikiEditsPart', + query.wikiEditContributions), + }), + + data: (query, _creditContributions, _contextContributions) => ({ + normalContributionArtistsDifferFromContext: + query.normalContributionArtistsDifferFromContext, + + normalContributionAnnotationsDifferFromContext: + query.normalContributionAnnotationsDifferFromContext, + + hasWikiEdits: + !empty(query.wikiEditContributions), + }), + + slots: { + // This string is mandatory. + normalStringKey: {type: 'string'}, + + // This string is optional. + // Without it, there's no special behavior for "featuring" credits. + normalFeaturingStringKey: {type: 'string'}, + + // This string is optional. + // Without it, "featuring" credits will always be alongside main credits. + // It won't be used if contextContributions isn't provided. + featuringStringKey: {type: 'string'}, + + additionalStringOptions: {validate: v => v.isObject}, + + showAnnotation: {type: 'boolean', default: false}, + showExternalLinks: {type: 'boolean', default: false}, + showChronology: {type: 'boolean', default: false}, + showWikiEdits: {type: 'boolean', default: false}, + + trimAnnotation: {type: 'boolean', default: false}, + + chronologyKind: {type: 'string'}, + }, + + generate(data, relations, slots, {html, language}) { + if (!slots.normalStringKey) return html.blank(); + + for (const link of [ + ...relations.normalContributionLinks, + ...relations.featuringContributionLinks, + ]) { + link.setSlots({ + showExternalLinks: slots.showExternalLinks, + showChronology: slots.showChronology, + trimAnnotation: slots.trimAnnotation, + chronologyKind: slots.chronologyKind, + }); + } + + for (const link of relations.normalContributionLinks) { + link.setSlots({ + showAnnotation: slots.showAnnotation, + }); + } + + for (const link of relations.featuringContributionLinks) { + link.setSlots({ + showAnnotation: + (slots.featuringStringKey || slots.normalFeaturingStringKey + ? false + : slots.showAnnotation), + }); + } + + if (empty(relations.normalContributionLinks)) { + return html.blank(); + } + + const artistsList = + (data.hasWikiEdits && slots.showWikiEdits + ? language.$('misc.artistLink.withEditsForWiki', { + artists: + language.formatConjunctionList(relations.normalContributionLinks), + + edits: + relations.wikiEditsPart.slots({ + showAnnotation: slots.showAnnotation, + }), + }) + : language.formatConjunctionList(relations.normalContributionLinks)); + + const featuringList = + language.formatConjunctionList(relations.featuringContributionLinks); + + const everyoneList = + language.formatConjunctionList([ + ...relations.normalContributionLinks, + ...relations.featuringContributionLinks, + ]); + + const effectivelyDiffers = + (slots.showAnnotation && data.normalContributionAnnotationsDifferFromContext) || + (data.normalContributionArtistsDifferFromContext); + + if (empty(relations.featuringContributionLinks)) { + if (effectivelyDiffers) { + return language.$(slots.normalStringKey, { + ...slots.additionalStringOptions, + artists: artistsList, + }); + } else { + return html.blank(); + } + } + + if (effectivelyDiffers && slots.normalFeaturingStringKey) { + return language.$(slots.normalFeaturingStringKey, { + ...slots.additionalStringOptions, + artists: artistsList, + featuring: featuringList, + }); + } else if (slots.featuringStringKey) { + return language.$(slots.featuringStringKey, { + ...slots.additionalStringOptions, + artists: featuringList, + }); + } else { + return language.$(slots.normalStringKey, { + ...slots.additionalStringOptions, + artists: everyoneList, + }); + } + }, +}; diff --git a/src/content/dependencies/generateArtistCreditWikiEditsPart.js b/src/content/dependencies/generateArtistCreditWikiEditsPart.js new file mode 100644 index 00000000..70296e39 --- /dev/null +++ b/src/content/dependencies/generateArtistCreditWikiEditsPart.js @@ -0,0 +1,55 @@ +export default { + contentDependencies: [ + 'generateTextWithTooltip', + 'generateTooltip', + 'linkContribution', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, contributions) => ({ + textWithTooltip: + relation('generateTextWithTooltip'), + + tooltip: + relation('generateTooltip'), + + contributionLinks: + contributions + .map(contrib => relation('linkContribution', contrib)), + }), + + slots: { + showAnnotation: {type: 'boolean', default: true}, + }, + + generate: (relations, slots, {language}) => + language.encapsulate('misc.artistLink.withEditsForWiki', capsule => + relations.textWithTooltip.slots({ + attributes: + {class: 'wiki-edits'}, + + text: + language.$(capsule, 'edits'), + + tooltip: + relations.tooltip.slots({ + attributes: + {class: 'wiki-edits-tooltip'}, + + content: + language.$(capsule, 'editsLine', { + [language.onlyIfOptions]: ['artists'], + + artists: + language.formatConjunctionList( + relations.contributionLinks.map(link => + link.slots({ + showAnnotation: slots.showAnnotation, + trimAnnotation: true, + preventTooltip: true, + }))), + }), + }), + })), +}; diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js index 28f06a21..6a24275e 100644 --- a/src/content/dependencies/generateArtistGalleryPage.js +++ b/src/content/dependencies/generateArtistGalleryPage.js @@ -1,5 +1,4 @@ -import {sortAlbumsTracksChronologically} from '#sort'; -import {stitchArrays} from '#sugar'; +import {sortArtworksChronologically} from '#sort'; export default { contentDependencies: [ @@ -7,82 +6,59 @@ export default { 'generateCoverGrid', 'generatePageLayout', 'image', - 'linkAlbum', - 'linkTrack', + 'linkAnythingMan', ], extraDependencies: ['html', 'language'], - query(artist) { - const things = - ([ - artist.albumCoverArtistContributions, - artist.trackCoverArtistContributions, - ]).flat() - .map(({thing}) => thing); - - sortAlbumsTracksChronologically(things, { - latestFirst: true, - getDate: thing => thing.coverArtDate ?? thing.date, - }); - - return {things}; - }, - - relations(relation, query, artist) { - const relations = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.artistNavLinks = - relation('generateArtistNavLinks', artist); - - relations.coverGrid = - relation('generateCoverGrid'); - - relations.links = - query.things.map(thing => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))); - - relations.images = - query.things.map(thing => - relation('image', thing.artTags)); - - return relations; - }, - - data(query, artist) { - const data = {}; - - data.name = artist.name; - - data.numArtworks = query.things.length; - - data.names = - query.things.map(thing => thing.name); - - data.paths = - query.things.map(thing => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])); - - data.dimensions = - query.things.map(thing => thing.coverArtDimensions); - - data.otherCoverArtists = - query.things.map(thing => - (thing.coverArtistContribs.length > 1 - ? thing.coverArtistContribs - .filter(({artist: otherArtist}) => otherArtist !== artist) - .map(({artist: otherArtist}) => otherArtist.name) - : null)); - - return data; - }, + query: (artist) => ({ + artworks: + sortArtworksChronologically( + ([ + artist.albumCoverArtistContributions, + artist.trackCoverArtistContributions, + ]).flat() + .filter(contrib => !contrib.annotation?.startsWith(`edits for wiki`)) + .map(contrib => contrib.thing), + {latestFirst: true}), + }), + + relations: (relation, query, artist) => ({ + layout: + relation('generatePageLayout'), + + artistNavLinks: + relation('generateArtistNavLinks', artist), + + coverGrid: + relation('generateCoverGrid'), + + links: + query.artworks + .map(artwork => relation('linkAnythingMan', artwork.thing)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + }), + + data: (query, artist) => ({ + name: + artist.name, + + numArtworks: + query.artworks.length, + + names: + query.artworks + .map(artwork => artwork.thing.name), + + otherCoverArtists: + query.artworks + .map(artwork => artwork.artistContribs + .filter(contrib => contrib.artist !== artist) + .map(contrib => contrib.artist.name)), + }), generate: (data, relations, {html, language}) => language.encapsulate('artistGalleryPage', pageCapsule => @@ -99,7 +75,7 @@ export default { html.tag('p', {class: 'quick-info'}, language.$(pageCapsule, 'infoLine', { coverArts: - language.countCoverArts(data.numArtworks, { + language.countArtworks(data.numArtworks, { unit: true, }), })), @@ -107,27 +83,16 @@ export default { relations.coverGrid .slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, - })), - - // TODO: Can this be [language.onlyIfOptions]? info: data.otherCoverArtists.map(names => - (names === null - ? null - : language.$('misc.albumGrid.details.otherCoverArtists', { - artists: language.formatUnitList(names), - }))), + language.$('misc.coverGrid.details.otherCoverArtists', { + [language.onlyIfOptions]: ['artists'], + + artists: language.formatUnitList(names), + })), }), ], diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js index f84d00de..e1fa7a0b 100644 --- a/src/content/dependencies/generateArtistGroupContributionsInfo.js +++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js @@ -1,83 +1,90 @@ -import {empty, filterProperties, stitchArrays, unique} from '#sugar'; +import {accumulateSum, empty, stitchArrays, withEntries} from '#sugar'; export default { contentDependencies: ['linkGroup'], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({groupCategoryData}) { - return { - groupOrder: groupCategoryData.flatMap(category => category.groups), - } - }, + sprawl: ({groupCategoryData}) => ({ + groupOrder: + groupCategoryData.flatMap(category => category.groups), + }), - query(sprawl, tracksAndAlbums) { - const filteredAlbums = tracksAndAlbums.filter(thing => !thing.album); - const filteredTracks = tracksAndAlbums.filter(thing => thing.album); + query(sprawl, contributions) { + const allGroupsUnordered = + new Set(contributions.flatMap(contrib => contrib.groups)); - const allAlbums = unique([ - ...filteredAlbums, - ...filteredTracks.map(track => track.album), - ]); + const allGroupsOrdered = + sprawl.groupOrder.filter(group => allGroupsUnordered.has(group)); - const allGroupsUnordered = new Set(Array.from(allAlbums).flatMap(album => album.groups)); - const allGroupsOrdered = sprawl.groupOrder.filter(group => allGroupsUnordered.has(group)); + const groupToThingsCountedForContributions = + new Map(allGroupsOrdered.map(group => [group, new Set])); - const mapTemplate = allGroupsOrdered.map(group => [group, 0]); - const groupToCountMap = new Map(mapTemplate); - const groupToDurationMap = new Map(mapTemplate); - const groupToDurationCountMap = new Map(mapTemplate); + const groupToThingsCountedForDuration = + new Map(allGroupsOrdered.map(group => [group, new Set])); - for (const album of filteredAlbums) { - for (const group of album.groups) { - groupToCountMap.set(group, groupToCountMap.get(group) + 1); - } - } + for (const contrib of contributions) { + for (const group of contrib.groups) { + if (contrib.countInContributionTotals) { + groupToThingsCountedForContributions.get(group).add(contrib.thing); + } - for (const track of filteredTracks) { - for (const group of track.album.groups) { - groupToCountMap.set(group, groupToCountMap.get(group) + 1); - if (track.duration && track.originalReleaseTrack === null) { - groupToDurationMap.set(group, groupToDurationMap.get(group) + track.duration); - groupToDurationCountMap.set(group, groupToDurationCountMap.get(group) + 1); + if (contrib.countInDurationTotals) { + groupToThingsCountedForDuration.get(group).add(contrib.thing); } } } + const groupToTotalContributions = + withEntries( + groupToThingsCountedForContributions, + entries => entries.map( + ([group, things]) => + ([group, things.size]))); + + const groupToTotalDuration = + withEntries( + groupToThingsCountedForDuration, + entries => entries.map( + ([group, things]) => + ([group, accumulateSum(things, thing => thing.duration)]))) + const groupsSortedByCount = allGroupsOrdered - .slice() - .sort((a, b) => groupToCountMap.get(b) - groupToCountMap.get(a)); + .filter(group => groupToTotalContributions.get(group) > 0) + .sort((a, b) => + (groupToTotalContributions.get(b) + - groupToTotalContributions.get(a))); - // The filter here ensures all displayed groups have at least some duration - // when sorting by duration. const groupsSortedByDuration = allGroupsOrdered - .filter(group => groupToDurationMap.get(group) > 0) - .sort((a, b) => groupToDurationMap.get(b) - groupToDurationMap.get(a)); + .filter(group => groupToTotalDuration.get(group) > 0) + .sort((a, b) => + (groupToTotalDuration.get(b) + - groupToTotalDuration.get(a))); const groupCountsSortedByCount = groupsSortedByCount - .map(group => groupToCountMap.get(group)); + .map(group => groupToTotalContributions.get(group)); const groupDurationsSortedByCount = groupsSortedByCount - .map(group => groupToDurationMap.get(group)); + .map(group => groupToTotalDuration.get(group)); const groupDurationsApproximateSortedByCount = groupsSortedByCount - .map(group => groupToDurationCountMap.get(group) > 1); + .map(group => groupToThingsCountedForDuration.get(group).size > 1); const groupCountsSortedByDuration = groupsSortedByDuration - .map(group => groupToCountMap.get(group)); + .map(group => groupToTotalContributions.get(group)); const groupDurationsSortedByDuration = groupsSortedByDuration - .map(group => groupToDurationMap.get(group)); + .map(group => groupToTotalDuration.get(group)); const groupDurationsApproximateSortedByDuration = groupsSortedByDuration - .map(group => groupToDurationCountMap.get(group) > 1); + .map(group => groupToThingsCountedForDuration.get(group).size > 1); return { groupsSortedByCount, @@ -93,29 +100,35 @@ export default { }; }, - relations(relation, query) { - return { - groupLinksSortedByCount: - query.groupsSortedByCount - .map(group => relation('linkGroup', group)), + relations: (relation, query) => ({ + groupLinksSortedByCount: + query.groupsSortedByCount + .map(group => relation('linkGroup', group)), - groupLinksSortedByDuration: - query.groupsSortedByDuration - .map(group => relation('linkGroup', group)), - }; - }, + groupLinksSortedByDuration: + query.groupsSortedByDuration + .map(group => relation('linkGroup', group)), + }), - data(query) { - return filterProperties(query, [ - 'groupCountsSortedByCount', - 'groupDurationsSortedByCount', - 'groupDurationsApproximateSortedByCount', + data: (query) => ({ + groupCountsSortedByCount: + query.groupCountsSortedByCount, - 'groupCountsSortedByDuration', - 'groupDurationsSortedByDuration', - 'groupDurationsApproximateSortedByDuration', - ]); - }, + groupDurationsSortedByCount: + query.groupDurationsSortedByCount, + + groupDurationsApproximateSortedByCount: + query.groupDurationsApproximateSortedByCount, + + groupCountsSortedByDuration: + query.groupCountsSortedByDuration, + + groupDurationsSortedByDuration: + query.groupDurationsSortedByDuration, + + groupDurationsApproximateSortedByDuration: + query.groupDurationsApproximateSortedByDuration, + }), slots: { title: { diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index f9ce7e3b..1f738de4 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -1,7 +1,8 @@ -import {empty, unique} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; export default { contentDependencies: [ + 'generateArtistArtworkColumn', 'generateArtistGroupContributionsInfo', 'generateArtistInfoPageArtworksChunkedList', 'generateArtistInfoPageCommentaryChunkedList', @@ -9,44 +10,43 @@ export default { 'generateArtistInfoPageTracksChunkedList', 'generateArtistNavLinks', 'generateContentHeading', - 'generateCoverArtwork', 'generatePageLayout', 'linkArtistGallery', 'linkExternal', + 'linkGroup', 'transformContent', ], 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), + trackContributions: [ + ...artist.trackArtistContributions, + ...artist.trackContributorContributions, + ], + + artworkContributions: [ + ...artist.albumCoverArtistContributions, + ...artist.albumWallpaperArtistContributions, + ...artist.albumBannerArtistContributions, + ...artist.trackCoverArtistContributions, + ], // Banners and wallpapers don't show up in the artist gallery page, only // cover art. hasGallery: !empty(artist.albumCoverArtistContributions) || !empty(artist.trackCoverArtistContributions), + + aliasLinkedGroups: + artist.closelyLinkedGroups + .filter(({annotation}) => + annotation === 'alias'), + + generalLinkedGroups: + artist.closelyLinkedGroups + .filter(({annotation}) => + annotation !== 'alias'), }), relations: (relation, query, artist) => ({ @@ -56,10 +56,8 @@ export default { artistNavLinks: relation('generateArtistNavLinks', artist), - cover: - (artist.hasAvatar - ? relation('generateCoverArtwork', []) - : null), + artworkColumn: + relation('generateArtistArtworkColumn', artist), contentHeading: relation('generateContentHeading'), @@ -67,6 +65,14 @@ export default { contextNotes: relation('transformContent', artist.contextNotes), + closeGroupLinks: + query.generalLinkedGroups + .map(({group}) => relation('linkGroup', group)), + + aliasGroupLinks: + query.aliasLinkedGroups + .map(({group}) => relation('linkGroup', group)), + visitLinks: artist.urls .map(url => relation('linkExternal', url)), @@ -75,13 +81,16 @@ export default { relation('generateArtistInfoPageTracksChunkedList', artist), tracksGroupInfo: - relation('generateArtistGroupContributionsInfo', query.allTracks), + relation('generateArtistGroupContributionsInfo', query.trackContributions), artworksChunkedList: - relation('generateArtistInfoPageArtworksChunkedList', artist), + relation('generateArtistInfoPageArtworksChunkedList', artist, false), + + editsForWikiArtworksChunkedList: + relation('generateArtistInfoPageArtworksChunkedList', artist, true), artworksGroupInfo: - relation('generateArtistGroupContributionsInfo', query.allArtworks), + relation('generateArtistGroupContributionsInfo', query.artworkContributions), artistGalleryLink: (query.hasGallery @@ -92,23 +101,26 @@ export default { relation('generateArtistInfoPageFlashesChunkedList', artist), commentaryChunkedList: - relation('generateArtistInfoPageCommentaryChunkedList', artist), + relation('generateArtistInfoPageCommentaryChunkedList', artist, false), + + wikiEditorCommentaryChunkedList: + relation('generateArtistInfoPageCommentaryChunkedList', artist, true), }), data: (query, artist) => ({ name: artist.name, - directory: - artist.directory, - - avatarFileExtension: - (artist.hasAvatar - ? artist.avatarFileExtension - : null), + closeGroupAnnotations: + query.generalLinkedGroups + .map(({annotation}) => annotation), totalTrackCount: - query.allTracks.length, + unique( + query.trackContributions + .filter(contrib => contrib.countInContributionTotals) + .map(contrib => contrib.thing)) + .length, totalDuration: artist.totalDuration, @@ -120,16 +132,8 @@ export default { title: data.name, headingMode: 'sticky', - cover: - (relations.cover - ? relations.cover.slots({ - path: [ - 'media.artistAvatar', - data.directory, - data.avatarFileExtension, - ], - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ html.tags([ @@ -144,6 +148,49 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + language.encapsulate(pageCapsule, 'closelyLinkedGroups', capsule => [ + language.encapsulate(capsule, capsule => { + const [workingCapsule, option] = + (relations.closeGroupLinks.length === 0 + ? [null, null] + : relations.closeGroupLinks.length === 1 + ? [language.encapsulate(capsule, 'one'), 'group'] + : [language.encapsulate(capsule, 'multiple'), 'groups']); + + if (!workingCapsule) return html.blank(); + + return language.$(workingCapsule, { + [option]: + language.formatUnitList( + stitchArrays({ + link: relations.closeGroupLinks, + annotation: data.closeGroupAnnotations, + }).map(({link, annotation}) => + language.encapsulate(capsule, 'group', workingCapsule => { + const workingOptions = {group: link}; + + if (annotation) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = annotation; + } + + return language.$(workingCapsule, workingOptions); + }))), + }); + }), + + language.$(capsule, 'alias', { + [language.onlyIfOptions]: ['groups'], + + groups: + language.formatConjunctionList(relations.aliasGroupLinks), + }), + ])), + + html.tag('p', + {[html.onlyIfContent]: true}, language.$('releaseInfo.visitOn', { [language.onlyIfOptions]: ['links'], @@ -181,17 +228,19 @@ export default { {href: '#tracks'}, language.$(pageCapsule, 'trackList.title')), - !html.isBlank(relations.artworksChunkedList) && - html.tag('a', - {href: '#art'}, - language.$(pageCapsule, 'artList.title')), + (!html.isBlank(relations.artworksChunkedList) || + !html.isBlank(relations.editsForWikiArtworksChunkedList)) && + 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.isBlank(relations.commentaryChunkedList) || + !html.isBlank(relations.wikiEditorCommentaryChunkedList)) && html.tag('a', {href: '#commentary'}, language.$(pageCapsule, 'commentaryList.title')), @@ -276,6 +325,22 @@ export default { countUnit: 'artworks', })), }), + + html.tags([ + language.encapsulate(pageCapsule, 'wikiEditArtworks', capsule => + relations.contentHeading.clone() + .slots({ + tag: 'p', + + title: + language.$(capsule, {artist: data.name}), + + stickyTitle: + language.$(capsule, 'sticky'), + })), + + relations.editsForWikiArtworksChunkedList, + ]), ]), html.tags([ @@ -298,6 +363,22 @@ export default { }), relations.commentaryChunkedList, + + html.tags([ + language.encapsulate(pageCapsule, 'wikiEditorCommentary', capsule => + relations.contentHeading.clone() + .slots({ + tag: 'p', + + title: + language.$(capsule, {artist: data.name}), + + stickyTitle: + language.$(capsule, 'sticky'), + })), + + relations.wikiEditorCommentaryChunkedList, + ]), ]), ], diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js index 2b10df3e..66e4204a 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js @@ -5,6 +5,8 @@ export default { 'linkAlbum', ], + extraDependencies: ['html'], + relations: (relation, album, contribs) => ({ template: relation('generateArtistInfoPageChunk'), @@ -24,11 +26,25 @@ export default { .map(contrib => contrib.date), }), - generate: (data, relations) => + slots: { + filterEditsForWiki: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots) => relations.template.slots({ mode: 'album', albumLink: relations.albumLink, - dates: data.dates, - items: relations.items, + + dates: + (slots.filterEditsForWiki + ? Array.from({length: data.dates}, () => null) + : data.dates), + + items: + relations.items.map(item => + item.slot('filterEditsForWiki', slots.filterEditsForWiki)), }), }; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js index e8d887b1..cb436b0f 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js @@ -1,8 +1,11 @@ +import {empty} from '#sugar'; + export default { contentDependencies: [ 'generateArtistInfoPageChunkItem', 'generateArtistInfoPageOtherArtistLinks', 'linkTrack', + 'transformContent', ], extraDependencies: ['html', 'language'], @@ -24,11 +27,14 @@ export default { trackLink: (query.kind === 'track-cover' - ? relation('linkTrack', contrib.thing) + ? relation('linkTrack', contrib.thing.thing) : null), otherArtistLinks: relation('generateArtistInfoPageOtherArtistLinks', [contrib]), + + originDetails: + relation('transformContent', contrib.thing.originDetails), }), data: (query, contrib) => ({ @@ -37,13 +43,50 @@ export default { annotation: contrib.annotation, + + label: + contrib.thing.label, }), - generate: (data, relations, {html, language}) => + slots: { + filterEditsForWiki: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots, {html, language}) => relations.template.slots({ otherArtistLinks: relations.otherArtistLinks, - annotation: data.annotation, + annotation: + language.encapsulate('artistPage.creditList.entry.artwork.accent', workingCapsule => { + const workingOptions = {}; + + const artworkLabel = data.label; + + if (artworkLabel) { + workingCapsule += '.withLabel'; + workingOptions.label = + language.typicallyLowerCase(artworkLabel); + } + + const contribAnnotation = + (slots.filterEditsForWiki + ? data.annotation?.replace(/^edits for wiki(: )?/, '') + : data.annotation); + + if (contribAnnotation) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = contribAnnotation; + } + + if (empty(Object.keys(workingOptions))) { + return html.blank(); + } + + return language.$(workingCapsule, workingOptions); + }), content: language.encapsulate('artistPage.creditList.entry', capsule => @@ -58,5 +101,11 @@ export default { : data.kind === 'banner' ? language.$(capsule, 'bannerArt') : language.$(capsule, 'coverArt')))))), + + originDetails: + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }), }), }; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index caefb7a3..75a4aa5a 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -8,7 +8,7 @@ export default { 'generateArtistInfoPageArtworksChunk', ], - query(artist) { + query(artist, filterEditsForWiki) { const query = {}; const allContributions = [ @@ -18,28 +18,36 @@ export default { ...artist.trackCoverArtistContributions, ]; + const filteredContributions = + allContributions + .filter(({annotation}) => + (filterEditsForWiki + ? annotation?.startsWith(`edits for wiki`) + : !annotation?.startsWith(`edits for wiki`))); + sortContributionsChronologically( - allContributions, - sortAlbumsTracksChronologically); + filteredContributions, + sortAlbumsTracksChronologically, + {getThing: contrib => contrib.thing.thing}); query.contribs = - chunkByConditions(allContributions, [ + chunkByConditions(filteredContributions, [ ({date: date1}, {date: date2}) => +date1 !== +date2, - ({thing: thing1}, {thing: thing2}) => + ({thing: {thing: thing1}}, {thing: {thing: thing2}}) => (thing1.album ?? thing1) !== (thing2.album ?? thing2), ]); query.albums = query.contribs - .map(contribs => contribs[0].thing) + .map(contribs => contribs[0].thing.thing) .map(thing => thing.album ?? thing); return query; }, - relations: (relation, query, _artist) => ({ + relations: (relation, query, _artist, _filterEditsForWiki) => ({ chunkedList: relation('generateArtistInfoPageChunkedList'), @@ -51,8 +59,14 @@ export default { relation('generateArtistInfoPageArtworksChunk', album, contribs)), }), - generate: (relations) => + data: (_query, _artist, filterEditsForWiki) => ({ + filterEditsForWiki, + }), + + generate: (data, relations) => relations.chunkedList.slots({ - chunks: relations.chunks, + chunks: + relations.chunks.map(chunk => + chunk.slot('filterEditsForWiki', data.filterEditsForWiki)), }), }; diff --git a/src/content/dependencies/generateArtistInfoPageChunk.js b/src/content/dependencies/generateArtistInfoPageChunk.js index c16d50f3..fce68a7d 100644 --- a/src/content/dependencies/generateArtistInfoPageChunk.js +++ b/src/content/dependencies/generateArtistInfoPageChunk.js @@ -8,6 +8,8 @@ export default { validate: v => v.is('flash', 'album'), }, + id: {type: 'string'}, + albumLink: { type: 'html', mutable: false, @@ -99,9 +101,13 @@ export default { } return html.tags([ - html.tag('dt', accentedLink), + html.tag('dt', + slots.id && {id: slots.id}, + accentedLink), + html.tag('dd', html.tag('ul', + {class: 'offset-tooltips'}, slots.items)), ]); }, diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js index 9d406c67..c80aeab7 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js @@ -1,8 +1,14 @@ import {empty} from '#sugar'; export default { + contentDependencies: ['generateTextWithTooltip'], extraDependencies: ['html', 'language'], + relations: (relation) => ({ + textWithTooltip: + relation('generateTextWithTooltip'), + }), + slots: { content: { type: 'html', @@ -18,41 +24,80 @@ export default { validate: v => v.strictArrayOf(v.isHTML), }, - rerelease: {type: 'boolean'}, + rereleaseTooltip: { + type: 'html', + mutable: false, + }, + + firstReleaseTooltip: { + type: 'html', + mutable: false, + }, + + originDetails: { + type: 'html', + mutable: false, + }, }, - generate: (slots, {html, language}) => + generate: (relations, slots, {html, language}) => language.encapsulate('artistPage.creditList.entry', entryCapsule => html.tag('li', slots.rerelease && {class: 'rerelease'}, - 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; - } - }))), + html.tags([ + language.encapsulate(entryCapsule, workingCapsule => { + const workingOptions = {entry: slots.content}; + + if (!html.isBlank(slots.rereleaseTooltip)) { + workingCapsule += '.rerelease'; + workingOptions.rerelease = + relations.textWithTooltip.slots({ + attributes: {class: 'rerelease'}, + text: language.$(entryCapsule, 'rerelease.term'), + tooltip: slots.rereleaseTooltip, + }); + + return language.$(workingCapsule, workingOptions); + } + + if (!html.isBlank(slots.firstReleaseTooltip)) { + workingCapsule += '.firstRelease'; + workingOptions.firstRelease = + relations.textWithTooltip.slots({ + attributes: {class: 'first-release'}, + text: language.$(entryCapsule, 'firstRelease.term'), + tooltip: slots.firstReleaseTooltip, + }); + + return language.$(workingCapsule, workingOptions); + } + + let anyAccent = false; + + if (!empty(slots.otherArtistLinks)) { + anyAccent = true; + workingCapsule += '.withArtists'; + workingOptions.artists = + language.formatConjunctionList(slots.otherArtistLinks); + } + + if (!html.isBlank(slots.annotation)) { + anyAccent = true; + workingCapsule += '.withAnnotation'; + workingOptions.annotation = slots.annotation; + } + + if (anyAccent) { + return language.$(workingCapsule, workingOptions); + } else { + return slots.content; + } + }), + + html.tag('span', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + slots.originDetails), + ]))), }; diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js index 72bbf1b6..88c5ed54 100644 --- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js @@ -19,7 +19,7 @@ export default { extraDependencies: ['html', 'language'], - query(artist) { + query(artist, filterWikiEditorCommentary) { const processEntry = ({ thing, entry, @@ -43,6 +43,7 @@ export default { flash, annotation: entry.annotation, + annotationParts: entry.annotationParts, }, }); @@ -87,6 +88,12 @@ export default { .flatMap(thing => thing.commentary .filter(entry => entry.artists.includes(artist)) + + .filter(entry => + (filterWikiEditorCommentary + ? entry.isWikiEditorCommentary + : !entry.isWikiEditorCommentary)) + .map(entry => processEntry({thing, entry}))); const processAlbumEntries = ({albums}) => @@ -146,7 +153,7 @@ export default { return {chunks}; }, - relations: (relation, query) => ({ + relations: (relation, query, _artist, filterWikiEditorCommentary) => ({ chunks: query.chunks .map(() => relation('generateArtistInfoPageChunk')), @@ -178,13 +185,16 @@ export default { itemAnnotations: query.chunks .map(({chunk}) => chunk - .map(({annotation}) => - (annotation - ? relation('transformContent', annotation) - : null))), + .map(entry => + relation('transformContent', + (filterWikiEditorCommentary + ? entry.annotationParts + .filter(part => part !== 'wiki editor') + .join(', ') + : entry.annotation)))), }), - data: (query) => ({ + data: (query, _artist, _filterWikiEditorCommentary) => ({ chunkTypes: query.chunks .map(({chunkType}) => chunkType), @@ -197,6 +207,8 @@ export default { generate: (data, relations, {html, language}) => html.tag('dl', + {[html.onlyIfContent]: true}, + stitchArrays({ chunk: relations.chunks, chunkLink: relations.chunkLinks, @@ -230,9 +242,10 @@ export default { }).map(({item, link, annotation, type}) => item.slots({ annotation: - (annotation - ? annotation.slot('mode', 'inline') - : null), + annotation.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }), content: (type === 'album' @@ -254,7 +267,10 @@ export default { item.slots({ annotation: (annotation - ? annotation.slot('mode', 'inline') + ? annotation.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }) : null), content: diff --git a/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js b/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js new file mode 100644 index 00000000..f86dead7 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js @@ -0,0 +1,75 @@ +import {sortChronologically} from '#sort'; +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generateTooltip', + 'linkOtherReleaseOnArtistInfoPage', + ], + + extraDependencies: ['html', 'language'], + + query: (track) => ({ + rereleases: + sortChronologically(track.allReleases).slice(1), + }), + + relations: (relation, query, track, artist) => ({ + tooltip: + relation('generateTooltip'), + + firstReleaseColorStyle: + relation('generateColorStyleAttribute', track.color), + + rereleaseLinks: + query.rereleases + .map(rerelease => + relation('linkOtherReleaseOnArtistInfoPage', rerelease, artist)), + }), + + data: (query, track) => ({ + firstReleaseDate: + track.dateFirstReleased ?? + track.album.date, + + rereleaseDates: + query.rereleases + .map(rerelease => + rerelease.dateFirstReleased ?? + rerelease.album.date), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('artistPage.creditList.entry.firstRelease', capsule => + relations.tooltip.slots({ + attributes: [ + {class: 'first-release-tooltip'}, + relations.firstReleaseColorStyle, + ], + + contentAttributes: [ + {[html.joinChildren]: html.tag('hr', {class: 'cute'})}, + ], + + content: + stitchArrays({ + rereleaseLink: relations.rereleaseLinks, + rereleaseDate: data.rereleaseDates, + }).map(({rereleaseLink, rereleaseDate}) => + html.tags([ + language.$(capsule, 'rerelease', { + album: + html.metatag('blockwrap', rereleaseLink), + }), + + html.tag('br'), + + language.formatRelativeDate(rereleaseDate, data.firstReleaseDate, { + considerRoundingDays: true, + approximate: true, + absolute: true, + }), + ])), + })), +}; diff --git a/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js b/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js new file mode 100644 index 00000000..1d849919 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js @@ -0,0 +1,61 @@ +import {sortChronologically} from '#sort'; + +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generateTooltip', + 'linkOtherReleaseOnArtistInfoPage' + ], + + extraDependencies: ['html', 'language'], + + query: (track) => ({ + firstRelease: + sortChronologically(track.allReleases)[0], + }), + + relations: (relation, query, track, artist) => ({ + tooltip: + relation('generateTooltip'), + + rereleaseColorStyle: + relation('generateColorStyleAttribute', track.color), + + firstReleaseLink: + relation('linkOtherReleaseOnArtistInfoPage', query.firstRelease, artist), + }), + + data: (query, track) => ({ + rereleaseDate: + track.dateFirstReleased ?? + track.album.date, + + firstReleaseDate: + query.firstRelease.dateFirstReleased ?? + query.firstRelease.album.date, + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('artistPage.creditList.entry.rerelease', capsule => + relations.tooltip.slots({ + attributes: [ + {class: 'rerelease-tooltip'}, + relations.rereleaseColorStyle, + ], + + content: [ + language.$(capsule, 'firstRelease', { + album: + html.metatag('blockwrap', relations.firstReleaseLink), + }), + + html.tag('br'), + + language.formatRelativeDate(data.firstReleaseDate, data.rereleaseDate, { + considerRoundingDays: true, + approximate: true, + absolute: true, + }), + ], + })), +}; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunk.js b/src/content/dependencies/generateArtistInfoPageTracksChunk.js index b42e4165..f6d70901 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunk.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js @@ -40,7 +40,7 @@ export default { contribs .filter(contrib => contrib.countInDurationTotals) .map(contrib => contrib.thing) - .filter(track => track.isOriginalRelease) + .filter(track => track.isMainRelease) .filter(track => track.duration > 0)); data.duration = diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js index 96976826..a42d6fee 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js @@ -1,9 +1,12 @@ +import {sortChronologically} from '#sort'; import {empty} from '#sugar'; export default { contentDependencies: [ 'generateArtistInfoPageChunkItem', + 'generateArtistInfoPageFirstReleaseTooltip', 'generateArtistInfoPageOtherArtistLinks', + 'generateArtistInfoPageRereleaseTooltip', 'linkTrack', ], @@ -61,6 +64,26 @@ export default { ]; } + // It's kinda awkward to perform this chronological sort here, + // per track, rather than just reusing the one that's done to + // sort all the items on the page altogether... but then, the + // sort for the page is actually *a different* sort, on purpsoe. + // That sort is according to the dates of the contributions; + // this is according to the dates of the tracks. Those can be + // different - and it's the latter that determines whether the + // track is a rerelease! + const allReleasesChronologically = + sortChronologically(query.track.allReleases); + + query.isFirstRelease = + allReleasesChronologically[0] === query.track; + + query.isRerelease = + allReleasesChronologically[0] !== query.track; + + query.hasOtherReleases = + !empty(query.track.otherReleases); + return query; }, @@ -73,15 +96,22 @@ export default { otherArtistLinks: relation('generateArtistInfoPageOtherArtistLinks', contribs), + + rereleaseTooltip: + (query.isRerelease + ? relation('generateArtistInfoPageRereleaseTooltip', query.track, artist) + : null), + + firstReleaseTooltip: + (query.isFirstRelease && query.hasOtherReleases + ? relation('generateArtistInfoPageFirstReleaseTooltip', query.track, artist) + : null), }), data: (query) => ({ duration: query.track.duration, - rerelease: - query.track.isRerelease, - contribAnnotations: (query.displayedContributions ? query.displayedContributions @@ -92,7 +122,8 @@ export default { generate: (data, relations, {html, language}) => relations.template.slots({ otherArtistLinks: relations.otherArtistLinks, - rerelease: data.rerelease, + rereleaseTooltip: relations.rereleaseTooltip, + firstReleaseTooltip: relations.firstReleaseTooltip, annotation: (data.contribAnnotations diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index 7c01accb..84eb29ac 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -1,6 +1,7 @@ import {sortAlbumsTracksChronologically, sortContributionsChronologically} from '#sort'; -import {chunkByConditions, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; +import {chunkArtistTrackContributions} from '#wiki-data'; export default { contentDependencies: [ @@ -21,19 +22,7 @@ export default { 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, - ])); + chunkArtistTrackContributions(allContributions); query.albums = query.contribs @@ -58,8 +47,35 @@ export default { contribs)), }), - generate: (relations) => + data: (query, _artist) => ({ + albumDirectories: + query.albums + .map(album => album.directory), + + albumChunkIndices: + query.albums + .reduce(([indices, map], album) => { + if (map.has(album)) { + const n = map.get(album); + indices.push(n); + map.set(album, n + 1); + } else { + indices.push(0); + map.set(album, 1); + } + return [indices, map]; + }, [[], new Map()]) + [0], + }), + + generate: (data, relations) => relations.chunkedList.slots({ - chunks: relations.chunks, + chunks: + stitchArrays({ + chunk: relations.chunks, + albumDirectory: data.albumDirectories, + albumChunkIndex: data.albumChunkIndices, + }).map(({chunk, albumDirectory, albumChunkIndex}) => + chunk.slot('id', `tracks-${albumDirectory}-${albumChunkIndex}`)), }), }; diff --git a/src/content/dependencies/generateArtistNavLinks.js b/src/content/dependencies/generateArtistNavLinks.js index 527e4741..1b4b6eca 100644 --- a/src/content/dependencies/generateArtistNavLinks.js +++ b/src/content/dependencies/generateArtistNavLinks.js @@ -2,43 +2,44 @@ import {empty} from '#sugar'; export default { contentDependencies: [ + 'generateInterpageDotSwitcher', 'linkArtist', 'linkArtistGallery', ], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({wikiInfo}) { - return { - enableListings: wikiInfo.enableListings, - }; - }, + sprawl: ({wikiInfo}) => ({ + enableListings: + wikiInfo.enableListings, + }), - relations(relation, sprawl, artist) { - const relations = {}; + query: (_sprawl, artist) => ({ + hasGallery: + !empty(artist.albumCoverArtistContributions) || + !empty(artist.trackCoverArtistContributions), + }), - relations.artistMainLink = - relation('linkArtist', artist); + relations: (relation, query, _sprawl, artist) => ({ + switcher: + relation('generateInterpageDotSwitcher'), - relations.artistInfoLink = - relation('linkArtist', artist); + artistMainLink: + relation('linkArtist', artist), - if ( - !empty(artist.albumCoverArtistContributions) || - !empty(artist.trackCoverArtistContributions) - ) { - relations.artistGalleryLink = - relation('linkArtistGallery', artist); - } + artistInfoLink: + relation('linkArtist', artist), - return relations; - }, + artistGalleryLink: + (query.hasGallery + ? relation('linkArtistGallery', artist) + : null), + }), - data(sprawl) { - return { - enableListings: sprawl.enableListings, - }; - }, + data: (_query, sprawl) => ({ + enableListings: + sprawl.enableListings, + }), slots: { showExtraLinks: {type: 'boolean', default: false}, @@ -48,53 +49,46 @@ export default { }, }, - generate(data, relations, slots, {html, language}) { - const infoLink = - relations.artistInfoLink?.slots({ - attributes: {class: slots.currentExtra === null && 'current'}, - content: language.$('misc.nav.info'), - }); - - const {content: extraLinks = []} = - slots.showExtraLinks && - {content: [ - relations.artistGalleryLink?.slots({ - attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('misc.nav.gallery'), - }), - ]}; - - const mostAccentLinks = [ - ...extraLinks, - ].filter(Boolean); - - // Don't show the info accent link all on its own. - const allAccentLinks = - (empty(mostAccentLinks) - ? [] - : [infoLink, ...mostAccentLinks]); - - const accent = - (empty(allAccentLinks) - ? html.blank() - : `(${language.formatUnitList(allAccentLinks)})`); - - return [ - {auto: 'home'}, - - data.enableListings && - { - path: ['localized.listingIndex'], - title: language.$('listingIndex.title'), - }, + generate: (data, relations, slots, {html, language}) => [ + {auto: 'home'}, + data.enableListings && { - accent, - html: - language.$('artistPage.nav.artist', { - artist: relations.artistMainLink, - }), + path: ['localized.listingIndex'], + title: language.$('listingIndex.title'), }, - ]; - }, + + { + html: + language.$('artistPage.nav.artist', { + artist: relations.artistMainLink, + }), + + accent: + relations.switcher.slots({ + links: [ + relations.artistInfoLink.slots({ + attributes: [ + slots.currentExtra === null && + {class: 'current'}, + + {[html.onlyIfSiblings]: true}, + ], + + content: language.$('misc.nav.info'), + }), + + slots.showExtraLinks && + relations.artistGalleryLink?.slots({ + attributes: [ + slots.currentExtra === 'gallery' && + {class: 'current'}, + ], + + content: language.$('misc.nav.gallery'), + }), + ], + }), + }, + ], }; diff --git a/src/content/dependencies/generateBackToAlbumLink.js b/src/content/dependencies/generateBackToAlbumLink.js new file mode 100644 index 00000000..6648b463 --- /dev/null +++ b/src/content/dependencies/generateBackToAlbumLink.js @@ -0,0 +1,15 @@ +export default { + contentDependencies: ['linkAlbum'], + extraDependencies: ['language'], + + relations: (relation, track) => ({ + trackLink: + relation('linkAlbum', track), + }), + + generate: (relations, {language}) => + relations.trackLink.slots({ + content: language.$('albumPage.nav.backToAlbum'), + color: false, + }), +}; diff --git a/src/content/dependencies/generateBackToTrackLink.js b/src/content/dependencies/generateBackToTrackLink.js new file mode 100644 index 00000000..8677d811 --- /dev/null +++ b/src/content/dependencies/generateBackToTrackLink.js @@ -0,0 +1,15 @@ +export default { + contentDependencies: ['linkTrack'], + extraDependencies: ['language'], + + relations: (relation, track) => ({ + trackLink: + relation('linkTrack', track), + }), + + generate: (relations, {language}) => + relations.trackLink.slots({ + content: language.$('trackPage.nav.backToTrack'), + color: false, + }), +}; diff --git a/src/content/dependencies/generateColorStyleRules.js b/src/content/dependencies/generateColorStyleRules.js deleted file mode 100644 index c412b8f2..00000000 --- a/src/content/dependencies/generateColorStyleRules.js +++ /dev/null @@ -1,42 +0,0 @@ -export default { - contentDependencies: ['generateColorStyleVariables'], - extraDependencies: ['html'], - - relations: (relation) => ({ - variables: - relation('generateColorStyleVariables'), - }), - - data: (color) => ({ - color: - color ?? null, - }), - - slots: { - color: { - validate: v => v.isColor, - }, - }, - - generate(data, relations, slots) { - const color = data.color ?? slots.color; - - if (!color) { - return ''; - } - - return [ - `:root {`, - ...( - relations.variables - .slots({ - color, - context: 'page-root', - mode: 'property-list', - }) - .content - .map(line => line + ';')), - `}`, - ].join('\n'); - }, -}; diff --git a/src/content/dependencies/generateColorStyleTag.js b/src/content/dependencies/generateColorStyleTag.js new file mode 100644 index 00000000..2b1a21dd --- /dev/null +++ b/src/content/dependencies/generateColorStyleTag.js @@ -0,0 +1,51 @@ +export default { + contentDependencies: ['generateColorStyleVariables', 'generateStyleTag'], + extraDependencies: ['html'], + + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + + variables: + relation('generateColorStyleVariables'), + }), + + data: (color) => ({ + color: + color ?? null, + }), + + slots: { + color: { + validate: v => v.isColor, + }, + }, + + generate(data, relations, slots, {html}) { + const color = + data.color ?? slots.color; + + if (!color) { + return html.blank(); + } + + return relations.styleTag.slots({ + attributes: [ + {class: 'color-style'}, + {'data-color': color}, + ], + + rules: [ + { + select: ':root', + declare: + relations.variables.slots({ + color, + context: 'page-root', + mode: 'declarations', + }).content, + }, + ], + }); + }, +}; diff --git a/src/content/dependencies/generateColorStyleVariables.js b/src/content/dependencies/generateColorStyleVariables.js index 5270dbe4..c872d0b6 100644 --- a/src/content/dependencies/generateColorStyleVariables.js +++ b/src/content/dependencies/generateColorStyleVariables.js @@ -18,7 +18,7 @@ export default { }, mode: { - validate: v => v.is('style', 'property-list'), + validate: v => v.is('style', 'declarations'), default: 'style', }, }, @@ -50,15 +50,15 @@ export default { `--shadow-color: ${shadow}`, ]; - let selectedProperties; + let selectedDeclarations; switch (slots.context) { case 'any-content': - selectedProperties = anyContent; + selectedDeclarations = anyContent; break; case 'image-box': - selectedProperties = [ + selectedDeclarations = [ `--primary-color: ${primary}`, `--dim-color: ${dim}`, `--deep-color: ${deep}`, @@ -67,14 +67,14 @@ export default { break; case 'page-root': - selectedProperties = [ + selectedDeclarations = [ ...anyContent, `--page-primary-color: ${primary}`, ]; break; case 'primary-only': - selectedProperties = [ + selectedDeclarations = [ `--primary-color: ${primary}`, ]; break; @@ -82,10 +82,10 @@ export default { switch (slots.mode) { case 'style': - return selectedProperties.join('; '); + return selectedDeclarations.join('; '); - case 'property-list': - return selectedProperties; + case 'declarations': + return selectedDeclarations.map(declaration => declaration + ';'); } }, }; diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index 7c4aed80..367de506 100644 --- a/src/content/dependencies/generateCommentaryEntry.js +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -2,6 +2,7 @@ import {empty} from '#sugar'; export default { contentDependencies: [ + 'generateCommentaryEntryDate', 'generateColorStyleAttribute', 'linkArtist', 'transformContent', @@ -11,14 +12,14 @@ export default { relations: (relation, entry) => ({ artistLinks: - (!empty(entry.artists) && !entry.artistDisplayText + (!empty(entry.artists) && !entry.artistText ? entry.artists .map(artist => relation('linkArtist', artist)) : null), artistsContent: - (entry.artistDisplayText - ? relation('transformContent', entry.artistDisplayText) + (entry.artistText + ? relation('transformContent', entry.artistText) : null), annotationContent: @@ -33,17 +34,16 @@ export default { colorStyle: relation('generateColorStyleAttribute'), - }), - data: (entry) => ({ - date: entry.date, + date: + relation('generateCommentaryEntryDate', entry), }), slots: { color: {validate: v => v.isColor}, }, - generate: (data, relations, slots, {html, language}) => + generate: (relations, slots, {html, language}) => language.encapsulate('misc.artistCommentary.entry', entryCapsule => html.tags([ html.tag('p', {class: 'commentary-entry-heading'}, @@ -51,56 +51,55 @@ export default { relations.colorStyle.clone() .slot('color', slots.color), - language.encapsulate(entryCapsule, 'title', titleCapsule => [ - html.tag('time', - {[html.onlyIfContent]: true}, - - language.$(titleCapsule, 'date', { - [language.onlyIfOptions]: ['date'], + !html.isBlank(relations.date) && + {class: 'dated'}, - date: - language.formatDate(data.date), + language.encapsulate(entryCapsule, 'title', titleCapsule => [ + html.tag('span', {class: 'commentary-entry-heading-text'}, + 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.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }); + } + + if (workingCapsule === accentCapsule) { + return html.blank(); + } else { + return language.$(workingCapsule, workingOptions); + } + }))); + + if (!html.isBlank(accent)) { + workingCapsule += '.withAccent'; + workingOptions.accent = accent; + } + + return language.$(workingCapsule, workingOptions); })), - 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); - }), + relations.date, ])), html.tag('blockquote', {class: 'commentary-entry-body'}, diff --git a/src/content/dependencies/generateCommentaryEntryDate.js b/src/content/dependencies/generateCommentaryEntryDate.js new file mode 100644 index 00000000..f1cf5cb3 --- /dev/null +++ b/src/content/dependencies/generateCommentaryEntryDate.js @@ -0,0 +1,93 @@ +export default { + contentDependencies: ['generateTextWithTooltip', 'generateTooltip'], + extraDependencies: ['html', 'language'], + + relations: (relation, _entry) => ({ + textWithTooltip: + relation('generateTextWithTooltip'), + + tooltip: + relation('generateTooltip'), + }), + + data: (entry) => ({ + date: entry.date, + secondDate: entry.secondDate, + dateKind: entry.dateKind, + + accessDate: entry.accessDate, + accessKind: entry.accessKind, + }), + + generate(data, relations, {html, language}) { + const titleCapsule = language.encapsulate('misc.artistCommentary.entry.title'); + + const willDisplayTooltip = + !!(data.accessKind && data.accessDate); + + const topAttributes = + {class: 'commentary-date'}; + + const time = + html.tag('time', + {[html.onlyIfContent]: true}, + + (willDisplayTooltip + ? {class: 'text-with-tooltip-interaction-cue'} + : topAttributes), + + language.encapsulate(titleCapsule, 'date', workingCapsule => { + const workingOptions = {}; + + if (!data.date) { + return html.blank(); + } + + const rangeNeeded = + data.dateKind === 'sometime' || + data.dateKind === 'throughout'; + + if (rangeNeeded && !data.secondDate) { + workingOptions.date = language.formatDate(data.date); + return language.$(workingCapsule, workingOptions); + } + + if (data.dateKind) { + workingCapsule += '.' + data.dateKind; + } + + if (data.secondDate) { + workingCapsule += '.range'; + workingOptions.dateRange = + language.formatDateRange(data.date, data.secondDate); + } else { + workingOptions.date = + language.formatDate(data.date); + } + + return language.$(workingCapsule, workingOptions); + })); + + if (willDisplayTooltip) { + return relations.textWithTooltip.slots({ + customInteractionCue: true, + + attributes: topAttributes, + text: time, + + tooltip: + relations.tooltip.slots({ + attributes: {class: 'commentary-date-tooltip'}, + + content: + language.$(titleCapsule, 'date', data.accessKind, { + date: + language.formatDate(data.accessDate), + }), + }), + }); + } else { + return time; + } + }, +} diff --git a/src/content/dependencies/generateCommentarySection.js b/src/content/dependencies/generateCommentarySection.js deleted file mode 100644 index c5090660..00000000 --- a/src/content/dependencies/generateCommentarySection.js +++ /dev/null @@ -1,44 +0,0 @@ -import {empty} from '#sugar'; - -export default { - contentDependencies: [ - 'transformContent', - 'generateCommentaryEntry', - 'generateContentHeading', - ], - - extraDependencies: ['html', 'language'], - - relations: (relation, entries) => ({ - heading: - relation('generateContentHeading'), - - entries: - (entries - ? entries.map(entry => - relation('generateCommentaryEntry', entry)) - : []), - }), - - data: (entries) => ({ - firstEntryIsDated: - (empty(entries) - ? null - : !!entries[0].date), - }), - - generate: (data, relations, {html, language}) => - html.tags([ - relations.heading - .slots({ - title: language.$('misc.artistCommentary'), - attributes: [ - {id: 'artist-commentary'}, - data.firstEntryIsDated && - {class: 'first-entry-is-dated'}, - ], - }), - - relations.entries, - ]), -}; diff --git a/src/content/dependencies/generateContentContentHeading.js b/src/content/dependencies/generateContentContentHeading.js new file mode 100644 index 00000000..314ef197 --- /dev/null +++ b/src/content/dependencies/generateContentContentHeading.js @@ -0,0 +1,39 @@ +export default { + contentDependencies: ['generateContentHeading'], + extraDependencies: ['html', 'language'], + + relations: (relation, _thing) => ({ + contentHeading: + relation('generateContentHeading'), + }), + + data: (thing) => ({ + name: + thing.name, + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + string: { + type: 'string', + }, + }, + + generate: (data, relations, slots, {html, language}) => + relations.contentHeading.slots({ + attributes: slots.attributes, + + title: + language.$(slots.string, { + thing: + html.tag('i', data.name), + }), + + stickyTitle: + language.$(slots.string, 'sticky'), + }), +}; diff --git a/src/content/dependencies/generateContributionList.js b/src/content/dependencies/generateContributionList.js index 8e8c5020..d1c3de0f 100644 --- a/src/content/dependencies/generateContributionList.js +++ b/src/content/dependencies/generateContributionList.js @@ -20,8 +20,8 @@ export default { .map(contributionLink => html.tag('li', contributionLink.slots({ + showAnnotation: true, showExternalLinks: true, - showContribution: true, showChronology: true, preventWrapping: false, chronologyKind: slots.chronologyKind, diff --git a/src/content/dependencies/generateContributionTooltipChronologySection.js b/src/content/dependencies/generateContributionTooltipChronologySection.js index 78c9051c..378c0e1c 100644 --- a/src/content/dependencies/generateContributionTooltipChronologySection.js +++ b/src/content/dependencies/generateContributionTooltipChronologySection.js @@ -1,3 +1,19 @@ +import Thing from '#thing'; + +function getName(thing) { + if (!thing) { + return null; + } + + const referenceType = thing.constructor[Thing.referenceType]; + + if (referenceType === 'artwork') { + return thing.thing.name; + } + + return thing.name; +} + export default { contentDependencies: ['linkAnythingMan'], extraDependencies: ['html', 'language'], @@ -30,14 +46,10 @@ export default { data: (query, _contribution) => ({ previousName: - (query.previous - ? query.previous.thing.name - : null), + getName(query.previous?.thing), nextName: - (query.next - ? query.next.thing.name - : null), + getName(query.next?.thing), }), slots: { diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index 3d5a614f..78a6103b 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -1,58 +1,57 @@ -import {stitchArrays} from '#sugar'; - export default { - contentDependencies: ['image', 'linkArtTag'], + contentDependencies: [ + 'generateColorStyleAttribute', + 'generateCoverArtworkArtTagDetails', + 'generateCoverArtworkArtistDetails', + 'generateCoverArtworkOriginDetails', + 'generateCoverArtworkReferenceDetails', + 'image', + ], + extraDependencies: ['html'], - query: (artTags) => ({ - linkableArtTags: - (artTags - ? artTags.filter(tag => !tag.isContentWarning) - : []), - }), + relations: (relation, artwork) => ({ + colorStyleAttribute: + relation('generateColorStyleAttribute'), - relations: (relation, query, artTags) => ({ image: - relation('image', artTags), + relation('image', artwork), - tagLinks: - query.linkableArtTags - .filter(tag => !tag.isContentWarning) - .map(tag => relation('linkArtTag', tag)), - }), + originDetails: + relation('generateCoverArtworkOriginDetails', artwork), - data: (query) => { - const data = {}; + artTagDetails: + relation('generateCoverArtworkArtTagDetails', artwork), - const seenShortNames = new Set(); - const duplicateShortNames = new Set(); + artistDetails: + relation('generateCoverArtworkArtistDetails', artwork), - for (const {nameShort: shortName} of query.linkableArtTags) { - if (seenShortNames.has(shortName)) { - duplicateShortNames.add(shortName); - } else { - seenShortNames.add(shortName); - } - } + referenceDetails: + relation('generateCoverArtworkReferenceDetails', artwork), + }), - data.preferShortName = - query.linkableArtTags - .map(artTag => !duplicateShortNames.has(artTag.nameShort)); + data: (artwork) => ({ + attachAbove: + artwork.attachAbove, - return data; - }, + attachedArtworkIsMainArtwork: + (artwork.attachAbove + ? artwork.attachedArtwork.isMainArtwork + : null), - slots: { - path: { - validate: v => v.validateArrayItems(v.isString), - }, + color: + artwork.thing.color ?? null, - alt: { - type: 'string', - }, + dimensions: + artwork.dimensions, + }), + + slots: { + alt: {type: 'string'}, color: { - validate: v => v.isColor, + validate: v => v.anyOf(v.isBoolean, v.isColor), + default: false, }, mode: { @@ -60,74 +59,99 @@ export default { default: 'primary', }, - dimensions: { - validate: v => v.isDimensions, + showOriginDetails: {type: 'boolean', default: false}, + showArtTagDetails: {type: 'boolean', default: false}, + showArtistDetails: {type: 'boolean', default: false}, + showReferenceDetails: {type: 'boolean', default: false}, + + details: { + type: 'html', + mutable: false, }, }, generate(data, relations, slots, {html}) { + const {image} = relations; + + image.setSlot('alt', slots.alt); + const square = - (slots.dimensions - ? slots.dimensions[0] === slots.dimensions[1] + (data.dimensions + ? data.dimensions[0] === data.dimensions[1] : true); - const sizeSlots = - (square - ? {square: true} - : {dimensions: slots.dimensions}); - - switch (slots.mode) { - case 'primary': - return html.tags([ - relations.image.slots({ - path: slots.path, - alt: slots.alt, - color: slots.color, - thumb: 'medium', - reveal: true, - link: true, - ...sizeSlots, - }), - - 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': - return relations.image.slots({ - path: slots.path, - alt: slots.alt, - color: slots.color, - thumb: 'small', - reveal: false, - link: false, - ...sizeSlots, - }); - - case 'commentary': - return relations.image.slots({ - path: slots.path, - alt: slots.alt, - color: slots.color, - thumb: 'medium', - reveal: true, - link: true, - lazy: true, - ...sizeSlots, - - attributes: - {class: 'commentary-art'}, - }); - - default: - return html.blank(); + if (square) { + image.setSlot('square', true); + } else { + image.setSlot('dimensions', data.dimensions); + } + + const attributes = html.attributes(); + + let color = null; + if (typeof slots.color === 'boolean') { + if (slots.color) { + color = data.color; + } + } else if (slots.color) { + color = slots.color; + } + + if (color) { + relations.colorStyleAttribute.setSlot('color', color); + attributes.add(relations.colorStyleAttribute); } + + return html.tags([ + data.attachAbove && + html.tag('div', {class: 'cover-artwork-joiner'}), + + html.tag('div', {class: 'cover-artwork'}, + slots.mode === 'commentary' && + {class: 'commentary-art'}, + + data.attachAbove && + data.attachedArtworkIsMainArtwork && + {class: 'attached-artwork-is-main-artwork'}, + + attributes, + + (slots.mode === 'primary' + ? [ + relations.image.slots({ + thumb: 'medium', + reveal: true, + link: true, + }), + + slots.showOriginDetails && + relations.originDetails, + + slots.showArtTagDetails && + relations.artTagDetails, + + slots.showArtistDetails && + relations.artistDetails, + + slots.showReferenceDetails && + relations.referenceDetails, + + slots.details, + ] + : slots.mode === 'thumbnail' + ? relations.image.slots({ + thumb: 'small', + reveal: false, + link: false, + }) + : slots.mode === 'commentary' + ? relations.image.slots({ + thumb: 'medium', + reveal: true, + link: true, + lazy: true, + }) + : html.blank())), + ]); }, }; diff --git a/src/content/dependencies/generateCoverArtworkArtTagDetails.js b/src/content/dependencies/generateCoverArtworkArtTagDetails.js new file mode 100644 index 00000000..4d908665 --- /dev/null +++ b/src/content/dependencies/generateCoverArtworkArtTagDetails.js @@ -0,0 +1,75 @@ +import {compareArrays, empty, stitchArrays} from '#sugar'; + +function linkable(tag) { + return !tag.isContentWarning; +} + +export default { + contentDependencies: ['linkArtTagGallery'], + extraDependencies: ['html', 'language'], + + query: (artwork) => ({ + linkableArtTags: + artwork.artTags.filter(linkable), + + mainArtworkLinkableArtTags: + (artwork.mainArtwork + ? artwork.mainArtwork.artTags.filter(linkable) + : null), + }), + + relations: (relation, query, _artwork) => ({ + artTagLinks: + query.linkableArtTags + .map(tag => relation('linkArtTagGallery', tag)), + }), + + data: (query, artwork) => { + const data = {}; + + data.attachAbove = artwork.attachAbove; + + data.sameAsMainArtwork = + !artwork.isMainArtwork && + query.mainArtworkLinkableArtTags && + !empty(query.mainArtworkLinkableArtTags) && + compareArrays( + query.mainArtworkLinkableArtTags, + query.linkableArtTags); + + const seenShortNames = new Set(); + const duplicateShortNames = new Set(); + + for (const {nameShort: shortName} of query.linkableArtTags) { + if (seenShortNames.has(shortName)) { + duplicateShortNames.add(shortName); + } else { + seenShortNames.add(shortName); + } + } + + data.preferShortName = + query.linkableArtTags + .map(artTag => !duplicateShortNames.has(artTag.nameShort)); + + return data; + }, + + generate: (data, relations, {html, language}) => + language.encapsulate('misc.coverArtwork', capsule => + html.tag('ul', {class: 'image-details'}, + {[html.onlyIfContent]: true}, + + {class: 'art-tag-details'}, + + (data.sameAsMainArtwork && data.attachAbove + ? html.blank() + : data.sameAsMainArtwork && relations.artTagLinks.length >= 3 + ? language.$(capsule, 'sameTagsAsMainArtwork') + : stitchArrays({ + artTagLink: relations.artTagLinks, + preferShortName: data.preferShortName, + }).map(({artTagLink, preferShortName}) => + html.tag('li', + artTagLink.slot('preferShortName', preferShortName)))))), +}; diff --git a/src/content/dependencies/generateCoverArtworkArtistDetails.js b/src/content/dependencies/generateCoverArtworkArtistDetails.js new file mode 100644 index 00000000..3ead80ab --- /dev/null +++ b/src/content/dependencies/generateCoverArtworkArtistDetails.js @@ -0,0 +1,25 @@ +export default { + contentDependencies: ['linkArtistGallery'], + extraDependencies: ['html', 'language'], + + relations: (relation, artwork) => ({ + artistLinks: + artwork.artistContribs + .map(contrib => contrib.artist) + .map(artist => + relation('linkArtistGallery', artist)), + }), + + generate: (relations, {html, language}) => + html.tag('p', {class: 'image-details'}, + {[html.onlyIfContent]: true}, + + {class: 'illustrator-details'}, + + language.$('misc.coverGrid.details.coverArtists', { + [language.onlyIfOptions]: ['artists'], + + artists: + language.formatConjunctionList(relations.artistLinks), + })), +}; diff --git a/src/content/dependencies/generateCoverArtworkOriginDetails.js b/src/content/dependencies/generateCoverArtworkOriginDetails.js new file mode 100644 index 00000000..8628179e --- /dev/null +++ b/src/content/dependencies/generateCoverArtworkOriginDetails.js @@ -0,0 +1,170 @@ +import Thing from '#thing'; + +export default { + contentDependencies: [ + 'generateArtistCredit', + 'generateAbsoluteDatetimestamp', + 'linkAlbum', + 'transformContent', + ], + + extraDependencies: ['html', 'language', 'pagePath'], + + query: (artwork) => ({ + artworkThingType: + artwork.thing.constructor[Thing.referenceType], + + attachedArtistContribs: + (artwork.attachedArtwork + ? artwork.attachedArtwork.artistContribs + : null) + }), + + relations: (relation, query, artwork) => ({ + credit: + relation('generateArtistCredit', + artwork.artistContribs, + query.attachedArtistContribs ?? []), + + source: + relation('transformContent', artwork.source), + + originDetails: + relation('transformContent', artwork.originDetails), + + albumLink: + (query.artworkThingType === 'album' + ? relation('linkAlbum', artwork.thing) + : null), + + datetimestamp: + (artwork.date && artwork.date !== artwork.thing.date + ? relation('generateAbsoluteDatetimestamp', artwork.date) + : null), + }), + + + data: (query, artwork) => ({ + label: + artwork.label, + + artworkThingType: + query.artworkThingType, + }), + + generate: (data, relations, {html, language, pagePath}) => + language.encapsulate('misc.coverArtwork', capsule => + html.tag('p', {class: 'image-details'}, + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + {class: 'origin-details'}, + + (() => { + relations.datetimestamp?.setSlots({ + style: 'year', + tooltip: true, + }); + + const artworkBy = + language.encapsulate(capsule, 'artworkBy', workingCapsule => { + const workingOptions = {}; + + if (data.label) { + workingCapsule += '.customLabel'; + workingOptions.label = data.label; + } + + if (relations.datetimestamp) { + workingCapsule += '.withYear'; + workingOptions.year = relations.datetimestamp; + } + + return relations.credit.slots({ + showAnnotation: true, + showExternalLinks: true, + showChronology: true, + showWikiEdits: true, + + trimAnnotation: false, + + chronologyKind: 'coverArt', + + normalStringKey: workingCapsule, + additionalStringOptions: workingOptions, + }); + }); + + const trackArtFromAlbum = + pagePath[0] === 'track' && + data.artworkThingType === 'album' && + language.$(capsule, 'trackArtFromAlbum', { + album: + relations.albumLink.slot('color', false), + }); + + const source = + language.encapsulate(capsule, 'source', workingCapsule => { + const workingOptions = { + [language.onlyIfOptions]: ['source'], + source: relations.source.slot('mode', 'inline'), + }; + + if (html.isBlank(artworkBy) && data.label) { + workingCapsule += '.customLabel'; + workingOptions.label = data.label; + } + + if (html.isBlank(artworkBy) && relations.datetimestamp) { + workingCapsule += '.withYear'; + workingOptions.year = relations.datetimestamp; + } + + return language.$(workingCapsule, workingOptions); + }); + + const label = + html.isBlank(artworkBy) && + html.isBlank(source) && + language.encapsulate(capsule, 'customLabel', workingCapsule => { + const workingOptions = { + [language.onlyIfOptions]: ['label'], + label: data.label, + }; + + if (relations.datetimestamp) { + workingCapsule += '.withYear'; + workingOptions.year = relations.datetimestamp; + } + + return language.$(workingCapsule, workingOptions); + }); + + const year = + html.isBlank(artworkBy) && + html.isBlank(source) && + html.isBlank(label) && + language.$(capsule, 'year', { + [language.onlyIfOptions]: ['year'], + year: relations.datetimestamp, + }); + + const originDetails = + html.tag('span', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + })); + + return [ + artworkBy, + trackArtFromAlbum, + source, + label, + year, + originDetails, + ]; + })())), +}; diff --git a/src/content/dependencies/generateCoverArtworkReferenceDetails.js b/src/content/dependencies/generateCoverArtworkReferenceDetails.js new file mode 100644 index 00000000..035ab586 --- /dev/null +++ b/src/content/dependencies/generateCoverArtworkReferenceDetails.js @@ -0,0 +1,60 @@ +export default { + contentDependencies: ['linkReferencedArtworks', 'linkReferencingArtworks'], + extraDependencies: ['html', 'language'], + + relations: (relation, artwork) => ({ + referencedArtworksLink: + relation('linkReferencedArtworks', artwork), + + referencingArtworksLink: + relation('linkReferencingArtworks', artwork), + }), + + data: (artwork) => ({ + referenced: + artwork.referencedArtworks.length, + + referencedBy: + artwork.referencedByArtworks.length, + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('releaseInfo', capsule => { + const referencedText = + language.$(capsule, 'referencesArtworks', { + [language.onlyIfOptions]: ['artworks'], + + artworks: + language.countArtworks(data.referenced, { + blankIfZero: true, + unit: true, + }), + }); + + const referencingText = + language.$(capsule, 'referencedByArtworks', { + [language.onlyIfOptions]: ['artworks'], + + artworks: + language.countArtworks(data.referencedBy, { + blankIfZero: true, + unit: true, + }), + }); + + return ( + html.tag('p', {class: 'image-details'}, + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + {class: 'reference-details'}, + + [ + !html.isBlank(referencedText) && + relations.referencedArtworksLink.slot('content', referencedText), + + !html.isBlank(referencingText) && + relations.referencingArtworksLink.slot('content', referencingText), + ])); + }), +} diff --git a/src/content/dependencies/generateCoverCarousel.js b/src/content/dependencies/generateCoverCarousel.js index 69220da6..0705d93e 100644 --- a/src/content/dependencies/generateCoverCarousel.js +++ b/src/content/dependencies/generateCoverCarousel.js @@ -2,24 +2,16 @@ import {empty, repeat, stitchArrays} from '#sugar'; import {getCarouselLayoutForNumberOfItems} from '#wiki-data'; export default { - contentDependencies: ['generateGridActionLinks'], extraDependencies: ['html'], - relations(relation) { - return { - actionLinks: relation('generateGridActionLinks'), - }; - }, - slots: { images: {validate: v => v.strictArrayOf(v.isHTML)}, links: {validate: v => v.strictArrayOf(v.isHTML)}, lazy: {validate: v => v.anyOf(v.isWholeNumber, v.isBoolean)}, - actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, - generate(relations, slots, {html}) { + generate(slots, {html}) { const stitched = stitchArrays({ image: slots.images, @@ -27,7 +19,7 @@ export default { }); if (empty(stitched)) { - return; + return html.blank(); } const layout = getCarouselLayoutForNumberOfItems(stitched.length); @@ -58,9 +50,6 @@ export default { }), })))), ])), - - relations.actionLinks - .slot('actionLinks', slots.actionLinks), ]); }, }; diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 0433aaf1..e4dfd905 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -15,23 +15,59 @@ export default { links: {validate: v => v.strictArrayOf(v.isHTML)}, names: {validate: v => v.strictArrayOf(v.isHTML)}, info: {validate: v => v.strictArrayOf(v.isHTML)}, + notFromThisGroup: {validate: v => v.strictArrayOf(v.isBoolean)}, + + // Differentiating from sparseArrayOf here - this list of classes should + // have the same length as the items above, i.e. nulls aren't going to be + // filtered out of it, but it is okay to *include* null (standing in for + // no classes for this grid item). + classes: { + validate: v => + v.strictArrayOf( + v.optional( + v.anyOf( + v.isArray, + v.isString))), + }, lazy: {validate: v => v.anyOf(v.isWholeNumber, v.isBoolean)}, actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, - generate(relations, slots, {html, language}) { - return ( - html.tag('div', {class: 'grid-listing'}, [ + generate: (relations, slots, {html, language}) => + html.tag('div', {class: 'grid-listing'}, + {[html.onlyIfContent]: true}, + + [ stitchArrays({ + classes: slots.classes, image: slots.images, link: slots.links, name: slots.names, info: slots.info, - }).map(({image, link, name, info}, index) => + + notFromThisGroup: + slots.notFromThisGroup ?? + Array.from(slots.links).fill(null) + }).map(({ + classes, + image, + link, + name, + info, + notFromThisGroup, + }, index) => link.slots({ - attributes: {class: ['grid-item', 'box']}, + attributes: [ + {class: ['grid-item', 'box']}, + + (classes + ? {class: classes} + : null), + ], + colorContext: 'image-box', + content: [ image.slots({ thumb: 'medium', @@ -44,16 +80,31 @@ export default { : false), }), - html.tag('span', {[html.onlyIfContent]: true}, - language.sanitize(name)), + html.tag('span', + {[html.onlyIfContent]: true}, + + (notFromThisGroup + ? language.encapsulate('misc.coverGrid.details.notFromThisGroup', capsule => + language.$(capsule, { + name, + marker: + html.tag('span', {class: 'grid-name-marker'}, + language.$(capsule, 'marker')), + })) + : language.sanitize(name))), + + html.tag('span', + {[html.onlyIfContent]: true}, - html.tag('span', {[html.onlyIfContent]: true}, - language.sanitize(info)), + language.$('misc.coverGrid.details.accent', { + [language.onlyIfOptions]: ['details'], + + details: info, + })), ], })), relations.actionLinks .slot('actionLinks', slots.actionLinks), - ])); - }, + ]), }; diff --git a/src/content/dependencies/generateDatetimestampTemplate.js b/src/content/dependencies/generateDatetimestampTemplate.js index d9ed036a..a92d15fc 100644 --- a/src/content/dependencies/generateDatetimestampTemplate.js +++ b/src/content/dependencies/generateDatetimestampTemplate.js @@ -31,8 +31,10 @@ export default { slots.mainContent), tooltip: - slots.tooltip?.slots({ - attributes: [{class: 'datetimestamp-tooltip'}], - }), + (html.isBlank(slots.tooltip) + ? null + : slots.tooltip.slots({ + attributes: [{class: 'datetimestamp-tooltip'}], + })), }), }; diff --git a/src/content/dependencies/generateDotSwitcherTemplate.js b/src/content/dependencies/generateDotSwitcherTemplate.js new file mode 100644 index 00000000..22205922 --- /dev/null +++ b/src/content/dependencies/generateDotSwitcherTemplate.js @@ -0,0 +1,41 @@ +export default { + extraDependencies: ['html'], + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + options: { + validate: v => v.strictArrayOf(v.isHTML), + }, + + initialOptionIndex: {type: 'number'}, + }, + + generate: (slots, {html}) => + html.tag('span', {class: 'dot-switcher'}, + {[html.onlyIfContent]: true}, + {[html.noEdgeWhitespace]: true}, + {[html.joinChildren]: ''}, + + slots.attributes, + + slots.options + .map((option, index) => + html.tag('span', + {[html.onlyIfContent]: true}, + + html.resolve(option, {normalize: 'tag'}) + .onlyIfSiblings && + {[html.onlyIfSiblings]: true}, + + index === slots.initialOptionIndex && + {class: 'current'}, + + [ + html.metatag('imaginary-sibling'), + option, + ]))), +}; diff --git a/src/content/dependencies/generateExpandableGallerySection.js b/src/content/dependencies/generateExpandableGallerySection.js new file mode 100644 index 00000000..122ca4b1 --- /dev/null +++ b/src/content/dependencies/generateExpandableGallerySection.js @@ -0,0 +1,92 @@ +export default { + contentDependencies: ['generateContentHeading'], + extraDependencies: ['html', 'language'], + + relations: (relation) => ({ + contentHeading: + relation('generateContentHeading'), + }), + + slots: { + title: { + type: 'html', + mutable: false, + }, + + contentAboveCut: { + type: 'html', + mutable: false, + }, + + contentBelowCut: { + type: 'html', + mutable: false, + }, + + caption: { + type: 'html', + mutable: false, + }, + + expandCue: { + type: 'html', + mutable: false, + }, + + collapseCue: { + type: 'html', + mutable: false, + }, + }, + + generate: (relations, slots, {html, language}) => + html.tag('section', {class: 'expandable-gallery-section'}, [ + relations.contentHeading.slots({ + tag: 'h2', + title: slots.title, + }), + + html.tag('div', {class: 'section-content-above-cut'}, + {[html.onlyIfContent]: true}, + + slots.contentAboveCut), + + html.tag('div', {class: 'section-content-below-cut'}, + {[html.onlyIfContent]: true}, + + !html.isBlank(slots.contentBelowCut) && + {style: 'display: none'}, + + slots.contentBelowCut), + + html.tag('div', {class: 'section-expando'}, + {[html.onlyIfSiblings]: true}, + + html.tag('div', {class: 'section-expando-content'}, + {[html.joinChildren]: html.tag('br')}, + + [ + html.tag('span', {class: 'section-caption'}, + slots.caption), + + !html.isBlank(slots.contentBelowCut) && + language.$('misc.coverGrid.expandCollapseCue', { + cue: + html.tag('a', {class: 'section-expando-toggle'}, + {href: '#'}, + + {[html.joinChildren]: ''}, + {[html.noEdgeWhitespace]: true}, + + [ + html.tag('span', {class: 'section-expand-cue'}, + slots.expandCue), + + html.tag('span', {class: 'section-collapse-cue'}, + {style: 'display: none'}, + slots.collapseCue), + ]), + }), + ])), + ]), +}; diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js index 1fa6de51..84ab549d 100644 --- a/src/content/dependencies/generateFlashActGalleryPage.js +++ b/src/content/dependencies/generateFlashActGalleryPage.js @@ -1,4 +1,4 @@ -import {stitchArrays} from '#sugar'; +import striptags from 'striptags'; export default { contentDependencies: [ @@ -8,6 +8,7 @@ export default { 'generatePageLayout', 'image', 'linkFlash', + 'linkFlashAct', 'linkFlashIndex', ], @@ -20,6 +21,9 @@ export default { flashIndexLink: relation('linkFlashIndex'), + flashActNavLink: + relation('linkFlashAct', act), + flashActNavAccent: relation('generateFlashActNavAccent', act), @@ -31,7 +35,7 @@ export default { coverGridImages: act.flashes - .map(_flash => relation('image')), + .map(flash => relation('image', flash.coverArtwork)), flashLinks: act.flashes @@ -44,10 +48,6 @@ export default { flashNames: act.flashes.map(flash => flash.name), - - flashCoverPaths: - act.flashes.map(flash => - ['media.flashArt', flash.directory, flash.coverArtFileExtension]) }), generate: (data, relations, {language}) => @@ -55,7 +55,7 @@ export default { relations.layout.slots({ title: language.$(pageCapsule, 'title', { - flash: data.name, + flash: striptags(data.name), }), color: data.color, @@ -65,15 +65,9 @@ export default { mainContent: [ relations.coverGrid.slots({ links: relations.flashLinks, + images: relations.coverGridImages, names: data.flashNames, lazy: 6, - - images: - stitchArrays({ - image: relations.coverGridImages, - path: data.flashCoverPaths, - }).map(({image, path}) => - image.slot('path', path)), }), ], @@ -81,7 +75,7 @@ export default { navLinks: [ {auto: 'home'}, {html: relations.flashIndexLink}, - {auto: 'current'}, + {html: relations.flashActNavLink}, ], navBottomRowContent: relations.flashActNavAccent, diff --git a/src/content/dependencies/generateFlashActNavAccent.js b/src/content/dependencies/generateFlashActNavAccent.js index 424948f9..c4ec77b8 100644 --- a/src/content/dependencies/generateFlashActNavAccent.js +++ b/src/content/dependencies/generateFlashActNavAccent.js @@ -1,16 +1,17 @@ -import {atOffset, empty} from '#sugar'; +import {atOffset} from '#sugar'; export default { contentDependencies: [ - 'generatePreviousNextLinks', + 'generateInterpageDotSwitcher', + 'generateNextLink', + 'generatePreviousLink', 'linkFlashAct', ], - extraDependencies: ['html', 'language', 'wikiData'], + extraDependencies: ['wikiData'], - sprawl({flashActData}) { - return {flashActData}; - }, + sprawl: ({flashActData}) => + ({flashActData}), query(sprawl, flashAct) { // Like with generateFlashNavAccent, don't sort chronologically here. @@ -29,43 +30,35 @@ export default { return {previousFlashAct, nextFlashAct}; }, - relations(relation, query) { - const relations = {}; - - if (query.previousFlashAct || query.nextFlashAct) { - relations.previousNextLinks = - relation('generatePreviousNextLinks'); - - relations.previousFlashActLink = - (query.previousFlashAct - ? relation('linkFlashAct', query.previousFlashAct) - : null); - - relations.nextFlashActLink = - (query.nextFlashAct - ? relation('linkFlashAct', query.nextFlashAct) - : null); - } - - return relations; - }, - - generate(relations, {html, language}) { - const {content: previousNextLinks = []} = - relations.previousNextLinks && - relations.previousNextLinks.slots({ - previousLink: relations.previousFlashActLink, - nextLink: relations.nextFlashActLink, - }); - - const allLinks = [ - ...previousNextLinks, - ].filter(Boolean); - - if (empty(allLinks)) { - return html.blank(); - } - - return `(${language.formatUnitList(allLinks)})`; - }, + relations: (relation, query) => ({ + switcher: + relation('generateInterpageDotSwitcher'), + + previousLink: + relation('generatePreviousLink'), + + nextLink: + relation('generateNextLink'), + + previousFlashActLink: + (query.previousFlashAct + ? relation('linkFlashAct', query.previousFlashAct) + : null), + + nextFlashActLink: + (query.nextFlashAct + ? relation('linkFlashAct', query.nextFlashAct) + : null), + }), + + generate: (relations) => + relations.switcher.slots({ + links: [ + relations.previousLink + .slot('link', relations.previousFlashActLink), + + relations.nextLink + .slot('link', relations.nextFlashActLink), + ], + }), }; diff --git a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js index c5426a41..6d152c7c 100644 --- a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js +++ b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js @@ -44,10 +44,11 @@ export default { [ html.tag('summary', - html.tag('span', {class: 'group-name'}, - (data.customListTerminology - ? language.sanitize(data.customListTerminology) - : language.$('flashSidebar.flashList.entriesInThisSection')))), + html.tag('span', + html.tag('b', + (data.customListTerminology + ? language.sanitize(data.customListTerminology) + : language.$('flashSidebar.flashList.entriesInThisSection'))))), html.tag('ul', relations.flashLinks diff --git a/src/content/dependencies/generateFlashActSidebarSideMapBox.js b/src/content/dependencies/generateFlashActSidebarSideMapBox.js index 3d261ec3..7b26ef31 100644 --- a/src/content/dependencies/generateFlashActSidebarSideMapBox.js +++ b/src/content/dependencies/generateFlashActSidebarSideMapBox.js @@ -68,8 +68,8 @@ export default { [ html.tag('summary', - html.tag('span', {class: 'group-name'}, - sideName)), + html.tag('span', + html.tag('b', sideName))), html.tag('ul', actLinks.map((actLink, actIndex) => diff --git a/src/content/dependencies/generateFlashArtworkColumn.js b/src/content/dependencies/generateFlashArtworkColumn.js new file mode 100644 index 00000000..5987df9e --- /dev/null +++ b/src/content/dependencies/generateFlashArtworkColumn.js @@ -0,0 +1,11 @@ +export default { + contentDependencies: ['generateCoverArtwork'], + + relations: (relation, flash) => ({ + coverArtwork: + relation('generateCoverArtwork', flash.coverArtwork), + }), + + generate: (relations) => + relations.coverArtwork, +}; diff --git a/src/content/dependencies/generateFlashCoverArtwork.js b/src/content/dependencies/generateFlashCoverArtwork.js deleted file mode 100644 index af03ae6b..00000000 --- a/src/content/dependencies/generateFlashCoverArtwork.js +++ /dev/null @@ -1,26 +0,0 @@ -export default { - contentDependencies: ['generateCoverArtwork'], - - relations: (relation) => ({ - coverArtwork: - relation('generateCoverArtwork'), - }), - - data: (flash) => ({ - path: - ['media.flashArt', flash.directory, flash.coverArtFileExtension], - - color: - flash.color, - - dimensions: - flash.coverArtDimensions, - }), - - generate: (data, relations) => - 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 a21bb49e..2788406c 100644 --- a/src/content/dependencies/generateFlashIndexPage.js +++ b/src/content/dependencies/generateFlashIndexPage.js @@ -53,7 +53,7 @@ export default { actCoverGridImages: query.flashActs .map(act => act.flashes - .map(() => relation('image'))), + .map(flash => relation('image', flash.coverArtwork))), }), data: (query) => ({ @@ -73,11 +73,6 @@ export default { query.flashActs .map(act => act.flashes .map(flash => flash.name)), - - actCoverGridPaths: - query.flashActs - .map(act => act.flashes - .map(flash => ['media.flashArt', flash.directory, flash.coverArtFileExtension])), }), generate: (data, relations, {html, language}) => @@ -116,7 +111,6 @@ export default { coverGridImages: relations.actCoverGridImages, coverGridLinks: relations.actCoverGridLinks, coverGridNames: data.actCoverGridNames, - coverGridPaths: data.actCoverGridPaths, }).map(({ colorStyle, actLink, @@ -126,7 +120,6 @@ export default { coverGridImages, coverGridLinks, coverGridNames, - coverGridPaths, }, index) => [ html.tag('h2', {id: anchor}, @@ -135,15 +128,9 @@ export default { coverGrid.slots({ links: coverGridLinks, + images: coverGridImages, names: coverGridNames, lazy: index === 0 ? 4 : true, - - images: - stitchArrays({ - image: coverGridImages, - path: coverGridPaths, - }).map(({image, path}) => - image.slot('path', path)), }), ]), ], diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js index d06f0c01..ee043bfa 100644 --- a/src/content/dependencies/generateFlashInfoPage.js +++ b/src/content/dependencies/generateFlashInfoPage.js @@ -2,11 +2,13 @@ import {empty} from '#sugar'; export default { contentDependencies: [ - 'generateCommentarySection', + 'generateAdditionalNamesBox', + 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generateContributionList', 'generateFlashActSidebar', - 'generateFlashCoverArtwork', + 'generateFlashArtworkColumn', 'generateFlashNavAccent', 'generatePageLayout', 'generateTrackList', @@ -39,16 +41,22 @@ export default { sidebar: relation('generateFlashActSidebar', flash.act, flash), + additionalNamesBox: + relation('generateAdditionalNamesBox', flash.additionalNames), + externalLinks: query.urls .map(url => relation('linkExternal', url)), - cover: - relation('generateFlashCoverArtwork', flash), + artworkColumn: + relation('generateFlashArtworkColumn', flash), contentHeading: relation('generateContentHeading'), + contentContentHeading: + relation('generateContentContentHeading', flash), + flashActLink: relation('linkFlashAct', flash.act), @@ -61,8 +69,13 @@ export default { contributorContributionList: relation('generateContributionList', flash.contributorContribs), - artistCommentarySection: - relation('generateCommentarySection', flash.commentary), + artistCommentaryEntries: + flash.commentary + .map(entry => relation('generateCommentaryEntry', entry)), + + creditSourceEntries: + flash.creditingSources + .map(entry => relation('generateCommentaryEntry', entry)), }), data: (_query, flash) => ({ @@ -87,12 +100,9 @@ export default { color: data.color, headingMode: 'sticky', - cover: - (relations.cover - ? relations.cover.slots({ - alt: language.$('misc.alt.flashArt'), - }) - : null), + additionalNames: relations.additionalNamesBox, + + artworkColumnContent: relations.artworkColumn, mainContent: [ html.tag('p', @@ -117,7 +127,7 @@ export default { {[html.joinChildren]: html.tag('br')}, language.encapsulate('releaseInfo', capsule => [ - !html.isBlank(relations.artistCommentarySection) && + !html.isBlank(relations.artistCommentaryEntries) && language.encapsulate(capsule, 'readCommentary', capsule => language.$(capsule, { link: @@ -125,6 +135,15 @@ export default { {href: '#artist-commentary'}, language.$(capsule, 'link')), })), + + !html.isBlank(relations.creditSourceEntries) && + language.encapsulate(capsule, 'readCreditingSources', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#crediting-sources'}, + language.$(capsule, 'link')), + })), ])), html.tags([ @@ -152,7 +171,25 @@ export default { }), ]), - relations.artistCommentarySection, + html.tags([ + relations.contentContentHeading.clone() + .slots({ + attributes: {id: 'artist-commentary'}, + string: 'misc.artistCommentary', + }), + + relations.artistCommentaryEntries, + ]), + + html.tags([ + relations.contentContentHeading.clone() + .slots({ + attributes: {id: 'crediting-sources'}, + string: 'misc.creditingSources', + }), + + relations.creditSourceEntries, + ]), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateFlashNavAccent.js b/src/content/dependencies/generateFlashNavAccent.js index 55e056dc..0f5d2d6b 100644 --- a/src/content/dependencies/generateFlashNavAccent.js +++ b/src/content/dependencies/generateFlashNavAccent.js @@ -1,16 +1,17 @@ -import {atOffset, empty} from '#sugar'; +import {atOffset} from '#sugar'; export default { contentDependencies: [ - 'generatePreviousNextLinks', + 'generateInterpageDotSwitcher', + 'generateNextLink', + 'generatePreviousLink', 'linkFlash', ], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({flashActData}) { - return {flashActData}; - }, + sprawl: ({flashActData}) => + ({flashActData}), query(sprawl, flash) { // Don't sort chronologically here. The previous/next buttons should match @@ -31,43 +32,35 @@ export default { return {previousFlash, nextFlash}; }, - relations(relation, query) { - const relations = {}; - - if (query.previousFlash || query.nextFlash) { - relations.previousNextLinks = - relation('generatePreviousNextLinks'); - - relations.previousFlashLink = - (query.previousFlash - ? relation('linkFlash', query.previousFlash) - : null); - - relations.nextFlashLink = - (query.nextFlash - ? relation('linkFlash', query.nextFlash) - : null); - } - - return relations; - }, - - generate(relations, {html, language}) { - const {content: previousNextLinks = []} = - relations.previousNextLinks && - relations.previousNextLinks.slots({ - previousLink: relations.previousFlashLink, - nextLink: relations.nextFlashLink, - }); - - const allLinks = [ - ...previousNextLinks, - ].filter(Boolean); - - if (empty(allLinks)) { - return html.blank(); - } - - return `(${language.formatUnitList(allLinks)})`; - }, + relations: (relation, query) => ({ + switcher: + relation('generateInterpageDotSwitcher'), + + previousLink: + relation('generatePreviousLink'), + + nextLink: + relation('generateNextLink'), + + previousFlashLink: + (query.previousFlash + ? relation('linkFlash', query.previousFlash) + : null), + + nextFlashLink: + (query.nextFlash + ? relation('linkFlash', query.nextFlash) + : null), + }), + + generate: (relations) => + relations.switcher.slots({ + links: [ + relations.previousLink + .slot('link', relations.previousFlashLink), + + relations.nextLink + .slot('link', relations.nextFlashLink), + ], + }), }; diff --git a/src/content/dependencies/generateGridActionLinks.js b/src/content/dependencies/generateGridActionLinks.js index f5b1aaa6..585a02b9 100644 --- a/src/content/dependencies/generateGridActionLinks.js +++ b/src/content/dependencies/generateGridActionLinks.js @@ -1,5 +1,3 @@ -import {empty} from '#sugar'; - export default { extraDependencies: ['html'], @@ -7,16 +5,12 @@ export default { actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, - generate(slots, {html}) { - if (empty(slots.actionLinks)) { - return html.blank(); - } + generate: (slots, {html}) => + html.tag('div', {class: 'grid-actions'}, + {[html.onlyIfContent]: true}, - return ( - html.tag('div', {class: 'grid-actions'}, - slots.actionLinks - .filter(Boolean) - .map(link => link - .slot('attributes', {class: ['grid-item', 'box']})))); - }, + (slots.actionLinks ?? []) + .filter(link => link && !html.isBlank(link)) + .map(link => link + .slot('attributes', {class: ['grid-item', 'box']}))), }; diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index ceb54322..dfdad0e8 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -1,14 +1,14 @@ import {sortChronologically} from '#sort'; -import {empty, stitchArrays} from '#sugar'; import {filterItemsForCarousel, getTotalDuration} from '#wiki-data'; export default { contentDependencies: [ 'generateCoverCarousel', - 'generateCoverGrid', + 'generateGroupGalleryPageAlbumsByDateView', + 'generateGroupGalleryPageAlbumsBySeriesView', 'generateGroupNavLinks', 'generateGroupSecondaryNav', - 'generateGroupSidebar', + 'generateIntrapageDotSwitcher', 'generatePageLayout', 'generateQuickDescription', 'image', @@ -21,95 +21,73 @@ export default { sprawl: ({wikiInfo}) => ({enableGroupUI: wikiInfo.enableGroupUI}), - relations(relation, sprawl, group) { - const relations = {}; + query(_sprawl, group) { + const query = {}; - const albums = + query.allAlbums = sortChronologically(group.albums.slice(), {latestFirst: true}); - relations.layout = - relation('generatePageLayout'); + query.allTracks = + query.allAlbums.flatMap((album) => album.tracks); - relations.navLinks = - relation('generateGroupNavLinks', group); + query.carouselAlbums = + filterItemsForCarousel(group.featuredAlbums); - if (sprawl.enableGroupUI) { - relations.secondaryNav = - relation('generateGroupSecondaryNav', group); - - relations.sidebar = - relation('generateGroupSidebar', group); - } - - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); - - if (!empty(carouselAlbums)) { - relations.coverCarousel = - relation('generateCoverCarousel'); - - relations.carouselLinks = - carouselAlbums - .map(album => relation('linkAlbum', album)); + return query; + }, - relations.carouselImages = - carouselAlbums - .map(album => relation('image', album.artTags)); - } + relations: (relation, query, sprawl, group) => ({ + layout: + relation('generatePageLayout'), - relations.quickDescription = - relation('generateQuickDescription', group); + navLinks: + relation('generateGroupNavLinks', group), - relations.coverGrid = - relation('generateCoverGrid'); + secondaryNav: + (sprawl.enableGroupUI + ? relation('generateGroupSecondaryNav', group) + : null), - relations.gridLinks = - albums - .map(album => relation('linkAlbum', album)); + coverCarousel: + relation('generateCoverCarousel'), - relations.gridImages = - albums.map(album => - (album.hasCoverArt - ? relation('image', album.artTags) - : relation('image'))); + carouselLinks: + query.carouselAlbums + .map(album => relation('linkAlbum', album)), - return relations; - }, + carouselImages: + query.carouselAlbums + .map(album => relation('image', album.coverArtworks[0])), - data(sprawl, group) { - const data = {}; + quickDescription: + relation('generateQuickDescription', group), - data.name = group.name; - data.color = group.color; + albumViewSwitcher: + relation('generateIntrapageDotSwitcher'), - const albums = sortChronologically(group.albums.slice(), {latestFirst: true}); - const tracks = albums.flatMap((album) => album.tracks); + albumsBySeriesView: + relation('generateGroupGalleryPageAlbumsBySeriesView', group), - data.numAlbums = albums.length; - data.numTracks = tracks.length; - data.totalDuration = getTotalDuration(tracks, {originalReleasesOnly: true}); + albumsByDateView: + relation('generateGroupGalleryPageAlbumsByDateView', group), + }), - data.gridNames = albums.map(album => album.name); - data.gridDurations = albums.map(album => getTotalDuration(album.tracks)); - data.gridNumTracks = albums.map(album => album.tracks.length); + data: (query, _sprawl, group) => ({ + name: + group.name, - data.gridPaths = - albums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); + color: + group.color, - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); + numAlbums: + query.allAlbums.length, - if (!empty(group.featuredAlbums)) { - data.carouselPaths = - carouselAlbums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); - } + numTracks: + query.allTracks.length, - return data; - }, + totalDuration: + getTotalDuration(query.allTracks, {mainReleasesOnly: true}), + }), generate: (data, relations, {html, language}) => language.encapsulate('groupGalleryPage', pageCapsule => @@ -121,16 +99,10 @@ export default { mainClasses: ['top-index'], mainContent: [ - relations.coverCarousel - ?.slots({ - links: relations.carouselLinks, - images: - stitchArrays({ - image: relations.carouselImages, - path: data.carouselPaths, - }).map(({image, path}) => - image.slot('path', path)), - }), + relations.coverCarousel.slots({ + links: relations.carouselLinks, + images: relations.carouselImages, + }), relations.quickDescription, @@ -155,49 +127,78 @@ export default { })), })), - relations.coverGrid - .slots({ - links: relations.gridLinks, - names: data.gridNames, - images: - stitchArrays({ - image: relations.gridImages, - path: data.gridPaths, - name: data.gridNames, - }).map(({image, path, name}) => - image.slots({ - path, - missingSourceContent: - language.$('misc.albumGrid.noCoverArt', { - album: name, - }), - })), - info: - stitchArrays({ - numTracks: data.gridNumTracks, - duration: data.gridDurations, - }).map(({numTracks, duration}) => - language.$('misc.albumGrid.details', { - tracks: language.countTracks(numTracks, {unit: true}), - time: language.formatDuration(duration), - })), - }), + ([ + !html.isBlank(relations.albumsBySeriesView), + !html.isBlank(relations.albumsByDateView) + ]).filter(Boolean).length > 1 && + + language.encapsulate(pageCapsule, 'albumViewSwitcher', capsule => + html.tag('p', {class: 'gallery-view-switcher'}, + {class: ['drop', 'shiny']}, + + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + language.$(capsule), + + relations.albumViewSwitcher.slots({ + initialOptionIndex: 0, + + titles: [ + !html.isBlank(relations.albumsByDateView) && + language.$(capsule, 'byDate'), + + !html.isBlank(relations.albumsBySeriesView) && + language.$(capsule, 'bySeries'), + ].filter(Boolean), + + targetIDs: [ + !html.isBlank(relations.albumsByDateView) && + 'group-album-gallery-by-date', + + !html.isBlank(relations.albumsBySeriesView) && + 'group-album-gallery-by-series', + ].filter(Boolean), + }), + ])), + + /* + data.trackGridLabels.some(value => value !== null) && + html.tag('p', {class: 'gallery-set-switcher'}, + language.encapsulate(pageCapsule, 'setSwitcher', switcherCapsule => + language.$(switcherCapsule, { + sets: + relations.setSwitcher.slots({ + initialOptionIndex: 0, + + titles: + data.trackGridLabels.map(label => + label ?? + language.$(switcherCapsule, 'unlabeledSet')), + + targetIDs: + data.trackGridIDs, + }), + }))), + */ + + relations.albumsByDateView, + + relations.albumsBySeriesView.slots({ + attributes: [ + !html.isBlank(relations.albumsBySeriesView) && + {style: 'display: none'}, + ], + }), ], - leftSidebar: - (relations.sidebar - ? relations.sidebar - .slot('currentExtra', 'gallery') - .content /* TODO: Kludge. */ - : null), - navLinkStyle: 'hierarchical', navLinks: relations.navLinks .slot('currentExtra', 'gallery') .content, - secondaryNav: - relations.secondaryNav ?? null, + secondaryNav: relations.secondaryNav, })), }; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js new file mode 100644 index 00000000..7d9aa2d2 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js @@ -0,0 +1,66 @@ +import {stitchArrays} from '#sugar'; +import {getTotalDuration} from '#wiki-data'; + +export default { + contentDependencies: ['generateCoverGrid', 'image', 'linkAlbum'], + extraDependencies: ['language'], + + relations: (relation, albums, _group) => ({ + coverGrid: + relation('generateCoverGrid'), + + links: + albums.map(album => + relation('linkAlbum', album)), + + images: + albums.map(album => + (album.hasCoverArt + ? relation('image', album.coverArtworks[0]) + : relation('image'))) + }), + + data: (albums, group) => ({ + names: + albums.map(album => album.name), + + durations: + albums.map(album => getTotalDuration(album.tracks)), + + tracks: + albums.map(album => album.tracks.length), + + notFromThisGroup: + albums.map(album => !album.groups.includes(group)), + }), + + generate: (data, relations, {language}) => + language.encapsulate('misc.coverGrid', capsule => + relations.coverGrid.slots({ + links: relations.links, + names: data.names, + notFromThisGroup: data.notFromThisGroup, + + images: + stitchArrays({ + image: relations.images, + name: data.names, + }).map(({image, name}) => + image.slots({ + missingSourceContent: + language.$(capsule, 'noCoverArt', { + album: name, + }), + })), + + info: + stitchArrays({ + tracks: data.tracks, + duration: data.durations, + }).map(({tracks, duration}) => + language.$(capsule, 'details.albumLength', { + tracks: language.countTracks(tracks, {unit: true}), + time: language.formatDuration(duration), + })), + })), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js new file mode 100644 index 00000000..b7d01eb5 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js @@ -0,0 +1,39 @@ +import {sortChronologically} from '#sort'; + +export default { + contentDependencies: ['generateGroupGalleryPageAlbumGrid'], + extraDependencies: ['html', 'language'], + + query: (group) => ({ + albums: + sortChronologically(group.albums, {latestFirst: true}), + }), + + relations: (relation, query, group) => ({ + albumGrid: + relation('generateGroupGalleryPageAlbumGrid', + query.albums, + group), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (relations, slots, {html, language}) => + language.encapsulate('groupGalleryPage.albumsByDate', capsule => + html.tag('div', {id: 'group-album-gallery-by-date'}, + slots.attributes, + + {[html.onlyIfContent]: true}, + + html.tag('section', [ + html.tag('h2', + language.$(capsule, 'title')), + + relations.albumGrid, + ]))), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js new file mode 100644 index 00000000..0337275f --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js @@ -0,0 +1,26 @@ +export default { + contentDependencies: ['generateGroupGalleryPageSeriesSection'], + extraDependencies: ['html'], + + relations: (relation, group) => ({ + seriesSections: + group.serieses + .map(series => + relation('generateGroupGalleryPageSeriesSection', series)), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (relations, slots, {html}) => + html.tag('div', {id: 'group-album-gallery-by-series'}, + slots.attributes, + + {[html.onlyIfContent]: true}, + + relations.seriesSections), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageSeriesSection.js b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js new file mode 100644 index 00000000..2ccead5d --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js @@ -0,0 +1,156 @@ +import {sortChronologically} from '#sort'; + +export default { + contentDependencies: [ + 'generateExpandableGallerySection', + 'generateGroupGalleryPageAlbumGrid', + ], + + extraDependencies: ['html', 'language'], + + query(series) { + const query = {}; + + // Includes undated albums. + const albumsLatestFirst = + sortChronologically(series.albums, {latestFirst: true}); + + query.albumsAboveCut = albumsLatestFirst.slice(0, 4); + query.albumsBelowCut = albumsLatestFirst.slice(4); + + query.allAlbumsDated = + series.albums.every(album => album.date); + + query.anyAlbumNotFromThisGroup = + series.albums.some(album => !album.groups.includes(series.group)); + + query.latestAlbum = + albumsLatestFirst + .filter(album => album.date) + .at(0) ?? + null; + + query.earliestAlbum = + albumsLatestFirst + .filter(album => album.date) + .at(-1) ?? + null; + + return query; + }, + + relations: (relation, query, series) => ({ + gallerySection: + relation('generateExpandableGallerySection'), + + gridAboveCut: + relation('generateGroupGalleryPageAlbumGrid', + query.albumsAboveCut, + series.group), + + gridBelowCut: + relation('generateGroupGalleryPageAlbumGrid', + query.albumsBelowCut, + series.group), + }), + + data: (query, series) => ({ + name: + series.name, + + groupName: + series.group.name, + + albums: + series.albums.length, + + tracks: + series.albums + .flatMap(album => album.tracks) + .length, + + allAlbumsDated: + query.allAlbumsDated, + + anyAlbumNotFromThisGroup: + query.anyAlbumNotFromThisGroup, + + earliestAlbumDate: + (query.earliestAlbum + ? query.earliestAlbum.date + : null), + + latestAlbumDate: + (query.latestAlbum + ? query.latestAlbum.date + : null), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('groupGalleryPage.albumSection', capsule => + relations.gallerySection.slots({ + title: data.name, + + contentAboveCut: relations.gridAboveCut, + contentBelowCut: relations.gridBelowCut, + + caption: + language.encapsulate(capsule, 'caption', captionCapsule => + html.tags([ + data.anyAlbumNotFromThisGroup && + language.$(captionCapsule, 'seriesAlbumsNotFromGroup', { + marker: + language.$('misc.coverGrid.details.notFromThisGroup.marker'), + + series: + html.tag('i', data.name), + + group: data.groupName, + }), + + language.encapsulate(captionCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.tracks = + html.tag('b', + language.countTracks(data.tracks, {unit: true})); + + workingOptions.albums = + html.tag('b', + language.countAlbums(data.albums, {unit: true})); + + if (data.allAlbumsDated) { + const earliestDate = data.earliestAlbumDate; + const latestDate = data.latestAlbumDate; + + const earliestYear = earliestDate.getFullYear(); + const latestYear = latestDate.getFullYear(); + + if (earliestYear === latestYear) { + if (data.albums === 1) { + workingCapsule += '.withDate'; + workingOptions.date = + language.formatDate(earliestDate); + } else { + workingCapsule += '.withYear'; + workingOptions.year = + language.formatYear(earliestDate); + } + } else { + workingCapsule += '.withYearRange'; + workingOptions.yearRange = + language.formatYearRange(earliestDate, latestDate); + } + } + + return language.$(workingCapsule, workingOptions); + }), + ], {[html.joinChildren]: html.tag('br')})), + + expandCue: + language.$(capsule, 'expand'), + + collapseCue: + language.$(capsule, 'collapse'), + })), +}; diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js index 87f35656..7b9c2afa 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -1,10 +1,14 @@ +import {stitchArrays} from '#sugar'; + export default { contentDependencies: [ + 'generateColorStyleAttribute', 'generateGroupInfoPageAlbumsSection', 'generateGroupNavLinks', 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', + 'linkArtist', 'linkExternal', 'transformContent', ], @@ -14,9 +18,24 @@ export default { sprawl: ({wikiInfo}) => ({ enableGroupUI: wikiInfo.enableGroupUI, + + wikiColor: + wikiInfo.color, + }), + + query: (_sprawl, group) => ({ + aliasLinkedArtists: + group.closelyLinkedArtists + .filter(({annotation}) => + annotation === 'alias'), + + generalLinkedArtists: + group.closelyLinkedArtists + .filter(({annotation}) => + annotation !== 'alias'), }), - relations: (relation, sprawl, group) => ({ + relations: (relation, query, sprawl, group) => ({ layout: relation('generatePageLayout'), @@ -33,6 +52,17 @@ export default { ? relation('generateGroupSidebar', group) : null), + wikiColorAttribute: + relation('generateColorStyleAttribute', sprawl.wikiColor), + + closeArtistLinks: + query.generalLinkedArtists + .map(({artist}) => relation('linkArtist', artist)), + + aliasArtistLinks: + query.aliasLinkedArtists + .map(({artist}) => relation('linkArtist', artist)), + visitLinks: group.urls .map(url => relation('linkExternal', url)), @@ -44,12 +74,16 @@ export default { relation('generateGroupInfoPageAlbumsSection', group), }), - data: (_sprawl, group) => ({ + data: (query, _sprawl, group) => ({ name: group.name, color: group.color, + + closeArtistAnnotations: + query.generalLinkedArtists + .map(({annotation}) => annotation), }), generate: (data, relations, {html, language}) => @@ -62,6 +96,58 @@ export default { mainContent: [ html.tag('p', {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + language.encapsulate(pageCapsule, 'closelyLinkedArtists', capsule => [ + language.encapsulate(capsule, capsule => { + const [workingCapsule, option] = + (relations.closeArtistLinks.length === 0 + ? [null, null] + : relations.closeArtistLinks.length === 1 + ? [language.encapsulate(capsule, 'one'), 'artist'] + : [language.encapsulate(capsule, 'multiple'), 'artists']); + + if (!workingCapsule) return html.blank(); + + return language.$(workingCapsule, { + [option]: + language.formatUnitList( + stitchArrays({ + link: relations.closeArtistLinks, + annotation: data.closeArtistAnnotations, + }).map(({link, annotation}) => + language.encapsulate(capsule, 'artist', workingCapsule => { + const workingOptions = {}; + + workingOptions.artist = + link.slots({ + attributes: [relations.wikiColorAttribute], + }); + + if (annotation) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = annotation; + } + + return language.$(workingCapsule, workingOptions); + }))), + }); + }), + + language.$(capsule, 'aliases', { + [language.onlyIfOptions]: ['aliases'], + + aliases: + language.formatConjunctionList( + relations.aliasArtistLinks.map(link => + link.slots({ + attributes: [relations.wikiColorAttribute], + }))), + }), + ])), + + html.tag('p', + {[html.onlyIfContent]: true}, language.$('releaseInfo.visitOn', { [language.onlyIfOptions]: ['links'], diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js b/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js new file mode 100644 index 00000000..df42598d --- /dev/null +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js @@ -0,0 +1,47 @@ +import {sortChronologically} from '#sort'; + +export default { + contentDependencies: ['generateGroupInfoPageAlbumsListItem'], + + extraDependencies: ['html'], + + query: (group) => ({ + // Typically, a latestFirst: false (default) chronological sort would be + // appropriate here, but navigation between adjacent albums in a group is a + // rather "essential" movement or relationship in the wiki, and we consider + // the sorting order of a group's gallery page (latestFirst: true) to be + // "canonical" in this regard. We exactly match its sort here, but reverse + // it, to still present earlier albums preceding later ones. + albums: + sortChronologically(group.albums.slice(), {latestFirst: true}) + .reverse(), + }), + + relations: (relation, query, group) => ({ + items: + query.albums + .map(album => + relation('generateGroupInfoPageAlbumsListItem', + album, + group)), + }), + + slots: { + hidden: { + type: 'boolean', + default: false, + }, + }, + + generate: (relations, slots, {html}) => + html.tag('ul', + {id: 'group-album-list-by-date'}, + + slots.hidden && {style: 'display: none'}, + + {[html.onlyIfContent]: true}, + + relations.items + .map(item => + item.slot('accentMode', 'groups'))), +}; diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js b/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js new file mode 100644 index 00000000..bcd5d288 --- /dev/null +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js @@ -0,0 +1,87 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateContentHeading', + 'generateGroupInfoPageAlbumsListItem', + ], + + extraDependencies: ['html', 'language'], + + query: (group) => ({ + closelyLinkedArtists: + group.closelyLinkedArtists + .map(({artist}) => artist), + }), + + relations: (relation, _query, group) => ({ + seriesHeadings: + group.serieses + .map(() => relation('generateContentHeading')), + + seriesItems: + group.serieses + .map(series => series.albums + .map(album => + relation('generateGroupInfoPageAlbumsListItem', + album, + group))), + }), + + data: (query, group) => ({ + seriesNames: + group.serieses + .map(series => series.name), + + seriesItemsShowArtists: + group.serieses.map(series => + (series.showAlbumArtists === 'all' + ? new Array(series.albums.length).fill(true) + : series.showAlbumArtists === 'differing' + ? series.albums.map(album => + album.artistContribs + .map(contrib => contrib.artist) + .some(artist => !query.closelyLinkedArtists.includes(artist))) + : new Array(series.albums.length).fill(false))), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('groupInfoPage.albumList', listCapsule => + html.tag('dl', + {id: 'group-album-list-by-series'}, + {class: 'group-series-list'}, + + {[html.onlyIfContent]: true}, + + stitchArrays({ + name: data.seriesNames, + itemsShowArtists: data.seriesItemsShowArtists, + heading: relations.seriesHeadings, + items: relations.seriesItems, + }).map(({ + name, + itemsShowArtists, + heading, + items, + }) => + html.tags([ + heading.slots({ + tag: 'dt', + title: + language.$(listCapsule, 'series', { + series: name, + }), + }), + + html.tag('dd', + html.tag('ul', + stitchArrays({ + item: items, + showArtists: itemsShowArtists, + }).map(({item, showArtists}) => + item.slots({ + accentMode: + (showArtists ? 'artists' : null), + })))), + ])))), +}; diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js b/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js new file mode 100644 index 00000000..4680cb46 --- /dev/null +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js @@ -0,0 +1,137 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generateAbsoluteDatetimestamp', + 'generateArtistCredit', + 'generateColorStyleAttribute', + 'linkAlbum', + 'linkGroup', + ], + + extraDependencies: ['html', 'language'], + + query: (album, group) => { + const otherCategory = + album.groups + .map(group => group.category) + .find(category => category !== group.category); + + const otherGroups = + album.groups + .filter(group => group.category === otherCategory); + + return {otherGroups}; + }, + + relations: (relation, query, album, _group) => ({ + colorStyle: + relation('generateColorStyleAttribute', album.color), + + albumLink: + relation('linkAlbum', album), + + datetimestamp: + (album.date + ? relation('generateAbsoluteDatetimestamp', album.date) + : null), + + artistCredit: + relation('generateArtistCredit', album.artistContribs, []), + + otherGroupLinks: + query.otherGroups + .map(group => relation('linkGroup', group)), + }), + + data: (_query, album, group) => ({ + groupName: + group.name, + + notFromThisGroup: + !group.albums.includes(album), + }), + + slots: { + accentMode: { + validate: v => v.is('groups', 'artists'), + }, + }, + + generate: (data, relations, slots, {html, language}) => + html.tag('li', + relations.colorStyle, + + language.encapsulate('groupInfoPage.albumList.item', itemCapsule => + language.encapsulate(itemCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.album = + relations.albumLink.slot('color', false); + + const yearCapsule = language.encapsulate(itemCapsule, 'withYear'); + + if (relations.datetimestamp) { + workingCapsule += '.withYear'; + workingOptions.yearAccent = + language.$(yearCapsule, 'accent', { + year: + relations.datetimestamp.slots({style: 'year', tooltip: true}), + }); + } + + const otherGroupCapsule = language.encapsulate(itemCapsule, 'withOtherGroup'); + + if ( + (slots.accentMode === 'groups' || + slots.accentMode === null) && + data.notFromThisGroup + ) { + workingCapsule += '.withOtherGroup'; + workingOptions.otherGroupAccent = + html.tag('span', {class: 'other-group-accent'}, + language.$(otherGroupCapsule, 'notFromThisGroup', { + group: + data.groupName, + })); + } else if ( + slots.accentMode === 'groups' && + !empty(relations.otherGroupLinks) + ) { + workingCapsule += '.withOtherGroup'; + workingOptions.otherGroupAccent = + html.tag('span', {class: 'other-group-accent'}, + language.$(otherGroupCapsule, 'accent', { + groups: + language.formatConjunctionList( + relations.otherGroupLinks.map(groupLink => + groupLink.slot('color', false))), + })); + } + + const artistCapsule = language.encapsulate(itemCapsule, 'withArtists'); + const {artistCredit} = relations; + + artistCredit.setSlots({ + normalStringKey: + artistCapsule + '.by', + + featuringStringKey: + artistCapsule + '.featuring', + + normalFeaturingStringKey: + artistCapsule + '.by.featuring', + }); + + if (slots.accentMode === 'artists' && !html.isBlank(artistCredit)) { + workingCapsule += '.withArtists'; + workingOptions.by = + html.tag('span', {class: 'by'}, + // TODO: This is obviously evil. + html.metatag('chunkwrap', {split: /,| (?=and)/}, + html.resolve(artistCredit))); + } + + return language.$(workingCapsule, workingOptions); + }))), +}; diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js index 8899e98e..0b678e9d 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js @@ -1,68 +1,29 @@ -import {empty} from '#sugar'; -import {stitchArrays} from '#sugar'; - export default { contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateColorStyleAttribute', 'generateContentHeading', - 'linkAlbum', + 'generateGroupInfoPageAlbumsListByDate', + 'generateGroupInfoPageAlbumsListBySeries', + 'generateIntrapageDotSwitcher', '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) => ({ + relations: (relation, 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)), + albumsListByDate: + relation('generateGroupInfoPageAlbumsListByDate', group), - otherGroupLinks: - query.albumOtherGroups - .map(groups => groups - .map(group => relation('linkGroup', group))), + albumsListBySeries: + relation('generateGroupInfoPageAlbumsListBySeries', group), - datetimestamps: - group.albums.map(album => - (album.date - ? relation('generateAbsoluteDatetimestamp', album.date) - : null)), + viewSwitcher: + relation('generateIntrapageDotSwitcher'), }), generate: (relations, {html, language}) => @@ -78,59 +39,55 @@ export default { html.tag('p', {[html.onlyIfSiblings]: true}, - language.encapsulate(pageCapsule, 'viewAlbumGallery', capsule => - language.$(capsule, { - link: + language.encapsulate(pageCapsule, 'viewAlbumGallery', viewAlbumGalleryCapsule => + language.encapsulate(viewAlbumGalleryCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.link = relations.galleryLink - .slot('content', language.$(capsule, 'link')), + .slot('content', + language.$(viewAlbumGalleryCapsule, 'link')); + + if ( + !html.isBlank(relations.albumsListByDate) && + !html.isBlank(relations.albumsListBySeries) + ) { + workingCapsule += '.withViewSwitcher'; + workingOptions.viewSwitcher = + html.tag('span', {class: 'group-view-switcher'}, + language.encapsulate(pageCapsule, 'viewSwitcher', switcherCapsule => + language.$(switcherCapsule, { + options: + relations.viewSwitcher.slots({ + initialOptionIndex: 0, + + titles: [ + language.$(switcherCapsule, 'bySeries'), + language.$(switcherCapsule, 'byDate'), + ], + + targetIDs: [ + 'group-album-list-by-series', + 'group-album-list-by-date', + ], + }), + }))); + } + + return language.$(workingCapsule, workingOptions); }))), - 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); - }))))), + ((!html.isBlank(relations.albumsListByDate) && + !html.isBlank(relations.albumsListBySeries)) + + ? [ + relations.albumsListBySeries, + relations.albumsListByDate.slot('hidden', true), + ] + + : [ + relations.albumsListBySeries, + relations.albumsListByDate, + ]), ]))), }; diff --git a/src/content/dependencies/generateGroupNavAccent.js b/src/content/dependencies/generateGroupNavAccent.js new file mode 100644 index 00000000..0e4ebe8a --- /dev/null +++ b/src/content/dependencies/generateGroupNavAccent.js @@ -0,0 +1,53 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generateInterpageDotSwitcher', + 'linkGroup', + 'linkGroupGallery', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, group) => ({ + switcher: + relation('generateInterpageDotSwitcher'), + + infoLink: + relation('linkGroup', group), + + galleryLink: + (empty(group.albums) + ? null + : relation('linkGroupGallery', group)), + }), + + slots: { + currentExtra: { + validate: v => v.is('gallery'), + }, + }, + + generate: (relations, slots, {language}) => + relations.switcher.slots({ + links: [ + relations.infoLink.slots({ + attributes: [ + slots.currentExtra === null && + {class: 'current'}, + ], + + content: language.$('misc.nav.info'), + }), + + relations.galleryLink?.slots({ + attributes: [ + slots.currentExtra === 'gallery' && + {class: 'current'}, + ], + + content: language.$('misc.nav.gallery'), + }), + ], + }), +}; diff --git a/src/content/dependencies/generateGroupNavLinks.js b/src/content/dependencies/generateGroupNavLinks.js index 5cde2ab4..bdc3ee4c 100644 --- a/src/content/dependencies/generateGroupNavLinks.js +++ b/src/content/dependencies/generateGroupNavLinks.js @@ -1,48 +1,25 @@ -import {empty} from '#sugar'; - export default { - contentDependencies: [ - 'linkGroup', - 'linkGroupGallery', - ], - + contentDependencies: ['generateGroupNavAccent', 'linkGroup'], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({groupCategoryData, wikiInfo}) { - return { - groupCategoryData, - enableGroupUI: wikiInfo.enableGroupUI, - enableListings: wikiInfo.enableListings, - }; - }, - - relations(relation, sprawl, group) { - if (!sprawl.enableGroupUI) { - return {}; - } - - const relations = {}; + sprawl: ({groupCategoryData, wikiInfo}) => ({ + groupCategoryData, + enableGroupUI: wikiInfo.enableGroupUI, + enableListings: wikiInfo.enableListings, + }), - relations.mainLink = - relation('linkGroup', group); + relations: (relation, _sprawl, group) => ({ + mainLink: + relation('linkGroup', group), - relations.infoLink = - relation('linkGroup', group); + accent: + relation('generateGroupNavAccent', group), + }), - if (!empty(group.albums)) { - relations.galleryLink = - relation('linkGroupGallery', group); - } - - return relations; - }, - - data(sprawl) { - return { - enableGroupUI: sprawl.enableGroupUI, - enableListings: sprawl.enableListings, - }; - }, + data: (sprawl, _group) => ({ + enableGroupUI: sprawl.enableGroupUI, + enableListings: sprawl.enableListings, + }), slots: { showExtraLinks: {type: 'boolean', default: false}, @@ -52,53 +29,31 @@ export default { }, }, - generate(data, relations, slots, {language}) { - if (!data.enableGroupUI) { - return [ - {auto: 'home'}, - {auto: 'current'}, - ]; - } - - const infoLink = - relations.infoLink.slots({ - attributes: {class: slots.currentExtra === null && 'current'}, - content: language.$('misc.nav.info'), - }); - - const extraLinks = [ - relations.galleryLink?.slots({ - attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('misc.nav.gallery'), - }), - ]; - - const extrasPart = - (empty(extraLinks) - ? '' - : language.formatUnitList([infoLink, ...extraLinks])); - - const accent = - (extrasPart - ? `(${extrasPart})` - : null); - - return [ - {auto: 'home'}, - - data.enableListings && - { - path: ['localized.listingIndex'], - title: language.$('listingIndex.title'), - }, - - { - accent, - html: - language.$('groupPage.nav.group', { - group: relations.mainLink, - }), - }, - ].filter(Boolean); - }, + generate: (data, relations, slots, {language}) => + (data.enableGroupUI + ? [ + {auto: 'home'}, + + data.enableListings && + { + path: ['localized.listingIndex'], + title: language.$('listingIndex.title'), + }, + + { + html: + language.$('groupPage.nav.group', { + group: relations.mainLink, + }), + + accent: + relations.accent + .slot('currentExtra', slots.currentExtra), + }, + ].filter(Boolean) + + : [ + {auto: 'home'}, + {auto: 'current'}, + ]), }; diff --git a/src/content/dependencies/generateGroupSecondaryNav.js b/src/content/dependencies/generateGroupSecondaryNav.js index a4f81313..c48f3142 100644 --- a/src/content/dependencies/generateGroupSecondaryNav.js +++ b/src/content/dependencies/generateGroupSecondaryNav.js @@ -1,104 +1,20 @@ -import {atOffset} from '#sugar'; - export default { contentDependencies: [ - 'generateColorStyleAttribute', - 'generatePreviousNextLinks', 'generateSecondaryNav', - 'linkGroupDynamically', - 'linkListing', + 'generateGroupSecondaryNavCategoryPart', ], - extraDependencies: ['html', 'language', 'wikiData'], - - sprawl: ({listingSpec, wikiInfo}) => ({ - groupsByCategoryListing: - (wikiInfo.enableListings - ? listingSpec - .find(l => l.directory === 'groups/by-category') - : null), - }), - - query(sprawl, group) { - const groups = group.category.groups; - const index = groups.indexOf(group); - - return { - previousGroup: - atOffset(groups, index, -1), - - nextGroup: - atOffset(groups, index, +1), - }; - }, - - relations(relation, query, sprawl, group) { - const relations = {}; - - relations.secondaryNav = - relation('generateSecondaryNav'); - - if (sprawl.groupsByCategoryListing) { - relations.categoryLink = - relation('linkListing', sprawl.groupsByCategoryListing); - } + relations: (relation, group) => ({ + secondaryNav: + relation('generateSecondaryNav'), - relations.colorStyle = - relation('generateColorStyleAttribute', group.category.color); - - if (query.previousGroup || query.nextGroup) { - relations.previousNextLinks = - relation('generatePreviousNextLinks'); - } - - relations.previousGroupLink = - (query.previousGroup - ? relation('linkGroupDynamically', query.previousGroup) - : null); - - relations.nextGroupLink = - (query.nextGroup - ? relation('linkGroupDynamically', query.nextGroup) - : null); - - return relations; - }, - - data: (query, sprawl, group) => ({ - categoryName: group.category.name, + categoryPart: + relation('generateGroupSecondaryNavCategoryPart', group.category, group), }), - generate(data, relations, {html, language}) { - const previousNextPart = - (relations.previousNextLinks - ? relations.previousNextLinks - .slots({ - previousLink: relations.previousGroupLink, - nextLink: relations.nextGroupLink, - id: true, - }) - .content /* TODO: Kludge. */ - : null); - - const {categoryLink} = relations; - - categoryLink?.setSlot('content', data.categoryName); - - return relations.secondaryNav.slots({ - class: 'nav-links-groups', - content: - (previousNextPart - ? html.tag('span', {class: 'nav-link'}, - relations.colorStyle.slot('context', 'primary-only'), - - [ - categoryLink?.slot('color', false), - `(${language.formatUnitList(previousNextPart)})`, - ]) - : categoryLink - ? html.tag('span', {class: 'nav-link'}, - categoryLink) - : html.blank()), - }); - }, + generate: (relations) => + relations.secondaryNav.slots({ + attributes: {class: 'nav-links-groups'}, + content: relations.categoryPart, + }), }; diff --git a/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js b/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js new file mode 100644 index 00000000..b2adb9f8 --- /dev/null +++ b/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js @@ -0,0 +1,79 @@ +import {atOffset} from '#sugar'; + +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generateSecondaryNavParentSiblingsPart', + 'linkGroupDynamically', + 'linkListing', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({listingSpec, wikiInfo}) => ({ + groupsByCategoryListing: + (wikiInfo.enableListings + ? listingSpec + .find(l => l.directory === 'groups/by-category') + : null), + }), + + query(sprawl, category, group) { + const groups = category.groups; + const index = groups.indexOf(group); + + return { + previousGroup: + atOffset(groups, index, -1), + + nextGroup: + atOffset(groups, index, +1), + }; + }, + + relations: (relation, query, sprawl, category, group) => ({ + parentSiblingsPart: + relation('generateSecondaryNavParentSiblingsPart'), + + categoryLink: + (sprawl.groupsByCategoryListing + ? relation('linkListing', sprawl.groupsByCategoryListing) + : null), + + colorStyle: + relation('generateColorStyleAttribute', group.category.color), + + previousGroupLink: + (query.previousGroup + ? relation('linkGroupDynamically', query.previousGroup) + : null), + + nextGroupLink: + (query.nextGroup + ? relation('linkGroupDynamically', query.nextGroup) + : null), + }), + + data: (_query, _sprawl, category, _group) => ({ + name: category.name, + }), + + generate: (data, relations, {language}) => + relations.parentSiblingsPart.slots({ + colorStyle: relations.colorStyle, + id: true, + + mainLink: + (relations.categoryLink + ? relations.categoryLink.slots({ + content: language.sanitize(data.name), + }) + : null), + + previousLink: relations.previousGroupLink, + nextLink: relations.nextGroupLink, + + stringsKey: 'groupPage.secondaryNav.category', + mainLinkOption: 'category', + }), +}; diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js index d52c77b8..208ccd07 100644 --- a/src/content/dependencies/generateGroupSidebarCategoryDetails.js +++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js @@ -59,8 +59,7 @@ export default { html.tag('span', language.$(capsule, 'category', { category: - html.tag('span', {class: 'group-name'}, - data.name), + html.tag('b', data.name), }))), html.tag('ul', diff --git a/src/content/dependencies/generateImageOverlay.js b/src/content/dependencies/generateImageOverlay.js new file mode 100644 index 00000000..cfb78a1b --- /dev/null +++ b/src/content/dependencies/generateImageOverlay.js @@ -0,0 +1,50 @@ +export default { + extraDependencies: ['html', 'language'], + + generate: ({html, language}) => + html.tag('div', {id: 'image-overlay-container'}, + html.tag('div', {id: 'image-overlay-content-container'}, [ + html.tag('span', {id: 'image-overlay-image-area'}, + html.tag('span', {id: 'image-overlay-image-layout'}, [ + html.tag('img', {id: 'image-overlay-image'}), + html.tag('img', {id: 'image-overlay-image-thumb'}), + ])), + + 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')), + ]), + ])), + ])), +}; diff --git a/src/content/dependencies/generateInterpageDotSwitcher.js b/src/content/dependencies/generateInterpageDotSwitcher.js new file mode 100644 index 00000000..5a33444e --- /dev/null +++ b/src/content/dependencies/generateInterpageDotSwitcher.js @@ -0,0 +1,31 @@ +export default { + contentDependencies: ['generateDotSwitcherTemplate'], + extraDependencies: ['html', 'language'], + + relations: (relation) => ({ + template: + relation('generateDotSwitcherTemplate'), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + links: { + validate: v => v.strictArrayOf(v.isHTML), + }, + }, + + generate: (relations, slots) => + relations.template.slots({ + attributes: [ + {class: 'interpage'}, + slots.attributes, + ], + + // TODO: Do something to set a class on a link to the current page?? + options: slots.links, + }), +}; diff --git a/src/content/dependencies/generateIntrapageDotSwitcher.js b/src/content/dependencies/generateIntrapageDotSwitcher.js new file mode 100644 index 00000000..1d58367d --- /dev/null +++ b/src/content/dependencies/generateIntrapageDotSwitcher.js @@ -0,0 +1,49 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['generateDotSwitcherTemplate'], + extraDependencies: ['html', 'language'], + + relations: (relation) => ({ + template: + relation('generateDotSwitcherTemplate'), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + initialOptionIndex: {type: 'number'}, + + titles: { + validate: v => v.strictArrayOf(v.isHTML), + }, + + targetIDs: { + validate: v => v.strictArrayOf(v.isString), + }, + }, + + generate: (relations, slots, {html, language}) => + relations.template.slots({ + attributes: [ + {class: 'intrapage'}, + slots.attributes, + ], + + initialOptionIndex: slots.initialOptionIndex, + + options: + stitchArrays({ + title: slots.titles, + targetID: slots.targetIDs, + }).map(({title, targetID}) => + html.tag('a', {href: '#'}, + {'data-target-id': targetID}, + {[html.onlyIfContent]: true}, + + language.sanitize(title))), + }), +}; diff --git a/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js b/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js new file mode 100644 index 00000000..0a929429 --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js @@ -0,0 +1,22 @@ +export default { + contentDependencies: ['generateListAllAdditionalFilesChunk'], + extraDependencies: ['html', 'language'], + + relations: (relation, _album, additionalFiles) => ({ + chunk: + relation('generateListAllAdditionalFilesChunk', additionalFiles), + }), + + slots: { + stringsKey: {type: 'string'}, + }, + + generate: (relations, slots, {language}) => + language.encapsulate('listingPage', slots.stringsKey, pageCapsule => + relations.chunk.slots({ + title: + language.$(pageCapsule, 'albumFiles'), + + stringsKey: slots.stringsKey, + })), +}; diff --git a/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js b/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js new file mode 100644 index 00000000..a0af1375 --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js @@ -0,0 +1,51 @@ +export default { + contentDependencies: [ + 'generateContentHeading', + 'generateListAllAdditionalFilesAlbumChunk', + 'generateListAllAdditionalFilesTrackChunk', + 'linkAlbum', + ], + + extraDependencies: ['html'], + + relations: (relation, album, property) => ({ + heading: + relation('generateContentHeading'), + + albumLink: + relation('linkAlbum', album), + + albumChunk: + relation('generateListAllAdditionalFilesAlbumChunk', + album, + album[property] ?? []), + + trackChunks: + album.tracks.map(track => + relation('generateListAllAdditionalFilesTrackChunk', + track, + track[property] ?? [])), + }), + + slots: { + stringsKey: {type: 'string'}, + }, + + generate: (relations, slots, {html}) => + html.tags([ + relations.heading.slots({ + tag: 'h3', + title: relations.albumLink, + }), + + html.tag('dl', + {[html.onlyIfContent]: true}, + + [ + relations.albumChunk.slot('stringsKey', slots.stringsKey), + + relations.trackChunks.map(trackChunk => + trackChunk.slot('stringsKey', slots.stringsKey)), + ]), + ]), +}; diff --git a/src/content/dependencies/generateListAllAdditionalFilesChunk.js b/src/content/dependencies/generateListAllAdditionalFilesChunk.js index 659cf4e5..df652efd 100644 --- a/src/content/dependencies/generateListAllAdditionalFilesChunk.js +++ b/src/content/dependencies/generateListAllAdditionalFilesChunk.js @@ -1,91 +1,99 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { + contentDependencies: ['linkAdditionalFile'], extraDependencies: ['html', 'language'], + relations: (relation, additionalFiles) => ({ + links: + additionalFiles + .map(file => file.filenames + .map(filename => relation('linkAdditionalFile', file, filename))), + }), + + data: (additionalFiles) => ({ + titles: + additionalFiles + .map(file => file.title), + + filenames: + additionalFiles + .map(file => file.filenames), + }), + slots: { title: { type: 'html', mutable: false, }, - additionalFileTitles: { - validate: v => v.strictArrayOf(v.isHTML), - }, - - additionalFileLinks: { - validate: v => v.strictArrayOf(v.strictArrayOf(v.isHTML)), - }, - - additionalFileFiles: { - validate: v => v.strictArrayOf(v.strictArrayOf(v.isString)), - }, - stringsKey: {type: 'string'}, }, - generate(slots, {html, language}) { - if (empty(slots.additionalFileLinks)) { - return html.blank(); - } + generate: (data, relations, slots, {html, language}) => + language.encapsulate('listingPage', slots.stringsKey, pageCapsule => + html.tags([ + html.tag('dt', + {[html.onlyIfSiblings]: true}, + slots.title), - return html.tags([ - html.tag('dt', slots.title), - html.tag('dd', - html.tag('ul', - stitchArrays({ - additionalFileTitle: slots.additionalFileTitles, - additionalFileLinks: slots.additionalFileLinks, - additionalFileFiles: slots.additionalFileFiles, - }).map(({ - additionalFileTitle, - additionalFileLinks, - additionalFileFiles, - }) => - language.encapsulate('listingPage', slots.stringsKey, 'file', capsule => - (additionalFileLinks.length === 1 - ? html.tag('li', - additionalFileLinks[0].slots({ - content: - language.$(capsule, { - title: additionalFileTitle, - }), - })) + html.tag('dd', + {[html.onlyIfContent]: true}, - : additionalFileLinks.length === 0 - ? html.tag('li', - language.$(capsule, 'withNoFiles', { - title: additionalFileTitle, - })) + html.tag('ul', + {[html.onlyIfContent]: true}, - : 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), + stitchArrays({ + title: data.titles, + links: relations.links, + filenames: data.filenames, + }).map(({ + title, + links, + filenames, + }) => + language.encapsulate(pageCapsule, 'file', capsule => + (links.length === 1 + ? html.tag('li', + links[0].slots({ + content: + language.$(capsule, { + title: title, + }), + })) - files: - language.countAdditionalFiles( - additionalFileLinks.length, - {unit: true}), - }))), + : links.length === 0 + ? html.tag('li', + language.$(capsule, 'withNoFiles', { + title: title, + })) - html.tag('ul', - stitchArrays({ - additionalFileLink: additionalFileLinks, - additionalFileFile: additionalFileFiles, - }).map(({additionalFileLink, additionalFileFile}) => - html.tag('li', - additionalFileLink.slots({ - content: - language.$(capsule, { - title: additionalFileFile, - }), - })))), - ]))))))), - ]); - }, + : html.tag('li', {class: 'has-details'}, + html.tag('details', [ + html.tag('summary', + html.tag('span', + language.$(capsule, 'withMultipleFiles', { + title: + html.tag('b', title), + + files: + language.countAdditionalFiles( + links.length, + {unit: true}), + }))), + + html.tag('ul', + stitchArrays({ + link: links, + filename: filenames, + }).map(({link, filename}) => + html.tag('li', + link.slots({ + content: + language.$(capsule, { + title: filename, + }), + })))), + ]))))))), + ])), }; diff --git a/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js b/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js new file mode 100644 index 00000000..b2e5addf --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js @@ -0,0 +1,23 @@ +export default { + contentDependencies: ['generateListAllAdditionalFilesChunk', 'linkTrack'], + extraDependencies: ['html'], + + relations: (relation, track, additionalFiles) => ({ + trackLink: + relation('linkTrack', track), + + chunk: + relation('generateListAllAdditionalFilesChunk', additionalFiles), + }), + + slots: { + stringsKey: {type: 'string'}, + }, + + generate: (relations, slots) => + relations.chunk.slots({ + title: relations.trackLink, + stringsKey: slots.stringsKey, + }), +}; + diff --git a/src/content/dependencies/generateListingIndexList.js b/src/content/dependencies/generateListingIndexList.js index ed153652..78622e6e 100644 --- a/src/content/dependencies/generateListingIndexList.js +++ b/src/content/dependencies/generateListingIndexList.js @@ -107,8 +107,8 @@ export default { [ html.tag('summary', - html.tag('span', {class: 'group-name'}, - targetTitle)), + html.tag('span', + html.tag('b', targetTitle))), listingLinkList, ]))); diff --git a/src/content/dependencies/generateLyricsEntry.js b/src/content/dependencies/generateLyricsEntry.js new file mode 100644 index 00000000..0c91ce0c --- /dev/null +++ b/src/content/dependencies/generateLyricsEntry.js @@ -0,0 +1,91 @@ +export default { + contentDependencies: ['linkArtist', 'linkExternal', 'transformContent'], + extraDependencies: ['html', 'language'], + + relations: (relation, entry) => ({ + content: + relation('transformContent', entry.body), + + artistText: + relation('transformContent', entry.artistText), + + artistLinks: + entry.artists + .filter(artist => artist.name !== 'HSMusic Wiki') // smh + .map(artist => relation('linkArtist', artist)), + + sourceLinks: + entry.sourceURLs + .map(url => relation('linkExternal', url)), + + originDetails: + relation('transformContent', entry.originDetails), + }), + + data: (entry) => ({ + isWikiLyrics: + entry.isWikiLyrics, + + hasSquareBracketAnnotations: + entry.hasSquareBracketAnnotations, + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('misc.lyrics', capsule => + html.tag('div', {class: 'lyrics-entry'}, + slots.attributes, + + [ + html.tag('p', {class: 'lyrics-details'}, + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + language.$(capsule, 'source', { + [language.onlyIfOptions]: ['source'], + + source: + language.formatUnitList( + relations.sourceLinks.map(link => + link.slots({ + indicateExternal: true, + tab: 'separate', + }))), + }), + + data.isWikiLyrics && + language.$(capsule, 'contributors', { + [language.onlyIfOptions]: ['contributors'], + + contributors: + (html.isBlank(relations.artistText) + ? language.formatUnitList(relations.artistLinks) + : relations.artistText.slot('mode', 'inline')), + }), + + // This check is doubled up only for clarity: entries are coded + // in data so that `hasSquareBracketAnnotations` is only true + // if `isWikiLyrics` is also true. + data.isWikiLyrics && + data.hasSquareBracketAnnotations && + language.$(capsule, 'squareBracketAnnotations'), + ]), + + html.tag('p', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + })), + + relations.content.slot('mode', 'lyrics'), + ])), +}; diff --git a/src/content/dependencies/generateLyricsSection.js b/src/content/dependencies/generateLyricsSection.js new file mode 100644 index 00000000..f6b719a9 --- /dev/null +++ b/src/content/dependencies/generateLyricsSection.js @@ -0,0 +1,81 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateContentHeading', + 'generateIntrapageDotSwitcher', + 'generateLyricsEntry', + 'transformContent', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, entries) => ({ + heading: + relation('generateContentHeading'), + + switcher: + relation('generateIntrapageDotSwitcher'), + + entries: + entries + .map(entry => relation('generateLyricsEntry', entry)), + + annotations: + entries + .map(entry => entry.annotation) + .map(annotation => relation('transformContent', annotation)), + }), + + data: (entries) => ({ + ids: + Array.from( + {length: entries.length}, + (_, index) => 'lyrics-entry-' + index), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('releaseInfo.lyrics', capsule => + html.tags([ + relations.heading + .slots({ + attributes: {id: 'lyrics'}, + title: language.$(capsule), + }), + + html.tag('p', {class: 'lyrics-switcher'}, + {[html.onlyIfContent]: true}, + + language.$(capsule, 'switcher', { + [language.onlyIfOptions]: ['entries'], + + entries: + relations.switcher.slots({ + initialOptionIndex: 0, + + titles: + relations.annotations.map(annotation => + annotation.slots({ + mode: 'inline', + textOnly: true, + })), + + targetIDs: + data.ids, + }), + })), + + stitchArrays({ + entry: relations.entries, + id: data.ids, + }).map(({entry, id}, index) => + entry.slots({ + attributes: [ + {id}, + + index >= 1 && + {style: 'display: none'}, + ], + })), + ])), +}; diff --git a/src/content/dependencies/generateNewsEntryNavAccent.js b/src/content/dependencies/generateNewsEntryNavAccent.js new file mode 100644 index 00000000..5d168e41 --- /dev/null +++ b/src/content/dependencies/generateNewsEntryNavAccent.js @@ -0,0 +1,40 @@ +export default { + contentDependencies: [ + 'generateInterpageDotSwitcher', + 'generateNextLink', + 'generatePreviousLink', + 'linkNewsEntry', + ], + + relations: (relation, previousEntry, nextEntry) => ({ + switcher: + relation('generateInterpageDotSwitcher'), + + previousLink: + relation('generatePreviousLink'), + + nextLink: + relation('generateNextLink'), + + previousEntryLink: + (previousEntry + ? relation('linkNewsEntry', previousEntry) + : null), + + nextEntryLink: + (nextEntry + ? relation('linkNewsEntry', nextEntry) + : null), + }), + + generate: (relations) => + relations.switcher.slots({ + links: [ + relations.previousLink + .slot('link', relations.previousEntryLink), + + relations.nextLink + .slot('link', relations.nextEntryLink), + ], + }), +}; diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js index 2c382cfa..4abd87d1 100644 --- a/src/content/dependencies/generateNewsEntryPage.js +++ b/src/content/dependencies/generateNewsEntryPage.js @@ -3,10 +3,9 @@ import {atOffset} from '#sugar'; export default { contentDependencies: [ + 'generateNewsEntryNavAccent', 'generateNewsEntryReadAnotherLinks', 'generatePageLayout', - 'generatePreviousNextLinks', - 'linkNewsEntry', 'linkNewsIndex', 'transformContent', ], @@ -31,65 +30,46 @@ export default { return {previousEntry, nextEntry}; }, - relations(relation, query, sprawl, newsEntry) { - const relations = {}; + relations: (relation, query, sprawl, newsEntry) => ({ + layout: + relation('generatePageLayout'), - relations.layout = - relation('generatePageLayout'); + content: + relation('transformContent', newsEntry.content), - relations.content = - relation('transformContent', newsEntry.content); + newsIndexLink: + relation('linkNewsIndex'), - relations.newsIndexLink = - relation('linkNewsIndex'); + readAnotherLinks: + relation('generateNewsEntryReadAnotherLinks', + newsEntry, + query.previousEntry, + query.nextEntry), - relations.currentEntryLink = - relation('linkNewsEntry', newsEntry); + navAccent: + relation('generateNewsEntryNavAccent', + query.previousEntry, + query.nextEntry), + }), - if (query.previousEntry || query.nextEntry) { - relations.previousNextLinks = - relation('generatePreviousNextLinks'); + data: (query, sprawl, newsEntry) => ({ + name: newsEntry.name, + date: newsEntry.date, - relations.readAnotherLinks = - relation('generateNewsEntryReadAnotherLinks', - newsEntry, - query.previousEntry, - query.nextEntry); + daysSincePreviousEntry: + query.previousEntry && + Math.round((newsEntry.date - query.previousEntry.date) / 86400000), - if (query.previousEntry) { - relations.previousEntryNavLink = - relation('linkNewsEntry', query.previousEntry); - } + daysUntilNextEntry: + query.nextEntry && + Math.round((query.nextEntry.date - newsEntry.date) / 86400000), - if (query.nextEntry) { - relations.nextEntryNavLink = - relation('linkNewsEntry', query.nextEntry); - } - } + previousEntryDate: + query.previousEntry?.date, - return relations; - }, - - data(query, sprawl, newsEntry) { - return { - name: newsEntry.name, - date: newsEntry.date, - - daysSincePreviousEntry: - query.previousEntry && - Math.round((newsEntry.date - query.previousEntry.date) / 86400000), - - daysUntilNextEntry: - query.nextEntry && - Math.round((query.nextEntry.date - newsEntry.date) / 86400000), - - previousEntryDate: - query.previousEntry?.date, - - nextEntryDate: - query.nextEntry?.date, - }; - }, + nextEntryDate: + query.nextEntry?.date, + }), generate: (data, relations, {html, language}) => language.encapsulate('newsEntryPage', pageCapsule => @@ -118,13 +98,7 @@ export default { {html: relations.newsIndexLink}, { auto: 'current', - accent: - (relations.previousNextLinks - ? `(${language.formatUnitList(relations.previousNextLinks.slots({ - previousLink: relations.previousEntryNavLink ?? null, - nextLink: relations.nextEntryNavLink ?? null, - }).content)})` - : null), + accent: relations.navAccent, }, ], })), diff --git a/src/content/dependencies/generateNextLink.js b/src/content/dependencies/generateNextLink.js new file mode 100644 index 00000000..2e48cd2b --- /dev/null +++ b/src/content/dependencies/generateNextLink.js @@ -0,0 +1,13 @@ +export default { + contentDependencies: ['generatePreviousNextLink'], + + relations: (relation) => ({ + link: + relation('generatePreviousNextLink'), + }), + + generate: (relations) => + relations.link.slots({ + direction: 'next', + }), +}; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 7e9e49a0..0326f415 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -1,13 +1,16 @@ import {openAggregate} from '#aggregate'; -import {empty} from '#sugar'; +import {atOffset, empty, repeat} from '#sugar'; export default { contentDependencies: [ - 'generateColorStyleRules', + 'generateColorStyleTag', 'generateFooterLocalizationLinks', + 'generateImageOverlay', 'generatePageSidebar', 'generateSearchSidebarBox', + 'generateStaticURLStyleTag', 'generateStickyHeadingContainer', + 'generateWikiWallpaperStyleTag', 'transformContent', ], @@ -16,25 +19,24 @@ export default { 'html', 'language', 'pagePath', + 'pagePathStringFromRoot', 'to', 'wikiData', ], - sprawl({wikiInfo}) { - return { - enableSearch: wikiInfo.enableSearch, - footerContent: wikiInfo.footerContent, - wikiColor: wikiInfo.color, - wikiName: wikiInfo.nameShort, - }; - }, + sprawl: ({wikiInfo}) => ({ + enableSearch: wikiInfo.enableSearch, + footerContent: wikiInfo.footerContent, + wikiColor: wikiInfo.color, + wikiName: wikiInfo.nameShort, + canonicalBase: wikiInfo.canonicalBase, + }), - data({wikiColor, wikiName}) { - return { - wikiColor, - wikiName, - }; - }, + data: (sprawl) => ({ + wikiColor: sprawl.wikiColor, + wikiName: sprawl.wikiName, + canonicalBase: sprawl.canonicalBase, + }), relations(relation, sprawl) { const relations = {}; @@ -58,8 +60,17 @@ export default { relation('transformContent', sprawl.footerContent); } - relations.colorStyleRules = - relation('generateColorStyleRules'); + relations.colorStyleTag = + relation('generateColorStyleTag'); + + relations.staticURLStyleTag = + relation('generateStaticURLStyleTag'); + + relations.wikiWallpaperStyleTag = + relation('generateWikiWallpaperStyleTag'); + + relations.imageOverlay = + relation('generateImageOverlay'); return relations; }, @@ -71,8 +82,13 @@ export default { }, showWikiNameInTitle: { - type: 'boolean', - default: true, + validate: v => v.is(true, false, 'auto'), + default: 'auto', + }, + + subtitle: { + type: 'html', + mutable: false, }, showSearch: { @@ -85,7 +101,7 @@ export default { mutable: false, }, - cover: { + artworkColumnContent: { type: 'html', mutable: false, }, @@ -99,9 +115,9 @@ export default { color: {validate: v => v.isColor}, - styleRules: { - validate: v => v.sparseArrayOf(v.isHTML), - default: [], + styleTags: { + type: 'html', + mutable: false, }, mainClasses: { @@ -228,6 +244,7 @@ export default { html, language, pagePath, + pagePathStringFromRoot, to, }) { const colors = getColors(slots.color ?? data.wikiColor); @@ -241,6 +258,41 @@ export default { const mainContentHTML = html.tags([slots.mainContent]).toString(); const hasID = id => mainContentHTML.includes(`id="${id}"`); + const oEmbedJSONHref = + (hasSocialEmbed && data.canonicalBase + ? data.canonicalBase + + pagePathStringFromRoot + + 'oembed.json' + : null); + + const canonicalHref = + (data.canonicalBase + ? data.canonicalBase + pagePathStringFromRoot + : null); + + const primaryCover = (() => { + const apparentFirst = tag => html.smooth(tag).content[0]; + + const maybeTemplate = + apparentFirst(slots.artworkColumnContent); + + if (!maybeTemplate) return null; + + const maybeTemplateContent = + html.resolve(maybeTemplate, {normalize: 'tag'}); + + const maybeCoverArtwork = + apparentFirst(maybeTemplateContent); + + if (!maybeCoverArtwork) return null; + + if (maybeCoverArtwork.attributes.has('class', 'cover-artwork')) { + return maybeTemplate; + } else { + return null; + } + })(); + const titleContentsHTML = (html.isBlank(slots.title) ? null @@ -255,12 +307,26 @@ export default { (html.isBlank(slots.title) ? null : slots.headingMode === 'sticky' - ? relations.stickyHeadingContainer.slots({ - title: titleContentsHTML, - cover: slots.cover, - }) + ? [ + relations.stickyHeadingContainer.slots({ + title: titleContentsHTML, + cover: primaryCover, + }), + + relations.stickyHeadingContainer.clone().slots({ + rootAttributes: {inert: true}, + }), + ] : html.tag('h1', titleContentsHTML)); + // TODO: There could be neat interactions with the sticky heading here, + // but for now subtitle is totally separate. + const subtitleHTML = + (html.isBlank(slots.subtitle) + ? null + : html.tag('h2', {class: 'page-subtitle'}, + language.sanitize(slots.subtitle))); + let footerContent = slots.footerContent; if (html.isBlank(footerContent) && relations.defaultFooterContent) { @@ -275,12 +341,19 @@ export default { html.tag('main', {id: 'content'}, {class: slots.mainClasses}, + !html.isBlank(subtitleHTML) && + {class: 'has-subtitle'}, + [ titleHTML, - html.tag('div', {id: 'cover-art-container'}, + html.tag('div', {id: 'artwork-column'}, {[html.onlyIfContent]: true}, - slots.cover), + {class: 'isolate-tooltip-z-indexing'}, + + slots.artworkColumnContent), + + subtitleHTML, slots.additionalNames, @@ -321,34 +394,32 @@ export default { slots.navLinks ?.filter(Boolean) - ?.map((cur, i) => { + ?.map((cur, i, entries) => { let content; if (cur.html) { content = cur.html; } else { + const attributes = html.attributes(); let title; - let href; switch (cur.auto) { case 'home': title = data.wikiName; - href = to('localized.home'); + attributes.set('href', to('localized.home')); break; case 'current': title = slots.title; - href = ''; + attributes.set('href', ''); break; case null: case undefined: title = cur.title; - href = to(...cur.path); + attributes.set('href', to(...cur.path)); break; } - content = html.tag('a', - {href}, - title); + content = html.tag('a', attributes, title); } const showAsCurrent = @@ -357,31 +428,51 @@ export default { (slots.navLinkStyle === 'hierarchical' && i === slots.navLinks.length - 1); - return ( + const navLink = html.tag('span', {class: 'nav-link'}, showAsCurrent && {class: 'current'}, [ html.tag('span', {class: 'nav-link-content'}, - // Use inline-block styling on the content span, - // rather than wrapping the whole nav-link in a proper - // blockwrap, so that if the content spans multiple - // lines, it'll kick the accent down beneath it. - i > 0 && - {class: 'blockwrap'}, - content), html.tag('span', {class: 'nav-link-accent'}, + {[html.noEdgeWhitespace]: true}, {[html.onlyIfContent]: true}, - cur.accent), - ])); + + language.$('misc.navAccent', { + [language.onlyIfOptions]: ['links'], + links: cur.accent, + })), + ]); + + if (slots.navLinkStyle === 'index') { + return navLink; + } + + const prev = + atOffset(entries, i, -1); + + if ( + prev && + prev.releaseRestToWrapTogether !== true && + (prev.releaseRestToWrapTogether === false || + prev.auto === 'home') + ) { + return navLink; + } else { + return html.metatag('blockwrap', navLink); + } })), html.tag('div', {class: 'nav-bottom-row'}, {[html.onlyIfContent]: true}, - slots.navBottomRowContent), + + language.$('misc.navAccent', { + [language.onlyIfOptions]: ['links'], + links: slots.navBottomRowContent, + })), html.tag('div', {class: 'nav-content'}, {[html.onlyIfContent]: true}, @@ -413,14 +504,15 @@ export default { let showingSidebarLeft; let showingSidebarRight; + let sidebarsInContentColumn = false; 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; + sidebarsInContentColumn = true; + showingSidebarLeft = true; } leftSidebar.setSlot( @@ -497,53 +589,40 @@ export default { {id: 'additional-files', string: 'additionalFiles'}, {id: 'commentary', string: 'commentary'}, {id: 'artist-commentary', string: 'artistCommentary'}, + {id: 'crediting-sources', string: 'creditingSources'}, + {id: 'referencing-sources', string: 'referencingSources'}, ])), ]); - const imageOverlayHTML = html.tag('div', {id: 'image-overlay-container'}, - html.tag('div', {id: 'image-overlay-content-container'}, [ - html.tag('a', {id: 'image-overlay-image-container'}, [ - html.tag('img', {id: 'image-overlay-image'}), - html.tag('img', {id: 'image-overlay-image-thumb'}), - ]), - - 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')), - })), + const slottedStyleTags = + html.smush(slots.styleTags); - 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')), + const slottedWallpaperStyleTag = + slottedStyleTags.content + .find(tag => tag.attributes.has('class', 'wallpaper-style')); - 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'}), - })), + const fallbackWallpaperStyleTag = + (slottedWallpaperStyleTag + ? html.blank() + : relations.wikiWallpaperStyleTag); - html.tag('span', {id: 'image-overlay-file-size-megabytes'}, - language.$('count.fileSize.megabytes', { - megabytes: - html.tag('span', {class: 'image-overlay-file-size-count'}), - })), - ]), - }), + const usingWallpaperStyleTag = + (slottedWallpaperStyleTag + ? slottedWallpaperStyleTag + : html.resolve(fallbackWallpaperStyleTag, {normalize: 'tag'})); - html.tag('span', {id: 'image-overlay-file-size-warning'}, - language.$(capsule, 'sizeWarning')), - ]), - ])), - ])); + const numWallpaperParts = + (usingWallpaperStyleTag && + usingWallpaperStyleTag.attributes.has('data-wallpaper-mode', 'parts') + ? parseInt(usingWallpaperStyleTag.attributes.get('data-num-wallpaper-parts')) + : 0); + + const wallpaperPartsHTML = + html.tag('div', {class: 'wallpaper-parts'}, + {[html.onlyIfContent]: true}, + + repeat(numWallpaperParts, () => + html.tag('div', {class: 'wallpaper-part'}))); const layoutHTML = [ navHTML, @@ -589,14 +668,30 @@ export default { html.tag('head', [ html.tag('title', - (slots.showWikiNameInTitle - ? language.formatString('misc.pageTitle.withWikiName', { - title: slots.title, - wikiName: data.wikiName, - }) - : language.formatString('misc.pageTitle', { - title: slots.title, - }))), + language.encapsulate('misc.pageTitle', workingCapsule => { + const workingOptions = {}; + + workingOptions.title = slots.title; + + if (!html.isBlank(slots.subtitle)) { + workingCapsule += '.withSubtitle'; + workingOptions.subtitle = slots.subtitle; + } + + const showWikiName = + (slots.showWikiNameInTitle === true + ? true + : slots.showWikiNameInTitle === 'auto' + ? html.isBlank(slots.subtitle) + : false); + + if (showWikiName) { + workingCapsule += '.withWikiName'; + workingOptions.wikiName = data.wikiName; + } + + return language.$(workingCapsule, workingOptions); + })), html.tag('meta', {charset: 'utf-8'}), html.tag('meta', { @@ -628,13 +723,15 @@ export default { Object.entries(meta) .filter(([key, value]) => value) .map(([key, value]) => html.tag('meta', {[key]: value}))), + */ - canonical && + canonicalHref && html.tag('link', { rel: 'canonical', - href: canonical, + href: canonicalHref, }), + /* ...( localizedCanonical .map(({lang, href}) => html.tag('link', { @@ -642,7 +739,6 @@ export default { hreflang: lang, href, }))), - */ hasSocialEmbed && @@ -650,16 +746,25 @@ export default { .clone() .slot('mode', 'html'), + oEmbedJSONHref && + html.tag('link', { + type: 'application/json+oembed', + href: oEmbedJSONHref, + }), + html.tag('link', { rel: 'stylesheet', href: to('staticCSS.path', 'site.css'), }), - html.tag('style', [ - relations.colorStyleRules - .slot('color', slots.color ?? data.wikiColor), - slots.styleRules, - ]), + relations.colorStyleTag + .slot('color', slots.color ?? data.wikiColor), + + relations.staticURLStyleTag, + + fallbackWallpaperStyleTag, + + slottedStyleTags, html.tag('script', { src: to('staticLib.path', 'chroma-js/chroma.min.js'), @@ -673,12 +778,14 @@ export default { html.tag('script', { blocking: 'render', type: 'module', - src: to('staticJS.path', 'client.js'), + src: to('staticJS.path', 'client/index.js'), }), ]), html.tag('body', [ + wallpaperPartsHTML, + html.tag('div', {id: 'page-container'}, showingSidebarLeft && {class: 'showing-sidebar-left'}, @@ -686,13 +793,16 @@ export default { showingSidebarRight && {class: 'showing-sidebar-right'}, + sidebarsInContentColumn && + {class: 'sidebars-in-content-column'}, + [ skippersHTML, layoutHTML, ]), // infoCardHTML, - imageOverlayHTML, + relations.imageOverlay, ]), ]) ]).toString(); diff --git a/src/content/dependencies/generatePageSidebarConjoinedBox.js b/src/content/dependencies/generatePageSidebarConjoinedBox.js index 05b1d469..7974c707 100644 --- a/src/content/dependencies/generatePageSidebarConjoinedBox.js +++ b/src/content/dependencies/generatePageSidebarConjoinedBox.js @@ -32,11 +32,7 @@ export default { .map((content, index, {length}) => [ content, index < length - 1 && - html.tag('hr', { - style: - `border-color: var(--primary-color); ` + - `border-style: none none dotted none`, - }), + html.tag('hr', {class: 'cute'}), ]), }), }; diff --git a/src/content/dependencies/generatePreviousLink.js b/src/content/dependencies/generatePreviousLink.js new file mode 100644 index 00000000..775367f9 --- /dev/null +++ b/src/content/dependencies/generatePreviousLink.js @@ -0,0 +1,13 @@ +export default { + contentDependencies: ['generatePreviousNextLink'], + + relations: (relation) => ({ + link: + relation('generatePreviousNextLink'), + }), + + generate: (relations) => + relations.link.slots({ + direction: 'previous', + }), +}; diff --git a/src/content/dependencies/generatePreviousNextLink.js b/src/content/dependencies/generatePreviousNextLink.js new file mode 100644 index 00000000..afae1228 --- /dev/null +++ b/src/content/dependencies/generatePreviousNextLink.js @@ -0,0 +1,58 @@ +export default { + extraDependencies: ['html', 'language'], + + slots: { + link: { + type: 'html', + mutable: true, + }, + + direction: { + validate: v => v.is('previous', 'next'), + }, + + id: { + type: 'boolean', + default: true, + }, + + showWithoutLink: { + type: 'boolean', + default: true, + }, + }, + + generate(slots, {html, language}) { + if (!slots.direction) { + return html.blank(); + } + + const attributes = html.attributes(); + + if (slots.id) { + attributes.set('id', `${slots.direction}-button`); + } + + if (html.isBlank(slots.link)) { + if (slots.showWithoutLink) { + return ( + html.tag('a', {class: 'inert-previous-next-link'}, + attributes, + language.$('misc.nav', slots.direction))); + } else { + return html.blank(); + } + } + + return html.resolve(slots.link, { + slots: { + tooltipStyle: 'browser', + color: false, + attributes, + + content: + language.$('misc.nav', slots.direction), + } + }); + }, +}; diff --git a/src/content/dependencies/generatePreviousNextLinks.js b/src/content/dependencies/generatePreviousNextLinks.js deleted file mode 100644 index 9771de39..00000000 --- a/src/content/dependencies/generatePreviousNextLinks.js +++ /dev/null @@ -1,50 +0,0 @@ -export default { - // Returns an array with the slotted previous and next links, prepared - // for inclusion in a page's navigation bar. Include with other links - // in the nav bar and then join them all as a unit list, for example. - - extraDependencies: ['html', 'language'], - - slots: { - previousLink: { - type: 'html', - mutable: true, - }, - - nextLink: { - type: 'html', - mutable: true, - }, - - id: { - type: 'boolean', - default: true, - }, - }, - - generate(slots, {html, language}) { - const previousNext = []; - - if (!html.isBlank(slots.previousLink)) { - previousNext.push( - slots.previousLink.slots({ - tooltipStyle: 'browser', - color: false, - attributes: {id: slots.id && 'previous-button'}, - content: language.$('misc.nav.previous'), - })); - } - - if (!html.isBlank(slots.nextLink)) { - previousNext.push( - slots.nextLink.slots({ - tooltipStyle: 'browser', - color: false, - attributes: {id: slots.id && 'next-button'}, - content: language.$('misc.nav.next'), - })); - } - - return previousNext; - }, -}; diff --git a/src/content/dependencies/generateQuickDescription.js b/src/content/dependencies/generateQuickDescription.js index 4c7c944a..e144503e 100644 --- a/src/content/dependencies/generateQuickDescription.js +++ b/src/content/dependencies/generateQuickDescription.js @@ -100,7 +100,7 @@ export default { children); const wrapContent = (attributes, content) => - html.tag('div', {class: 'description-content'}, + html.tag('blockquote', {class: 'description-content'}, {[html.onlyIfContent]: true}, attributes, diff --git a/src/content/dependencies/generateReferencedArtworksPage.js b/src/content/dependencies/generateReferencedArtworksPage.js new file mode 100644 index 00000000..83451eca --- /dev/null +++ b/src/content/dependencies/generateReferencedArtworksPage.js @@ -0,0 +1,100 @@ +export default { + contentDependencies: [ + 'generateCoverArtwork', + 'generateCoverGrid', + 'generatePageLayout', + 'image', + 'linkAnythingMan', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, artwork) => ({ + layout: + relation('generatePageLayout'), + + cover: + relation('generateCoverArtwork', artwork), + + coverGrid: + relation('generateCoverGrid'), + + links: + artwork.referencedArtworks.map(({artwork}) => + relation('linkAnythingMan', artwork.thing)), + + images: + artwork.referencedArtworks.map(({artwork}) => + relation('image', artwork)), + }), + + data: (artwork) => ({ + color: + artwork.thing.color, + + count: + artwork.referencedArtworks.length, + + names: + artwork.referencedArtworks + .map(({artwork}) => artwork.thing.name), + + coverArtistNames: + artwork.referencedArtworks + .map(({artwork}) => + artwork.artistContribs + .map(contrib => contrib.artist.name)), + }), + + slots: { + styleTags: {type: 'html', mutable: false}, + + title: {type: 'html', mutable: false}, + + navLinks: {validate: v => v.isArray}, + navBottomRowContent: {type: 'html', mutable: false}, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('referencedArtworksPage', pageCapsule => + relations.layout.slots({ + title: slots.title, + subtitle: language.$(pageCapsule, 'subtitle'), + + color: data.color, + styleTags: slots.styleTags, + + artworkColumnContent: + relations.cover.slots({ + showArtistDetails: true, + }), + + mainClasses: ['top-index'], + mainContent: [ + html.tag('p', {class: 'quick-info'}, + language.$(pageCapsule, 'statsLine', { + artworks: + language.countArtworks(data.count, { + unit: true, + }), + })), + + relations.coverGrid.slots({ + links: relations.links, + images: relations.images, + names: data.names, + + info: + data.coverArtistNames.map(names => + language.$('misc.coverGrid.details.coverArtists', { + artists: + language.formatUnitList(names), + })), + }), + ], + + navLinkStyle: 'hierarchical', + navLinks: slots.navLinks, + navBottomRowContent: slots.navBottomRowContent, + })), +}; diff --git a/src/content/dependencies/generateReferencingArtworksPage.js b/src/content/dependencies/generateReferencingArtworksPage.js new file mode 100644 index 00000000..e97b01f8 --- /dev/null +++ b/src/content/dependencies/generateReferencingArtworksPage.js @@ -0,0 +1,100 @@ +export default { + contentDependencies: [ + 'generateCoverArtwork', + 'generateCoverGrid', + 'generatePageLayout', + 'image', + 'linkAnythingMan', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, artwork) => ({ + layout: + relation('generatePageLayout'), + + cover: + relation('generateCoverArtwork', artwork), + + coverGrid: + relation('generateCoverGrid'), + + links: + artwork.referencedByArtworks.map(({artwork}) => + relation('linkAnythingMan', artwork.thing)), + + images: + artwork.referencedByArtworks.map(({artwork}) => + relation('image', artwork)), + }), + + data: (artwork) => ({ + color: + artwork.thing.color, + + count: + artwork.referencedByArtworks.length, + + names: + artwork.referencedByArtworks + .map(({artwork}) => artwork.thing.name), + + coverArtistNames: + artwork.referencedByArtworks + .map(({artwork}) => + artwork.artistContribs + .map(contrib => contrib.artist.name)), + }), + + slots: { + styleTags: {type: 'html', mutable: false}, + + title: {type: 'html', mutable: false}, + + navLinks: {validate: v => v.isArray}, + navBottomRowContent: {type: 'html', mutable: false}, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('referencingArtworksPage', pageCapsule => + relations.layout.slots({ + title: slots.title, + subtitle: language.$(pageCapsule, 'subtitle'), + + color: data.color, + styleTags: slots.styleTags, + + artworkColumnContent: + relations.cover.slots({ + showArtistDetails: true, + }), + + mainClasses: ['top-index'], + mainContent: [ + html.tag('p', {class: 'quick-info'}, + language.$(pageCapsule, 'statsLine', { + artworks: + language.countArtworks(data.count, { + unit: true, + }), + })), + + relations.coverGrid.slots({ + links: relations.links, + images: relations.images, + names: data.names, + + info: + data.coverArtistNames.map(names => + language.$('misc.coverGrid.details.coverArtists', { + artists: + language.formatUnitList(names), + })), + }), + ], + + navLinkStyle: 'hierarchical', + navLinks: slots.navLinks, + navBottomRowContent: slots.navBottomRowContent, + })), +}; diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js index 3e96ed44..016e0a2c 100644 --- a/src/content/dependencies/generateReleaseInfoContributionsLine.js +++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js @@ -1,45 +1,31 @@ -import {empty} from '#sugar'; - export default { - contentDependencies: ['linkContribution'], - extraDependencies: ['html', 'language'], - - relations(relation, contributions) { - if (empty(contributions)) { - return {}; - } + contentDependencies: ['generateArtistCredit'], + extraDependencies: ['html'], - return { - contributionLinks: - contributions - .map(contrib => relation('linkContribution', contrib)), - }; - }, + relations: (relation, contributions) => ({ + credit: + relation('generateArtistCredit', contributions, []), + }), slots: { - showContribution: {type: 'boolean', default: true}, - showExternalLinks: {type: 'boolean', default: true}, - showChronology: {type: 'boolean', default: true}, - stringKey: {type: 'string'}, + featuringStringKey: {type: 'string'}, + chronologyKind: {type: 'string'}, }, - generate(relations, slots, {html, language}) { - if (!relations.contributionLinks) { - return html.blank(); - } + generate: (relations, slots) => + relations.credit.slots({ + showAnnotation: true, + showExternalLinks: true, + showChronology: true, + showWikiEdits: true, - return language.$(slots.stringKey, { - artists: - language.formatConjunctionList( - relations.contributionLinks.map(link => - link.slots({ - showContribution: slots.showContribution, - showExternalLinks: slots.showExternalLinks, - showChronology: slots.showChronology, - chronologyKind: slots.chronologyKind, - }))), - }); - }, + trimAnnotation: false, + + chronologyKind: slots.chronologyKind, + + normalStringKey: slots.stringKey, + normalFeaturingStringKey: slots.featuringStringKey, + }), }; diff --git a/src/content/dependencies/generateReleaseInfoListenLine.js b/src/content/dependencies/generateReleaseInfoListenLine.js new file mode 100644 index 00000000..f2a6dd29 --- /dev/null +++ b/src/content/dependencies/generateReleaseInfoListenLine.js @@ -0,0 +1,150 @@ +import {isExternalLinkContext} from '#external-links'; +import {empty, stitchArrays, unique} from '#sugar'; + +function getReleaseContext(urlString, { + _artistURLs, + albumArtistURLs, +}) { + const composerBandcampDomains = + albumArtistURLs + .filter(url => url.hostname.endsWith('.bandcamp.com')) + .map(url => url.hostname); + + const url = new URL(urlString); + + if (url.hostname === 'homestuck.bandcamp.com') { + return 'officialRelease'; + } + + if (composerBandcampDomains.includes(url.hostname)) { + return 'composerRelease'; + } + + return null; +} + +export default { + contentDependencies: ['linkExternal'], + extraDependencies: ['html', 'language'], + + query(thing) { + const query = {}; + + query.album = + (thing.album + ? thing.album + : thing); + + query.artists = + thing.artistContribs + .map(contrib => contrib.artist); + + query.artistGroups = + query.artists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + query.albumArtists = + query.album.artistContribs + .map(contrib => contrib.artist); + + query.albumArtistGroups = + query.albumArtists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + return query; + }, + + relations: (relation, _query, thing) => ({ + links: + thing.urls.map(url => relation('linkExternal', url)), + }), + + data(query, thing) { + const data = {}; + + data.name = thing.name; + + const artistURLs = + unique([ + ...query.artists.flatMap(artist => artist.urls), + ...query.artistGroups.flatMap(group => group.urls), + ]).map(url => new URL(url)); + + const albumArtistURLs = + unique([ + ...query.albumArtists.flatMap(artist => artist.urls), + ...query.albumArtistGroups.flatMap(group => group.urls), + ]).map(url => new URL(url)); + + const boundGetReleaseContext = urlString => + getReleaseContext(urlString, { + artistURLs, + albumArtistURLs, + }); + + let releaseContexts = + thing.urls.map(boundGetReleaseContext); + + const albumReleaseContexts = + query.album.urls.map(boundGetReleaseContext); + + const presentReleaseContexts = + unique(releaseContexts.filter(Boolean)); + + const presentAlbumReleaseContexts = + unique(albumReleaseContexts.filter(Boolean)); + + if ( + presentReleaseContexts.length <= 1 && + presentAlbumReleaseContexts.length <= 1 + ) { + releaseContexts = + thing.urls.map(() => null); + } + + data.releaseContexts = releaseContexts; + + return data; + }, + + slots: { + visibleWithoutLinks: { + type: 'boolean', + default: false, + }, + + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('releaseInfo.listenOn', capsule => + (empty(relations.links) && slots.visibleWithoutLinks + ? language.$(capsule, 'noLinks', { + name: + html.tag('i', data.name), + }) + + : language.$('releaseInfo.listenOn', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatDisjunctionList( + stitchArrays({ + link: relations.links, + releaseContext: data.releaseContexts, + }).map(({link, releaseContext}) => + link.slot('context', [ + ... + (Array.isArray(slots.context) + ? slots.context + : [slots.context]), + + releaseContext, + ]))), + }))), +}; diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js index 188a678f..308a1105 100644 --- a/src/content/dependencies/generateSearchSidebarBox.js +++ b/src/content/dependencies/generateSearchSidebarBox.js @@ -57,6 +57,26 @@ export default { html.tag('template', {class: 'wiki-search-tag-result-kind-string'}, language.$(capsule, 'artTag')), ]), + + language.encapsulate(capsule, 'resultFilter', capsule => [ + html.tag('template', {class: 'wiki-search-album-result-filter-string'}, + language.$(capsule, 'album')), + + html.tag('template', {class: 'wiki-search-artist-result-filter-string'}, + language.$(capsule, 'artist')), + + html.tag('template', {class: 'wiki-search-flash-result-filter-string'}, + language.$(capsule, 'flash')), + + html.tag('template', {class: 'wiki-search-group-result-filter-string'}, + language.$(capsule, 'group')), + + html.tag('template', {class: 'wiki-search-track-result-filter-string'}, + language.$(capsule, 'track')), + + html.tag('template', {class: 'wiki-search-tag-result-filter-string'}, + language.$(capsule, 'artTag')), + ]), ], })), }; diff --git a/src/content/dependencies/generateSecondaryNav.js b/src/content/dependencies/generateSecondaryNav.js index e9aef66e..9ce7ce9b 100644 --- a/src/content/dependencies/generateSecondaryNav.js +++ b/src/content/dependencies/generateSecondaryNav.js @@ -7,14 +7,24 @@ export default { mutable: false, }, - class: { - validate: v => v.anyOf(v.isString, v.sparseArrayOf(v.isString)), + attributes: { + type: 'attributes', + mutable: false, + }, + + alwaysVisible: { + type: 'boolean', + default: false, }, }, generate: (slots, {html}) => html.tag('nav', {id: 'secondary-nav'}, {[html.onlyIfContent]: true}, - {class: slots.class}, + slots.attributes, + + slots.alwaysVisible && + {class: 'always-visible'}, + slots.content), }; diff --git a/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js b/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js new file mode 100644 index 00000000..f204f1fb --- /dev/null +++ b/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js @@ -0,0 +1,115 @@ +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generateInterpageDotSwitcher', + 'generateNextLink', + 'generatePreviousLink', + 'linkAlbumDynamically', + 'linkGroup', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation) => ({ + switcher: + relation('generateInterpageDotSwitcher'), + + previousLink: + relation('generatePreviousLink'), + + nextLink: + relation('generateNextLink'), + }), + + slots: { + showPreviousNext: { + type: 'boolean', + default: true, + }, + + id: { + type: 'boolean', + default: false, + }, + + attributes: { + type: 'attributes', + mutable: false, + }, + + colorStyle: { + type: 'html', + mutable: true, + }, + + mainLink: { + type: 'html', + mutable: true, + }, + + previousLink: { + type: 'html', + mutable: false, + }, + + nextLink: { + type: 'html', + mutable: false, + }, + + stringsKey: { + type: 'string', + }, + + mainLinkOption: { + type: 'string', + }, + }, + + generate: (relations, slots, {html, language}) => + html.tag('span', + {[html.onlyIfContent]: true}, + {[html.noEdgeWhitespace]: true}, + + slots.attributes, + + !html.isBlank(slots.colorStyle) && + slots.colorStyle + .slot('context', 'primary-only'), + + language.encapsulate(slots.stringsKey, workingCapsule => { + const workingOptions = { + [language.onlyIfOptions]: [slots.mainLinkOption], + }; + + workingOptions[slots.mainLinkOption] = + (html.isBlank(slots.mainLink) + ? null + : slots.mainLink + .slot('color', false)); + + if (slots.showPreviousNext) addPreviousNext: { + if (html.isBlank(slots.previousLink) && html.isBlank(slots.nextLink)) { + break addPreviousNext; + } + + workingCapsule += '.withPreviousNext'; + workingOptions.previousNext = + relations.switcher.slots({ + links: [ + relations.previousLink.slots({ + id: slots.id, + link: slots.previousLink, + }), + + relations.nextLink.slots({ + id: slots.id, + link: slots.nextLink, + }), + ], + }); + } + + return language.$(workingCapsule, workingOptions); + })), +}; diff --git a/src/content/dependencies/generateSocialEmbed.js b/src/content/dependencies/generateSocialEmbed.js index 0144c7fb..513ea518 100644 --- a/src/content/dependencies/generateSocialEmbed.js +++ b/src/content/dependencies/generateSocialEmbed.js @@ -1,5 +1,5 @@ export default { - extraDependencies: ['html', 'language', 'wikiData'], + extraDependencies: ['absoluteTo', 'html', 'language', 'wikiData'], sprawl({wikiInfo}) { return { @@ -23,10 +23,10 @@ export default { headingContent: {type: 'string'}, headingLink: {type: 'string'}, - imagePath: {type: 'string'}, + imagePath: {validate: v => v.strictArrayOf(v.isString)}, }, - generate(data, slots, {html, language}) { + generate(data, slots, {absoluteTo, html, language}) { switch (slots.mode) { case 'html': return html.tags([ @@ -40,17 +40,22 @@ export default { }), slots.imagePath && - html.tag('meta', {property: 'og:image', content: slots.imagePath}), + html.tag('meta', { + property: 'og:image', + content: absoluteTo(...slots.imagePath), + }), ]); case 'json': return JSON.stringify({ author_name: (slots.headingContent - ? language.$('misc.socialEmbed.heading', { - wikiName: data.shortWikiName, - heading: slots.headingContent, - }) + ? html.resolve( + language.$('misc.socialEmbed.heading', { + wikiName: data.shortWikiName, + heading: slots.headingContent, + }), + {normalize: 'string'}) : undefined), author_url: diff --git a/src/content/dependencies/generateStaticPage.js b/src/content/dependencies/generateStaticPage.js index 226152c7..931352b4 100644 --- a/src/content/dependencies/generateStaticPage.js +++ b/src/content/dependencies/generateStaticPage.js @@ -23,17 +23,19 @@ export default { title: data.name, headingMode: 'sticky', - styleRules: - (data.stylesheet - ? [data.stylesheet] - : []), + styleTags: [ + html.tag('style', {class: 'static-page-style'}, + {[html.onlyIfContent]: true}, + data.stylesheet), + ], mainClasses: ['long-content'], mainContent: [ relations.content, - data.script && - html.tag('script', data.script), + html.tag('script', + {[html.onlyIfContent]: true}, + data.script), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateStaticURLStyleTag.js b/src/content/dependencies/generateStaticURLStyleTag.js new file mode 100644 index 00000000..b927e5d6 --- /dev/null +++ b/src/content/dependencies/generateStaticURLStyleTag.js @@ -0,0 +1,23 @@ +export default { + contentDependencies: ['generateStyleTag'], + extraDependencies: ['to'], + + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + }), + + generate: (relations, {to}) => + relations.styleTag.slots({ + attributes: {class: 'static-url-style'}, + + rules: [ + { + select: '.image-media-link::after', + declare: [ + `mask-image: url("${to('staticMisc.path', 'image.svg')}");` + ], + }, + ], + }), +}; diff --git a/src/content/dependencies/generateStickyHeadingContainer.js b/src/content/dependencies/generateStickyHeadingContainer.js index 7f271715..ec3062a3 100644 --- a/src/content/dependencies/generateStickyHeadingContainer.js +++ b/src/content/dependencies/generateStickyHeadingContainer.js @@ -2,6 +2,11 @@ export default { extraDependencies: ['html'], slots: { + rootAttributes: { + type: 'attributes', + mutable: false, + }, + title: { type: 'html', mutable: false, @@ -13,29 +18,42 @@ export default { }, }, - generate: (slots, {html}) => - html.tag('div', {class: 'content-sticky-heading-container'}, + generate: (slots, {html}) => html.tags([ + html.tag('div', {class: 'content-sticky-heading-root'}, + slots.rootAttributes, + !html.isBlank(slots.cover) && {class: 'has-cover'}, - [ - html.tag('div', {class: 'content-sticky-heading-row'}, [ - html.tag('h1', slots.title), + html.tag('div', {class: 'content-sticky-heading-anchor'}, + html.tag('div', {class: 'content-sticky-heading-container'}, + !html.isBlank(slots.cover) && + {class: 'has-cover'}, + + [ + html.tag('div', {class: 'content-sticky-heading-row'}, [ + html.tag('h1', [ + html.tag('span', {class: 'reference-collapsed-heading'}, + {inert: true}, + + slots.title.clone()), + + slots.title, + ]), - html.tag('div', {class: 'content-sticky-heading-cover-container'}, - {[html.onlyIfContent]: true}, + html.tag('div', {class: 'content-sticky-heading-cover-container'}, + {[html.onlyIfContent]: true}, - html.tag('div', {class: 'content-sticky-heading-cover'}, - {[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.isBlank(slots.cover) + ? html.blank() + : slots.cover.slot('mode', 'thumbnail')))), + ]), - html.tag('div', {class: 'content-sticky-subheading-row'}, - html.tag('h2', {class: 'content-sticky-subheading'})), - ]), + html.tag('div', {class: 'content-sticky-subheading-row'}, + html.tag('h2', {class: 'content-sticky-subheading'})), + ]))), + ]), }; diff --git a/src/content/dependencies/generateStyleTag.js b/src/content/dependencies/generateStyleTag.js new file mode 100644 index 00000000..5ed09ae5 --- /dev/null +++ b/src/content/dependencies/generateStyleTag.js @@ -0,0 +1,48 @@ +import {empty} from '#sugar'; + +const indent = text => + text + .split('\n') + .map(line => ' '.repeat(4) + line) + .join('\n'); + +export default { + extraDependencies: ['html'], + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + rules: { + validate: v => + v.looseArrayOf( + v.validateProperties({ + select: v.isString, + declare: v.looseArrayOf(v.isString), + })), + }, + }, + + generate: (slots, {html}) => + html.tag('style', slots.attributes, + {[html.onlyIfContent]: true}, + + slots.rules + .filter(Boolean) + + .map(rule => ({ + select: rule.select, + declare: rule.declare.filter(Boolean), + })) + + .filter(rule => !empty(rule.declare)) + + .map(rule => + `${rule.select} {\n` + + indent(rule.declare.join('\n')) + '\n' + + `}`) + + .join('\n\n')), +}; diff --git a/src/content/dependencies/generateTextWithTooltip.js b/src/content/dependencies/generateTextWithTooltip.js index 462557d1..49ce1f61 100644 --- a/src/content/dependencies/generateTextWithTooltip.js +++ b/src/content/dependencies/generateTextWithTooltip.js @@ -36,6 +36,7 @@ export default { if (hasTooltip) { attributes = attributes.clone(); attributes.add({ + [html.onlyIfContent]: true, [html.joinChildren]: '', [html.noEdgeWhitespace]: true, class: 'text-with-tooltip', @@ -45,11 +46,19 @@ export default { const textPart = (hasTooltip && slots.customInteractionCue ? html.tag('span', {class: 'hoverable'}, + {[html.onlyIfContent]: true}, + slots.text) + : hasTooltip ? html.tag('span', {class: 'hoverable'}, + {[html.onlyIfContent]: true}, + html.tag('span', {class: 'text-with-tooltip-interaction-cue'}, + {[html.onlyIfContent]: true}, + slots.text)) + : slots.text); const content = diff --git a/src/content/dependencies/generateTooltip.js b/src/content/dependencies/generateTooltip.js index 8314d33c..b09ee230 100644 --- a/src/content/dependencies/generateTooltip.js +++ b/src/content/dependencies/generateTooltip.js @@ -22,6 +22,7 @@ export default { html.tag('span', {class: 'tooltip'}, {[html.noEdgeWhitespace]: true}, {[html.onlyIfContent]: true}, + {[html.onlyIfSiblings]: true}, slots.attributes, html.tag('span', {class: 'tooltip-content'}, diff --git a/src/content/dependencies/generateTrackAdditionalNamesBox.js b/src/content/dependencies/generateTrackAdditionalNamesBox.js deleted file mode 100644 index bad04b74..00000000 --- a/src/content/dependencies/generateTrackAdditionalNamesBox.js +++ /dev/null @@ -1,53 +0,0 @@ -import {empty} from '#sugar'; - -export default { - contentDependencies: ['generateAdditionalNamesBox'], - extraDependencies: ['html'], - - query: (track) => { - const { - additionalNames: own, - sharedAdditionalNames: shared, - inferredAdditionalNames: inferred, - } = track; - - if (empty(own) && empty(shared) && empty(inferred)) { - return {combinedList: []}; - } - - const firstFilter = - (empty(own) - ? new Set() - : new Set(own.map(({name}) => name))); - - const sharedFiltered = - shared.filter(({name}) => !firstFilter.has(name)) - - const secondFilter = - new Set([ - ...firstFilter, - ...sharedFiltered.map(({name}) => name), - ]); - - const inferredFiltered = - inferred.filter(({name}) => !secondFilter.has(name)); - - return { - combinedList: [ - ...own, - ...sharedFiltered, - ...inferredFiltered, - ], - }; - }, - - relations: (relation, query) => ({ - box: - (empty(query.combinedList) - ? null - : relation('generateAdditionalNamesBox', query.combinedList)), - }), - - generate: (relations, {html}) => - relations.box ?? html.blank(), -}; diff --git a/src/content/dependencies/generateTrackArtistCommentarySection.js b/src/content/dependencies/generateTrackArtistCommentarySection.js new file mode 100644 index 00000000..c7e7f0f8 --- /dev/null +++ b/src/content/dependencies/generateTrackArtistCommentarySection.js @@ -0,0 +1,147 @@ +import {empty, stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateContentContentHeading', + 'generateCommentaryEntry', + 'linkAlbum', + 'linkTrack', + ], + + extraDependencies: ['html', 'language'], + + query: (track) => ({ + otherSecondaryReleasesWithCommentary: + track.otherReleases + .filter(track => !track.isMainRelease) + .filter(track => !empty(track.commentary)), + }), + + relations: (relation, query, track) => ({ + contentContentHeading: + relation('generateContentContentHeading', track), + + mainReleaseTrackLink: + (track.isSecondaryRelease + ? relation('linkTrack', track.mainReleaseTrack) + : null), + + mainReleaseArtistCommentaryEntries: + (track.isSecondaryRelease + ? track.mainReleaseTrack.commentary + .map(entry => relation('generateCommentaryEntry', entry)) + : null), + + thisReleaseAlbumLink: + relation('linkAlbum', track.album), + + artistCommentaryEntries: + track.commentary + .map(entry => relation('generateCommentaryEntry', entry)), + + otherReleaseTrackLinks: + query.otherSecondaryReleasesWithCommentary + .map(track => relation('linkTrack', track)), + }), + + data: (query, track) => ({ + name: + track.name, + + isSecondaryRelease: + track.isSecondaryRelease, + + mainReleaseName: + (track.isSecondaryRelease + ? track.mainReleaseTrack.name + : null), + + mainReleaseAlbumName: + (track.isSecondaryRelease + ? track.mainReleaseTrack.album.name + : null), + + mainReleaseAlbumColor: + (track.isSecondaryRelease + ? track.mainReleaseTrack.album.color + : null), + + otherReleaseAlbumNames: + query.otherSecondaryReleasesWithCommentary + .map(track => track.album.name), + + otherReleaseAlbumColors: + query.otherSecondaryReleasesWithCommentary + .map(track => track.album.color), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('misc.artistCommentary', capsule => + html.tags([ + relations.contentContentHeading.slots({ + attributes: {id: 'artist-commentary'}, + string: 'misc.artistCommentary', + }), + + relations.artistCommentaryEntries, + + data.isSecondaryRelease && + html.tag('div', {class: 'inherited-commentary-section'}, + {[html.onlyIfContent]: true}, + + [ + html.tag('p', {class: ['drop', 'commentary-drop']}, + {[html.onlyIfSiblings]: true}, + + language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => { + const workingOptions = {}; + + workingOptions.album = + relations.mainReleaseTrackLink.slots({ + content: + data.mainReleaseAlbumName, + + color: + data.mainReleaseAlbumColor, + }); + + if (data.name !== data.mainReleaseName) { + workingCapsule += '.namedDifferently'; + workingOptions.name = + html.tag('i', data.mainReleaseName); + } + + return language.$(workingCapsule, workingOptions); + })), + + relations.mainReleaseArtistCommentaryEntries, + ]), + + html.tag('p', {class: ['drop', 'commentary-drop']}, + {[html.onlyIfContent]: true}, + + language.encapsulate(capsule, 'info.seeSpecificReleases', workingCapsule => { + const workingOptions = {}; + + workingOptions[language.onlyIfOptions] = ['albums']; + + workingOptions.albums = + language.formatUnitList( + stitchArrays({ + trackLink: relations.otherReleaseTrackLinks, + albumName: data.otherReleaseAlbumNames, + albumColor: data.otherReleaseAlbumColors, + }).map(({trackLink, albumName, albumColor}) => + trackLink.slots({ + content: language.sanitize(albumName), + color: albumColor, + }))); + + if (!html.isBlank(relations.artistCommentaryEntries)) { + workingCapsule += '.withMainCommentary'; + } + + return language.$(workingCapsule, workingOptions); + })), + ])), +}; diff --git a/src/content/dependencies/generateTrackArtworkColumn.js b/src/content/dependencies/generateTrackArtworkColumn.js new file mode 100644 index 00000000..f06d735b --- /dev/null +++ b/src/content/dependencies/generateTrackArtworkColumn.js @@ -0,0 +1,33 @@ +export default { + contentDependencies: ['generateCoverArtwork'], + extraDependencies: ['html'], + + relations: (relation, track) => ({ + albumCover: + (!track.hasUniqueCoverArt && track.album.hasCoverArt + ? relation('generateCoverArtwork', track.album.coverArtworks[0]) + : null), + + trackCovers: + (track.hasUniqueCoverArt + ? track.trackArtworks.map(artwork => + relation('generateCoverArtwork', artwork)) + : []), + }), + + generate: (relations, {html}) => + html.tags([ + relations.albumCover?.slots({ + showOriginDetails: true, + showArtTagDetails: true, + showReferenceDetails: true, + }), + + relations.trackCovers.map(cover => + cover.slots({ + showOriginDetails: true, + showArtTagDetails: true, + showReferenceDetails: true, + })), + ]), +}; diff --git a/src/content/dependencies/generateTrackCoverArtwork.js b/src/content/dependencies/generateTrackCoverArtwork.js deleted file mode 100644 index a241eaf2..00000000 --- a/src/content/dependencies/generateTrackCoverArtwork.js +++ /dev/null @@ -1,34 +0,0 @@ -export default { - contentDependencies: ['generateCoverArtwork'], - - relations: (relation, track) => ({ - coverArtwork: - relation('generateCoverArtwork', - (track.hasUniqueCoverArt - ? track.artTags - : track.album.artTags)), - }), - - data: (track) => ({ - path: - (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : ['media.albumCover', track.album.directory, track.album.coverArtFileExtension]), - - color: - track.color, - - dimensions: - (track.hasUniqueCoverArt - ? track.coverArtDimensions - : track.album.coverArtDimensions), - }), - - generate: (data, relations) => - relations.coverArtwork.slots({ - path: data.path, - color: data.color, - dimensions: data.dimensions, - }), -}; - diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 64ed0cb4..6c16ce27 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -1,20 +1,24 @@ export default { contentDependencies: [ - 'generateAlbumAdditionalFilesList', + 'generateAdditionalFilesList', + 'generateAdditionalNamesBox', 'generateAlbumNavAccent', 'generateAlbumSecondaryNav', 'generateAlbumSidebar', - 'generateAlbumStyleRules', - 'generateCommentarySection', + 'generateAlbumStyleTags', + 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generateContributionList', + 'generateLyricsSection', 'generatePageLayout', - 'generateTrackAdditionalNamesBox', - 'generateTrackCoverArtwork', + 'generateTrackArtistCommentarySection', + 'generateTrackArtworkColumn', 'generateTrackInfoPageFeaturedByFlashesList', 'generateTrackInfoPageOtherReleasesList', 'generateTrackList', 'generateTrackListDividedByGroups', + 'generateTrackNavLinks', 'generateTrackReleaseInfo', 'generateTrackSocialEmbed', 'linkAlbum', @@ -22,28 +26,27 @@ export default { 'transformContent', ], - extraDependencies: ['html', 'language', 'wikiData'], + extraDependencies: ['html', 'language'], - sprawl: ({wikiInfo}) => ({ - divideTrackListsByGroups: - wikiInfo.divideTrackListsByGroups, + query: (track) => ({ + mainReleaseTrack: + (track.isMainRelease + ? track + : track.mainReleaseTrack), }), - relations: (relation, sprawl, track) => ({ + relations: (relation, query, track) => ({ layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), socialEmbed: relation('generateTrackSocialEmbed', track), - albumLink: - relation('linkAlbum', track.album), - - trackLink: - relation('linkTrack', track), + navLinks: + relation('generateTrackNavLinks', track), albumNavAccent: relation('generateAlbumNavAccent', track.album, track), @@ -55,21 +58,22 @@ export default { relation('generateAlbumSidebar', track.album, track), additionalNamesBox: - relation('generateTrackAdditionalNamesBox', track), + relation('generateAdditionalNamesBox', track.additionalNames), - cover: - (track.hasUniqueCoverArt || track.album.hasCoverArt - ? relation('generateTrackCoverArtwork', track) - : null), + artworkColumn: + relation('generateTrackArtworkColumn', track), contentHeading: relation('generateContentHeading'), + contentContentHeading: + relation('generateContentContentHeading', track), + releaseInfo: relation('generateTrackReleaseInfo', track), otherReleasesList: - relation('generateTrackInfoPageOtherReleasesList', track), + relation('generateTrackInfoPageOtherReleasesList', track), contributorContributionList: relation('generateContributionList', track.contributorContribs), @@ -82,51 +86,45 @@ export default { referencedByTracksList: relation('generateTrackListDividedByGroups', - track.referencedByTracks, - sprawl.divideTrackListsByGroups), + query.mainReleaseTrack.referencedByTracks), sampledByTracksList: relation('generateTrackListDividedByGroups', - track.sampledByTracks, - sprawl.divideTrackListsByGroups), + query.mainReleaseTrack.sampledByTracks), flashesThatFeatureList: relation('generateTrackInfoPageFeaturedByFlashesList', track), - lyrics: - relation('transformContent', track.lyrics), + lyricsSection: + relation('generateLyricsSection', track.lyrics), sheetMusicFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.sheetMusicFiles), + relation('generateAdditionalFilesList', track.sheetMusicFiles), midiProjectFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.midiProjectFiles), + relation('generateAdditionalFilesList', track.midiProjectFiles), additionalFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.additionalFiles), + relation('generateAdditionalFilesList', track.additionalFiles), artistCommentarySection: - relation('generateCommentarySection', track.commentary), + relation('generateTrackArtistCommentarySection', track), + + creditingSourceEntries: + track.creditingSources + .map(entry => relation('generateCommentaryEntry', entry)), + + referencingSourceEntries: + track.referencingSources + .map(entry => relation('generateCommentaryEntry', entry)), }), - data: (sprawl, track) => ({ + data: (_query, track) => ({ name: track.name, color: track.color, - - hasTrackNumbers: - track.album.hasTrackNumbers, - - trackNumber: - track.album.tracks.indexOf(track) + 1, }), generate: (data, relations, {html, language}) => @@ -142,14 +140,10 @@ export default { additionalNames: relations.additionalNamesBox, color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, - cover: - (relations.cover - ? relations.cover.slots({ - alt: language.$('misc.alt.trackCover'), - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ relations.releaseInfo, @@ -194,17 +188,27 @@ export default { {href: '#artist-commentary'}, language.$(capsule, 'link')), })), - ])), - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'also-released-as'}, - title: language.$('releaseInfo.alsoReleasedAs'), - }), + !html.isBlank(relations.creditingSourceEntries) && + language.encapsulate(capsule, 'readCreditingSources', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#crediting-sources'}, + language.$(capsule, 'link')), + })), - relations.otherReleasesList, - ]), + !html.isBlank(relations.referencingSourceEntries) && + language.encapsulate(capsule, 'readReferencingSources', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#referencing-sources'}, + language.$(capsule, 'link')), + })), + ])), + + relations.otherReleasesList, html.tags([ relations.contentHeading.clone() @@ -316,17 +320,7 @@ export default { relations.flashesThatFeatureList, ]), - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'lyrics'}, - title: language.$('releaseInfo.lyrics'), - }), - - html.tag('blockquote', - {[html.onlyIfContent]: true}, - relations.lyrics.slot('mode', 'lyrics')), - ]), + relations.lyricsSection, html.tags([ relations.contentHeading.clone() @@ -359,34 +353,31 @@ export default { ]), relations.artistCommentarySection, - ], - - navLinkStyle: 'hierarchical', - - navLinks: [ - {auto: 'home'}, - - {html: relations.albumLink.slot('color', false)}, - { - html: - language.encapsulate(pageCapsule, 'nav.track', workingCapsule => { - const workingOptions = {}; - - workingOptions.track = - relations.trackLink - .slot('attributes', {class: 'current'}); + html.tags([ + relations.contentContentHeading.clone() + .slots({ + attributes: {id: 'crediting-sources'}, + string: 'misc.creditingSources', + }), - if (data.hasTrackNumbers) { - workingCapsule += '.withNumber'; - workingOptions.number = data.trackNumber; - } + relations.creditingSourceEntries, + ]), - return language.$(workingCapsule, workingOptions); + html.tags([ + relations.contentContentHeading.clone() + .slots({ + attributes: {id: 'referencing-sources'}, + string: 'misc.referencingSources', }), - }, + + relations.referencingSourceEntries, + ]), ], + navLinkStyle: 'hierarchical', + navLinks: html.resolve(relations.navLinks), + navBottomRowContent: relations.albumNavAccent.slots({ showTrackNavigation: true, diff --git a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js index 5958be9a..61654512 100644 --- a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js +++ b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js @@ -14,7 +14,7 @@ export default { sortedFeatures: (sprawl.enableFlashesAndGames ? sortFlashesChronologically( - [track, ...track.otherReleases].flatMap(track => + track.allReleases.flatMap(track => track.featuredInFlashes.map(flash => ({ flash, track, @@ -36,6 +36,8 @@ export default { .map(({track: directlyFeaturedTrack}) => (directlyFeaturedTrack === track ? null + : directlyFeaturedTrack.name === track.name + ? null : relation('linkTrack', directlyFeaturedTrack))), }), @@ -52,7 +54,6 @@ export default { const options = {flash: flashLink}; if (trackLink) { - attributes.add('class', 'rerelease'); parts.push('asDifferentRelease'); options.track = trackLink; } diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js index 004bba6d..ebd76577 100644 --- a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js +++ b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js @@ -1,80 +1,42 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateColorStyleAttribute', - 'generateRelativeDatetimestamp', - 'linkAlbum', - 'linkTrack', - ], - + contentDependencies: ['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: + data: (track) => ({ + albumNames: 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)), + .map(track => track.album.name), - items: - track.otherReleases.map(track => ({ - trackLink: relation('linkTrack', track), - albumLink: relation('linkAlbum', track.album), - })), + albumColors: + track.otherReleases + .map(track => track.album.color), }), - generate: (relations, {html, language}) => - html.tag('ul', + generate: (data, relations, {html, language}) => + html.tag('p', {[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))); - })), + language.$('releaseInfo.alsoReleasedOn', { + [language.onlyIfOptions]: ['albums'], + + albums: + language.formatConjunctionList( + stitchArrays({ + trackLink: relations.trackLinks, + albumName: data.albumNames, + albumColor: data.albumColors, + }).map(({trackLink, albumName, albumColor}) => + trackLink.slots({ + content: language.sanitize(albumName), + color: albumColor, + }))), + })), }; diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js index 7c3b11c1..53a32536 100644 --- a/src/content/dependencies/generateTrackList.js +++ b/src/content/dependencies/generateTrackList.js @@ -1,46 +1,28 @@ -import {empty, stitchArrays} from '#sugar'; - export default { - contentDependencies: ['linkTrack', 'linkContribution'], - - extraDependencies: ['html', 'language'], + contentDependencies: ['generateTrackListItem'], + extraDependencies: ['html'], relations: (relation, tracks) => ({ - trackLinks: - tracks - .map(track => relation('linkTrack', track)), - - contributionLinks: + items: tracks - .map(track => - track.artistContribs - .map(contrib => relation('linkContribution', contrib))), + .map(track => relation('generateTrackListItem', track, [])), }), - generate: (relations, {html, language}) => + slots: { + colorMode: { + validate: v => v.is('none', 'track', 'line'), + default: 'track', + }, + }, + + generate: (relations, slots, {html}) => 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); - }))))), + relations.items.map(item => + item.slots({ + showArtists: true, + showDuration: false, + colorMode: slots.colorMode, + }))), }; diff --git a/src/content/dependencies/generateTrackListDividedByGroups.js b/src/content/dependencies/generateTrackListDividedByGroups.js index 3cba479e..230868d6 100644 --- a/src/content/dependencies/generateTrackListDividedByGroups.js +++ b/src/content/dependencies/generateTrackListDividedByGroups.js @@ -7,9 +7,16 @@ export default { 'linkGroup', ], - extraDependencies: ['html', 'language'], + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({wikiInfo}) => ({ + divideTrackListsByGroups: + wikiInfo.divideTrackListsByGroups, + }), + + query(sprawl, tracks) { + const dividingGroups = sprawl.divideTrackListsByGroups; - query(tracks, dividingGroups) { const groupings = new Map(); const ungroupedTracks = []; @@ -43,9 +50,9 @@ export default { return {groups, groupedTracks, ungroupedTracks}; }, - relations: (relation, query, tracks, groups) => ({ + relations: (relation, query, sprawl, tracks) => ({ flatList: - (empty(groups) + (empty(sprawl.divideTrackListsByGroups) ? relation('generateTrackList', tracks) : null), @@ -66,7 +73,7 @@ export default { : relation('generateTrackList', query.ungroupedTracks)), }), - data: (query) => ({ + data: (query, _sprawl, _tracks) => ({ groupNames: query.groups .map(group => group.name), diff --git a/src/content/dependencies/generateTrackListItem.js b/src/content/dependencies/generateTrackListItem.js new file mode 100644 index 00000000..3c850a18 --- /dev/null +++ b/src/content/dependencies/generateTrackListItem.js @@ -0,0 +1,107 @@ +export default { + contentDependencies: [ + 'generateArtistCredit', + 'generateColorStyleAttribute', + 'generateTrackListMissingDuration', + 'linkTrack', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, track, contextContributions) => ({ + trackLink: + relation('linkTrack', track), + + credit: + relation('generateArtistCredit', + track.artistContribs, + contextContributions), + + colorStyle: + relation('generateColorStyleAttribute', track.color), + + missingDuration: + (track.duration + ? null + : relation('generateTrackListMissingDuration')), + }), + + data: (track, _contextContributions) => ({ + duration: + track.duration ?? 0, + + trackHasDuration: + !!track.duration, + }), + + slots: { + // showArtists enables showing artists *at all.* It doesn't take precedence + // over behavior which automatically collapses (certain) artists because of + // provided context contributions. + showArtists: { + type: 'boolean', + default: true, + }, + + // If true and the track doesn't have a duration, a missing-duration cue + // will be displayed instead. + showDuration: { + type: 'boolean', + default: false, + }, + + colorMode: { + validate: v => v.is('none', 'track', 'line'), + default: 'track', + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('trackList.item', itemCapsule => + html.tag('li', + slots.colorMode === 'line' && + relations.colorStyle.slot('context', 'primary-only'), + + language.encapsulate(itemCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.track = + relations.trackLink + .slot('color', slots.colorMode === 'track'); + + if (slots.showDuration) { + workingCapsule += '.withDuration'; + workingOptions.duration = + (data.trackHasDuration + ? language.$(itemCapsule, 'withDuration.duration', { + duration: + language.formatDuration(data.duration), + }) + : relations.missingDuration); + } + + const artistCapsule = language.encapsulate(itemCapsule, 'withArtists'); + + relations.credit.setSlots({ + normalStringKey: + artistCapsule + '.by', + + featuringStringKey: + artistCapsule + '.featuring', + + normalFeaturingStringKey: + artistCapsule + '.by.featuring', + }); + + if (!html.isBlank(relations.credit)) { + workingCapsule += '.withArtists'; + workingOptions.by = + html.tag('span', {class: 'by'}, + // TODO: This is obviously evil. + html.metatag('chunkwrap', {split: /,| (?=and)/}, + html.resolve(relations.credit))); + } + + return language.$(workingCapsule, workingOptions); + }))), +}; diff --git a/src/content/dependencies/generateAlbumTrackListMissingDuration.js b/src/content/dependencies/generateTrackListMissingDuration.js index b5917982..b5917982 100644 --- a/src/content/dependencies/generateAlbumTrackListMissingDuration.js +++ b/src/content/dependencies/generateTrackListMissingDuration.js diff --git a/src/content/dependencies/generateTrackNavLinks.js b/src/content/dependencies/generateTrackNavLinks.js new file mode 100644 index 00000000..6a8b7c64 --- /dev/null +++ b/src/content/dependencies/generateTrackNavLinks.js @@ -0,0 +1,64 @@ +export default { + contentDependencies: ['linkAlbum', 'linkTrack'], + extraDependencies: ['html', 'language'], + + relations: (relation, track) => ({ + albumLink: + relation('linkAlbum', track.album), + + trackLink: + relation('linkTrack', track), + }), + + data: (track) => ({ + hasTrackNumbers: + track.album.hasTrackNumbers, + + trackNumber: + track.trackNumber, + }), + + slots: { + currentExtra: { + validate: v => v.is('referenced-art', 'referencing-art'), + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('trackPage.nav', navCapsule => [ + {auto: 'home'}, + + {html: relations.albumLink.slot('color', false)}, + + { + html: + language.encapsulate(navCapsule, '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); + }), + + accent: + html.tag('a', + {[html.onlyIfContent]: true}, + + {href: ''}, + {class: 'current'}, + + (slots.currentExtra === 'referenced-art' + ? language.$('referencedArtworksPage.subtitle') + : slots.currentExtra === 'referencing-art' + ? language.$('referencingArtworksPage.subtitle') + : null)), + }, + ]), +}; diff --git a/src/content/dependencies/generateTrackReferencedArtworksPage.js b/src/content/dependencies/generateTrackReferencedArtworksPage.js new file mode 100644 index 00000000..7073409e --- /dev/null +++ b/src/content/dependencies/generateTrackReferencedArtworksPage.js @@ -0,0 +1,47 @@ +export default { + contentDependencies: [ + 'generateAlbumStyleTags', + 'generateBackToTrackLink', + 'generateReferencedArtworksPage', + 'generateTrackNavLinks', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, track) => ({ + page: + relation('generateReferencedArtworksPage', track.trackArtworks[0]), + + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), + + navLinks: + relation('generateTrackNavLinks', track), + + backToTrackLink: + relation('generateBackToTrackLink', track), + }), + + data: (track) => ({ + name: + track.name, + }), + + generate: (data, relations, {html, language}) => + relations.page.slots({ + title: + language.$('trackPage.title', { + track: + data.name, + }), + + styleTags: relations.albumStyleTags, + + navLinks: + html.resolve( + relations.navLinks + .slot('currentExtra', 'referenced-art')), + + navBottomRowContent: relations.backToTrackLink, + }), +}; diff --git a/src/content/dependencies/generateTrackReferencingArtworksPage.js b/src/content/dependencies/generateTrackReferencingArtworksPage.js new file mode 100644 index 00000000..a45144c8 --- /dev/null +++ b/src/content/dependencies/generateTrackReferencingArtworksPage.js @@ -0,0 +1,47 @@ +export default { + contentDependencies: [ + 'generateAlbumStyleTags', + 'generateBackToTrackLink', + 'generateReferencingArtworksPage', + 'generateTrackNavLinks', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, track) => ({ + page: + relation('generateReferencingArtworksPage', track.trackArtworks[0]), + + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), + + navLinks: + relation('generateTrackNavLinks', track), + + backToTrackLink: + relation('generateBackToTrackLink', track), + }), + + data: (track) => ({ + name: + track.name, + }), + + generate: (data, relations, {html, language}) => + relations.page.slots({ + title: + language.$('trackPage.title', { + track: + data.name, + }), + + styleTags: relations.albumStyleTags, + + navLinks: + html.resolve( + relations.navLinks + .slot('currentExtra', 'referencing-art')), + + navBottomRowContent: relations.backToTrackLink, + }), +}; diff --git a/src/content/dependencies/generateTrackReleaseBox.js b/src/content/dependencies/generateTrackReleaseBox.js new file mode 100644 index 00000000..ef02e2b9 --- /dev/null +++ b/src/content/dependencies/generateTrackReleaseBox.js @@ -0,0 +1,46 @@ +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generatePageSidebarBox', + 'linkTrack', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, track) => ({ + box: + relation('generatePageSidebarBox'), + + colorStyle: + relation('generateColorStyleAttribute', track.album.color), + + trackLink: + relation('linkTrack', track), + }), + + data: (track) => ({ + albumName: + track.album.name, + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('albumSidebar.releaseBox', boxCapsule => + relations.box.slots({ + attributes: [ + {class: 'track-release-sidebar-box'}, + relations.colorStyle, + ], + + content: [ + html.tag('h1', + language.$(boxCapsule, 'title', { + album: + relations.trackLink.slots({ + color: false, + content: + language.sanitize(data.albumName), + }), + })), + ], + })), +}; diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js index 8a081046..3298dcc4 100644 --- a/src/content/dependencies/generateTrackReleaseInfo.js +++ b/src/content/dependencies/generateTrackReleaseInfo.js @@ -1,9 +1,7 @@ -import {empty} from '#sugar'; - export default { contentDependencies: [ 'generateReleaseInfoContributionsLine', - 'linkExternal', + 'generateReleaseInfoListenLine', ], extraDependencies: ['html', 'language'], @@ -11,19 +9,11 @@ export default { relations(relation, track) { const relations = {}; - relations.artistContributionLinks = + relations.artistContributionsLine = relation('generateReleaseInfoContributionsLine', track.artistContribs); - if (track.hasUniqueCoverArt) { - relations.coverArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', track.coverArtistContribs); - } - - if (!empty(track.urls)) { - relations.externalLinks = - track.urls.map(url => - relation('linkExternal', url)); - } + relations.listenLine = + relation('generateReleaseInfoListenLine', track); return relations; }, @@ -37,7 +27,6 @@ export default { if ( track.hasUniqueCoverArt && - track.coverArtDate && +track.coverArtDate !== +track.date ) { data.coverArtDate = track.coverArtDate; @@ -54,26 +43,17 @@ export default { {[html.joinChildren]: html.tag('br')}, [ - relations.artistContributionLinks.slots({ + relations.artistContributionsLine.slots({ stringKey: capsule + '.by', + featuringStringKey: capsule + '.by.featuring', chronologyKind: 'track', }), - relations.coverArtistContributionsLine?.slots({ - stringKey: capsule + '.coverArtBy', - chronologyKind: 'trackArt', - }), - language.$(capsule, 'released', { [language.onlyIfOptions]: ['date'], date: language.formatDate(data.date), }), - language.$(capsule, 'artReleased', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.coverArtDate), - }), - language.$(capsule, 'duration', { [language.onlyIfOptions]: ['duration'], duration: language.formatDuration(data.duration), @@ -81,17 +61,9 @@ export default { ]), html.tag('p', - language.encapsulate(capsule, 'listenOn', capsule => - (relations.externalLinks - ? language.$(capsule, { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => link.slot('context', 'track'))), - }) - : language.$(capsule, 'noLinks', { - name: - html.tag('i', data.name), - })))), + relations.listenLine.slots({ + visibleWithoutLinks: true, + context: ['track'], + })), ])), }; diff --git a/src/content/dependencies/generateTrackSocialEmbed.js b/src/content/dependencies/generateTrackSocialEmbed.js index 9868f0e2..310816f3 100644 --- a/src/content/dependencies/generateTrackSocialEmbed.js +++ b/src/content/dependencies/generateTrackSocialEmbed.js @@ -4,7 +4,7 @@ export default { 'generateTrackSocialEmbedDescription', ], - extraDependencies: ['absoluteTo', 'language', 'urls'], + extraDependencies: ['absoluteTo', 'language'], relations(relation, track) { return { @@ -26,20 +26,18 @@ export default { data.trackDirectory = track.directory; data.albumDirectory = album.directory; + data.hasImage = track.hasUniqueCoverArt || album.hasCoverArt; + if (track.hasUniqueCoverArt) { - data.imageSource = 'track'; - data.coverArtFileExtension = track.coverArtFileExtension; + data.imagePath = track.trackArtworks[0].path; } else if (album.hasCoverArt) { - data.imageSource = 'album'; - data.coverArtFileExtension = album.coverArtFileExtension; - } else { - data.imageSource = 'none'; + data.imagePath = album.coverArtworks[0].path; } return data; }, - generate: (data, relations, {absoluteTo, language, urls}) => + generate: (data, relations, {absoluteTo, language}) => language.encapsulate('trackPage.socialEmbed', embedCapsule => relations.socialEmbed.slots({ title: @@ -47,6 +45,9 @@ export default { track: data.trackName, }), + description: + relations.description, + headingContent: language.$(embedCapsule, 'heading', { album: data.albumName, @@ -56,31 +57,8 @@ export default { 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) + (data.hasImage + ? data.imagePath : null), })), }; - -/* - socialEmbed: { - heading: language.$('trackPage.socialEmbed.heading', { - album: track.album.name, - }), - headingLink: absoluteTo('localized.album', album.directory), - title: language.$('trackPage.socialEmbed.title', { - track: track.name, - }), - description: getSocialEmbedDescription({getArtistString, language}), - image: '/' + getTrackCover(track, {to: urls.from('shared.root').to}), - color: track.color, - }, -*/ diff --git a/src/content/dependencies/generateTrackSocialEmbedDescription.js b/src/content/dependencies/generateTrackSocialEmbedDescription.js index cf21eadf..4706aa26 100644 --- a/src/content/dependencies/generateTrackSocialEmbedDescription.js +++ b/src/content/dependencies/generateTrackSocialEmbedDescription.js @@ -1,38 +1,39 @@ +import {empty} from '#sugar'; + export default { - generate() { - }, -}; + extraDependencies: ['html', 'language'], + + data: (track) => ({ + artistNames: + track.artistContribs + .map(contrib => contrib.artist.name), + + coverArtistNames: + track.coverArtistContribs + .map(contrib => contrib.artist.name), + }), -/* - const getSocialEmbedDescription = ({ - getArtistString: _getArtistString, - language, - }) => { - const hasArtists = !empty(track.artistContribs); - const hasCoverArtists = !empty(track.coverArtistContribs); - const getArtistString = (contribs) => - _getArtistString(contribs, { - // We don't want to put actual HTML tags in social embeds (sadly - // they don't get parsed and displayed, generally speaking), so - // override the link argument so that artist "links" just show - // their names. - link: {artist: (artist) => artist.name}, - }); - if (!hasArtists && !hasCoverArtists) return ''; - return language.formatString( - 'trackPage.socialEmbed.body' + - [hasArtists && '.withArtists', hasCoverArtists && '.withCoverArtists'] - .filter(Boolean) - .join(''), - Object.fromEntries( - [ - hasArtists && ['artists', getArtistString(track.artistContribs)], - hasCoverArtists && [ - 'coverArtists', - getArtistString(track.coverArtistContribs), - ], - ].filter(Boolean) - ) - ); - }; -*/ + generate: (data, {html, language}) => + language.encapsulate('trackPage.socialEmbed.body', baseCapsule => + language.encapsulate(baseCapsule, workingCapsule => { + const workingOptions = {}; + + if (!empty(data.artistNames)) { + workingCapsule += '.withArtists'; + workingOptions.artists = + language.formatConjunctionList(data.artistNames); + } + + if (!empty(data.coverArtistNames)) { + workingCapsule += '.withCoverArtists'; + workingOptions.coverArtists = + language.formatConjunctionList(data.coverArtistNames); + } + + if (workingCapsule === baseCapsule) { + return html.blank(); + } else { + return language.$(workingCapsule, workingOptions); + } + })), +}; diff --git a/src/content/dependencies/generateUnsafeMunchy.js b/src/content/dependencies/generateUnsafeMunchy.js new file mode 100644 index 00000000..c11aadc7 --- /dev/null +++ b/src/content/dependencies/generateUnsafeMunchy.js @@ -0,0 +1,10 @@ +export default { + extraDependencies: ['html'], + + slots: { + contentSource: {type: 'string'}, + }, + + generate: (slots, {html}) => + new html.Tag(null, null, slots.contentSource), +}; diff --git a/src/content/dependencies/generateWallpaperStyleTag.js b/src/content/dependencies/generateWallpaperStyleTag.js new file mode 100644 index 00000000..bf094300 --- /dev/null +++ b/src/content/dependencies/generateWallpaperStyleTag.js @@ -0,0 +1,80 @@ +import {empty, stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['generateStyleTag'], + extraDependencies: ['html', 'to'], + + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + }), + + slots: { + singleWallpaperPath: { + validate: v => v.strictArrayOf(v.isString), + }, + + singleWallpaperStyle: { + validate: v => v.isString, + }, + + wallpaperPartPaths: { + validate: v => + v.strictArrayOf(v.optional(v.strictArrayOf(v.isString))), + }, + + wallpaperPartStyles: { + validate: v => + v.strictArrayOf(v.optional(v.isString)), + }, + }, + + generate(relations, slots, {html, to}) { + const attributes = html.attributes(); + const rules = []; + + attributes.add('class', 'wallpaper-style'); + + if (empty(slots.wallpaperPartPaths)) { + attributes.set('data-wallpaper-mode', 'one'); + + rules.push({ + select: 'body::before', + declare: [ + `background-image: url("${to(...slots.singleWallpaperPath)}");`, + slots.singleWallpaperStyle, + ], + }); + } else { + attributes.set('data-wallpaper-mode', 'parts'); + attributes.set('data-num-wallpaper-parts', slots.wallpaperPartPaths.length); + + stitchArrays({ + path: slots.wallpaperPartPaths, + style: slots.wallpaperPartStyles, + }).forEach(({path, style}, index) => { + rules.push({ + select: `.wallpaper-part:nth-child(${index + 1})`, + declare: [ + path && `background-image: url("${to(...path)}");`, + style, + ], + }); + }); + + rules.push({ + select: 'body::before', + declare: [ + 'display: none;', + ], + }); + } + + relations.styleTag.setSlots({ + attributes, + rules, + }); + + return relations.styleTag; + }, +}; diff --git a/src/content/dependencies/generateWikiHomeAlbumsRow.js b/src/content/dependencies/generateWikiHomeAlbumsRow.js deleted file mode 100644 index 16c22bb3..00000000 --- a/src/content/dependencies/generateWikiHomeAlbumsRow.js +++ /dev/null @@ -1,150 +0,0 @@ -import {empty, stitchArrays} from '#sugar'; -import {getNewAdditions, getNewReleases} from '#wiki-data'; - -export default { - contentDependencies: [ - 'generateWikiHomeContentRow', - 'generateCoverCarousel', - 'generateCoverGrid', - 'image', - 'linkAlbum', - 'transformContent', - ], - - extraDependencies: ['language', 'wikiData'], - - sprawl({albumData}, row) { - const sprawl = {}; - - switch (row.sourceGroup) { - case 'new-releases': - sprawl.albums = getNewReleases(row.countAlbumsFromGroup, {albumData}); - break; - - case 'new-additions': - sprawl.albums = getNewAdditions(row.countAlbumsFromGroup, {albumData}); - break; - - default: - sprawl.albums = - (row.sourceGroup - ? row.sourceGroup.albums - .slice() - .reverse() - .filter(album => album.isListedOnHomepage) - .slice(0, row.countAlbumsFromGroup) - : []); - } - - if (!empty(row.sourceAlbums)) { - sprawl.albums.push(...row.sourceAlbums); - } - - return sprawl; - }, - - relations(relation, sprawl, row) { - const relations = {}; - - relations.contentRow = - relation('generateWikiHomeContentRow', row); - - if (row.displayStyle === 'grid') { - relations.coverGrid = - relation('generateCoverGrid'); - } - - if (row.displayStyle === 'carousel') { - relations.coverCarousel = - relation('generateCoverCarousel'); - } - - relations.links = - sprawl.albums - .map(album => relation('linkAlbum', album)); - - relations.images = - sprawl.albums - .map(album => relation('image', album.artTags)); - - if (row.actionLinks) { - relations.actionLinks = - row.actionLinks - .map(content => relation('transformContent', content)); - } - - return relations; - }, - - data(sprawl, row) { - const data = {}; - - data.displayStyle = row.displayStyle; - - if (row.displayStyle === 'grid') { - data.names = - sprawl.albums - .map(album => album.name); - } - - data.paths = - sprawl.albums - .map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); - - return data; - }, - - generate(data, relations, {language}) { - // Grids and carousels share some slots! Very convenient. - const commonSlots = {}; - - commonSlots.links = - relations.links; - - commonSlots.images = - stitchArrays({ - image: relations.images, - path: data.paths, - name: data.names ?? data.paths.slice().fill(null), - }).map(({image, path, name}) => - image.slots({ - path, - missingSourceContent: - language.$('misc.albumGrid.noCoverArt', { - [language.onlyIfOptions]: ['album'], - album: name, - }), - })); - - commonSlots.actionLinks = - (relations.actionLinks - ? relations.actionLinks - .map(contents => - contents - .slot('mode', 'single-link') - .content) - : null); - - let content; - - switch (data.displayStyle) { - case 'grid': - content = - relations.coverGrid.slots({ - ...commonSlots, - names: data.names, - }); - break; - - case 'carousel': - content = - relations.coverCarousel.slots(commonSlots); - break; - } - - return relations.contentRow.slots({content}); - }, -}; diff --git a/src/content/dependencies/generateWikiHomeContentRow.js b/src/content/dependencies/generateWikiHomeContentRow.js deleted file mode 100644 index 27b12e55..00000000 --- a/src/content/dependencies/generateWikiHomeContentRow.js +++ /dev/null @@ -1,28 +0,0 @@ -export default { - contentDependencies: ['generateColorStyleAttribute'], - extraDependencies: ['html'], - - relations: (relation, row) => ({ - colorStyle: - relation('generateColorStyleAttribute', row.color), - }), - - data: (row) => - ({name: row.name}), - - slots: { - content: { - type: 'html', - mutable: false, - }, - }, - - generate: (data, relations, slots, {html}) => - html.tag('section', {class: 'row'}, - relations.colorStyle, - - [ - html.tag('h2', data.name), - slots.content, - ]), -}; diff --git a/src/content/dependencies/generateWikiHomePage.js b/src/content/dependencies/generateWikiHomePage.js deleted file mode 100644 index ee14a587..00000000 --- a/src/content/dependencies/generateWikiHomePage.js +++ /dev/null @@ -1,116 +0,0 @@ -export default { - contentDependencies: [ - 'generatePageLayout', - 'generatePageSidebar', - 'generatePageSidebarBox', - 'generateWikiHomeAlbumsRow', - 'generateWikiHomeNewsBox', - 'transformContent', - ], - - extraDependencies: ['wikiData'], - - sprawl({wikiInfo}) { - return { - wikiName: wikiInfo.name, - - enableNews: wikiInfo.enableNews, - }; - }, - - relations(relation, sprawl, homepageLayout) { - const relations = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.sidebar = - relation('generatePageSidebar'); - - if (homepageLayout.sidebarContent) { - relations.customSidebarBox = - relation('generatePageSidebarBox'); - - relations.customSidebarContent = - relation('transformContent', homepageLayout.sidebarContent); - } - - if (sprawl.enableNews) { - relations.newsSidebarBox = - relation('generateWikiHomeNewsBox'); - } - - if (homepageLayout.navbarLinks) { - relations.customNavLinkContents = - homepageLayout.navbarLinks - .map(content => relation('transformContent', content)); - } - - relations.contentRows = - homepageLayout.rows.map(row => { - switch (row.type) { - case 'albums': - return relation('generateWikiHomeAlbumsRow', row); - default: - return null; - } - }); - - return relations; - }, - - data(sprawl) { - return { - wikiName: sprawl.wikiName, - }; - }, - - generate(data, relations) { - return relations.layout.slots({ - title: data.wikiName, - showWikiNameInTitle: false, - - mainClasses: ['top-index'], - headingMode: 'static', - - mainContent: [ - relations.contentRows, - ], - - leftSidebar: - relations.sidebar.slots({ - wide: true, - - boxes: [ - relations.customSidebarContent && - relations.customSidebarBox.slots({ - attributes: {class: 'custom-content-sidebar-box'}, - collapsible: false, - - content: - relations.customSidebarContent - .slot('mode', 'multiline'), - }), - - relations.newsSidebarBox, - ], - }), - - navLinkStyle: 'index', - navLinks: [ - {auto: 'home', current: true}, - - ...( - relations.customNavLinkContents - ?.map(content => ({ - html: - content.slots({ - mode: 'single-link', - preferShortLinkNames: true, - }), - })) - ?? []), - ], - }); - }, -}; diff --git a/src/content/dependencies/generateWikiHomepageActionsRow.js b/src/content/dependencies/generateWikiHomepageActionsRow.js new file mode 100644 index 00000000..9f501099 --- /dev/null +++ b/src/content/dependencies/generateWikiHomepageActionsRow.js @@ -0,0 +1,22 @@ +export default { + contentDependencies: ['generateGridActionLinks', 'transformContent'], + + relations: (relation, row) => ({ + template: + relation('generateGridActionLinks'), + + links: + row.actionLinks + .map(content => relation('transformContent', content)), + }), + + generate: (relations) => + relations.template.slots({ + actionLinks: + relations.links + .map(contents => + contents + .slot('mode', 'single-link') + .content), + }), +}; diff --git a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js new file mode 100644 index 00000000..b45bfc19 --- /dev/null +++ b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js @@ -0,0 +1,22 @@ +export default { + contentDependencies: ['generateCoverCarousel', 'image', 'linkAlbum'], + + relations: (relation, row) => ({ + coverCarousel: + relation('generateCoverCarousel'), + + links: + row.albums + .map(album => relation('linkAlbum', album)), + + images: + row.albums + .map(album => relation('image', album.coverArtworks[0])), + }), + + generate: (relations) => + relations.coverCarousel.slots({ + links: relations.links, + images: relations.images, + }), +}; diff --git a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js new file mode 100644 index 00000000..a00136ba --- /dev/null +++ b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js @@ -0,0 +1,78 @@ +import {empty, stitchArrays} from '#sugar'; +import {getNewAdditions, getNewReleases} from '#wiki-data'; + +export default { + contentDependencies: ['generateCoverGrid', 'image', 'linkAlbum'], + extraDependencies: ['language', 'wikiData'], + + sprawl({albumData}, row) { + const sprawl = {}; + + switch (row.sourceGroup) { + case 'new-releases': + sprawl.albums = getNewReleases(row.countAlbumsFromGroup, {albumData}); + break; + + case 'new-additions': + sprawl.albums = getNewAdditions(row.countAlbumsFromGroup, {albumData}); + break; + + default: + sprawl.albums = + (row.sourceGroup + ? row.sourceGroup.albums + .slice() + .reverse() + .filter(album => album.isListedOnHomepage) + .slice(0, row.countAlbumsFromGroup) + : []); + } + + if (!empty(row.sourceAlbums)) { + sprawl.albums.push(...row.sourceAlbums); + } + + return sprawl; + }, + + relations: (relation, sprawl, _row) => ({ + coverGrid: + relation('generateCoverGrid'), + + links: + sprawl.albums + .map(album => relation('linkAlbum', album)), + + images: + sprawl.albums + .map(album => + relation('image', + (album.hasCoverArt + ? album.coverArtworks[0] + : null))), + }), + + data: (sprawl, _row) => ({ + names: + sprawl.albums + .map(album => album.name), + }), + + generate: (data, relations, {language}) => + relations.coverGrid.slots({ + links: relations.links, + names: data.names, + + images: + stitchArrays({ + image: relations.images, + name: data.names, + }).map(({image, name}) => + image.slots({ + missingSourceContent: + language.$('misc.coverGrid.noCoverArt', { + album: name, + }), + })), + }), +}; diff --git a/src/content/dependencies/generateWikiHomeNewsBox.js b/src/content/dependencies/generateWikiHomepageNewsBox.js index 83a27695..83a27695 100644 --- a/src/content/dependencies/generateWikiHomeNewsBox.js +++ b/src/content/dependencies/generateWikiHomepageNewsBox.js diff --git a/src/content/dependencies/generateWikiHomepagePage.js b/src/content/dependencies/generateWikiHomepagePage.js new file mode 100644 index 00000000..8c09a007 --- /dev/null +++ b/src/content/dependencies/generateWikiHomepagePage.js @@ -0,0 +1,97 @@ +export default { + contentDependencies: [ + 'generatePageLayout', + 'generatePageSidebar', + 'generatePageSidebarBox', + 'generateWikiHomepageNewsBox', + 'generateWikiHomepageSection', + 'transformContent', + ], + + extraDependencies: ['wikiData'], + + sprawl: ({wikiInfo}) => ({ + wikiName: + wikiInfo.name, + + enableNews: + wikiInfo.enableNews, + }), + + relations: (relation, sprawl, homepageLayout) => ({ + layout: + relation('generatePageLayout'), + + sidebar: + relation('generatePageSidebar'), + + customSidebarBox: + relation('generatePageSidebarBox'), + + customSidebarContent: + relation('transformContent', homepageLayout.sidebarContent), + + newsSidebarBox: + (sprawl.enableNews + ? relation('generateWikiHomepageNewsBox') + : null), + + customNavLinkContents: + homepageLayout.navbarLinks + .map(content => relation('transformContent', content)), + + sections: + homepageLayout.sections + .map(section => relation('generateWikiHomepageSection', section)), + }), + + data: (sprawl) => ({ + wikiName: + sprawl.wikiName, + }), + + generate: (data, relations) => + relations.layout.slots({ + title: data.wikiName, + showWikiNameInTitle: false, + + mainClasses: ['top-index'], + headingMode: 'static', + + mainContent: [ + relations.sections, + ], + + leftSidebar: + relations.sidebar.slots({ + wide: true, + + boxes: [ + relations.customSidebarBox.slots({ + attributes: {class: 'custom-content-sidebar-box'}, + collapsible: false, + + content: + relations.customSidebarContent + .slot('mode', 'multiline'), + }), + + relations.newsSidebarBox, + ], + }), + + navLinkStyle: 'index', + navLinks: [ + {auto: 'home', current: true}, + + ... + relations.customNavLinkContents.map(content => ({ + html: + content.slots({ + mode: 'single-link', + preferShortLinkNames: true, + }), + })), + ], + }), +}; diff --git a/src/content/dependencies/generateWikiHomepageSection.js b/src/content/dependencies/generateWikiHomepageSection.js new file mode 100644 index 00000000..49a474da --- /dev/null +++ b/src/content/dependencies/generateWikiHomepageSection.js @@ -0,0 +1,39 @@ +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generateWikiHomepageActionsRow', + 'generateWikiHomepageAlbumCarouselRow', + 'generateWikiHomepageAlbumGridRow', + ], + + extraDependencies: ['html'], + + relations: (relation, homepageSection) => ({ + colorStyle: + relation('generateColorStyleAttribute', homepageSection.color), + + rows: + homepageSection.rows.map(row => + (row.type === 'actions' + ? relation('generateWikiHomepageActionsRow', row) + : row.type === 'album carousel' + ? relation('generateWikiHomepageAlbumCarouselRow', row) + : row.type === 'album grid' + ? relation('generateWikiHomepageAlbumGridRow', row) + : null)), + }), + + data: (homepageSection) => ({ + name: + homepageSection.name, + }), + + generate: (data, relations, {html}) => + html.tag('section', + relations.colorStyle, + + [ + html.tag('h2', data.name), + relations.rows, + ]), +}; diff --git a/src/content/dependencies/generateWikiWallpaperStyleTag.js b/src/content/dependencies/generateWikiWallpaperStyleTag.js new file mode 100644 index 00000000..12d27304 --- /dev/null +++ b/src/content/dependencies/generateWikiWallpaperStyleTag.js @@ -0,0 +1,38 @@ +export default { + contentDependencies: ['generateWallpaperStyleTag'], + extraDependencies: ['wikiData'], + + sprawl: ({wikiInfo}) => ({wikiInfo}), + + relations: (relation) => ({ + wallpaperStyleTag: + relation('generateWallpaperStyleTag'), + }), + + data: ({wikiInfo}) => ({ + singleWallpaperPath: [ + 'media.path', + 'bg.' + wikiInfo.wikiWallpaperFileExtension, + ], + + singleWallpaperStyle: + wikiInfo.wikiWallpaperStyle, + + wallpaperPartPaths: + wikiInfo.wikiWallpaperParts.map(part => + (part.asset + ? ['media.path', part.asset] + : null)), + + wallpaperPartStyles: + wikiInfo.wikiWallpaperParts.map(part => part.style), + }), + + generate: (data, relations) => + relations.wallpaperStyleTag.slots({ + singleWallpaperPath: data.singleWallpaperPath, + singleWallpaperStyle: data.singleWallpaperStyle, + wallpaperPartPaths: data.wallpaperPartPaths, + wallpaperPartStyles: data.wallpaperPartStyles, + }), +}; diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index b1f02819..bf47b14f 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -5,7 +5,7 @@ export default { extraDependencies: [ 'checkIfImagePathHasCachedThumbnails', 'getDimensionsOfImagePath', - 'getSizeOfImagePath', + 'getSizeOfMediaFile', 'getThumbnailEqualOrSmaller', 'getThumbnailsAvailableForDimensions', 'html', @@ -16,74 +16,83 @@ export default { contentDependencies: ['generateColorStyleAttribute'], - relations: (relation) => ({ + relations: (relation, _artwork) => ({ colorStyle: relation('generateColorStyleAttribute'), }), - data(artTags) { - const data = {}; - - if (artTags) { - data.contentWarnings = - artTags - .filter(tag => tag.isContentWarning) - .map(tag => tag.name); - } else { - data.contentWarnings = null; - } - - return data; - }, + data: (artwork) => ({ + path: + (artwork + ? artwork.path + : null), + + warnings: + (artwork + ? artwork.artTags + .filter(artTag => artTag.isContentWarning) + .map(artTag => artTag.name) + : null), + + dimensions: + (artwork + ? artwork.dimensions + : null), + }), slots: { - src: {type: 'string'}, - - path: { - validate: v => v.validateArrayItems(v.isString), - }, - thumb: {type: 'string'}, + reveal: {type: 'boolean', default: true}, + lazy: {type: 'boolean', default: false}, + square: {type: 'boolean', default: false}, + link: { validate: v => v.anyOf(v.isBoolean, v.isString), default: false, }, - color: { - validate: v => v.isColor, - }, + color: {validate: v => v.isColor}, - warnings: { - validate: v => v.looseArrayOf(v.isString), + // Added to the .image-container. + attributes: { + type: 'attributes', + mutable: false, }, - reveal: {type: 'boolean', default: true}, - lazy: {type: 'boolean', default: false}, - - square: {type: 'boolean', default: false}, + // Added to the <img> itself. + alt: {type: 'string'}, - dimensions: { - validate: v => v.isDimensions, - }, + // Specify 'src' or 'path', or the path will be used from the artwork. + // If none of the above is present, the message in missingSourceContent + // will be displayed instead. - alt: {type: 'string'}, + src: {type: 'string'}, - attributes: { - type: 'attributes', - mutable: false, + path: { + validate: v => v.validateArrayItems(v.isString), }, missingSourceContent: { type: 'html', mutable: false, }, + + // These will also be used from the artwork if not specified as slots. + + warnings: { + validate: v => v.looseArrayOf(v.isString), + }, + + dimensions: { + validate: v => v.isDimensions, + }, }, generate(data, relations, slots, { checkIfImagePathHasCachedThumbnails, getDimensionsOfImagePath, - getSizeOfImagePath, + getSizeOfMediaFile, getThumbnailEqualOrSmaller, getThumbnailsAvailableForDimensions, html, @@ -91,15 +100,14 @@ export default { missingImagePaths, to, }) { - let originalSrc; - - if (slots.src) { - originalSrc = slots.src; - } else if (!empty(slots.path)) { - originalSrc = to(...slots.path); - } else { - originalSrc = ''; - } + const originalSrc = + (slots.src + ? slots.src + : slots.path + ? to(...slots.path) + : data.path + ? to(...data.path) + : ''); // TODO: This feels janky. It's necessary to deal with static content that // includes strings like <img src="media/misc/foo.png">, but processing the @@ -121,29 +129,27 @@ export default { !isMissingImageFile && (typeof slots.link === 'string' || slots.link); - const contentWarnings = - slots.warnings ?? - data.contentWarnings; + const warnings = slots.warnings ?? data.warnings; + const dimensions = slots.dimensions ?? data.dimensions; const willReveal = slots.reveal && originalSrc && !isMissingImageFile && - !empty(contentWarnings); - - const willSquare = - slots.square; + !empty(warnings); const imgAttributes = html.attributes([ {class: 'image'}, slots.alt && {alt: slots.alt}, - slots.dimensions?.[0] && - {width: slots.dimensions[0]}, + dimensions && + dimensions[0] && + {width: dimensions[0]}, - slots.dimensions?.[1] && - {height: slots.dimensions[1]}, + dimensions && + dimensions[1] && + {height: dimensions[1]}, ]); const isPlaceholder = @@ -169,7 +175,7 @@ export default { html.tag('span', {class: 'reveal-warnings'}, language.$('misc.contentWarnings.warnings', { - warnings: language.formatUnitList(contentWarnings), + warnings: language.formatUnitList(warnings), })), html.tag('br'), @@ -224,19 +230,17 @@ export default { const originalDimensions = getDimensionsOfImagePath(mediaSrc); const availableThumbs = getThumbnailsAvailableForDimensions(originalDimensions); - const originalLength = Math.max(originalDimensions[0], originalDimensions[1]); const fileSize = (willLink && mediaSrc - ? getSizeOfImagePath(mediaSrc) + ? getSizeOfMediaFile(mediaSrc) : null); imgAttributes.add([ fileSize && {'data-original-size': fileSize}, - originalLength && - {'data-original-length': originalLength}, + {'data-dimensions': originalDimensions.join('x')}, !empty(availableThumbs) && {'data-thumbs': @@ -325,14 +329,14 @@ export default { wrapped = html.tag('div', {class: 'image-outer-area'}, - willSquare && + slots.square && {class: 'square-content'}, wrapped); wrapped = html.tag('div', {class: 'image-container'}, - willSquare && + slots.square && {class: 'square'}, typeof slots.link === 'string' && diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index a5009804..25d7324f 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -11,6 +11,11 @@ import {colors, logWarn} from '#cli'; import contentFunction, {ContentFunctionSpecError} from '#content-function'; import {annotateFunction} from '#sugar'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const codeSrcPath = path.resolve(__dirname, '..'); +const codeRootPath = path.resolve(codeSrcPath, '..'); + function cachebust(filePath) { if (filePath in cachebust.cache) { cachebust.cache[filePath] += 1; @@ -42,7 +47,9 @@ export function watchContentDependencies({ close, }); - const eslint = new ESLint(); + const eslint = new ESLint({ + cwd: codeRootPath, + }); const metaPath = fileURLToPath(import.meta.url); const metaDirname = path.dirname(metaPath); diff --git a/src/content/dependencies/linkAdditionalFile.js b/src/content/dependencies/linkAdditionalFile.js new file mode 100644 index 00000000..a8a940b1 --- /dev/null +++ b/src/content/dependencies/linkAdditionalFile.js @@ -0,0 +1,29 @@ +export default { + contentDependencies: ['linkTemplate'], + + query: (file, filename) => ({ + index: + file.filenames.indexOf(filename), + }), + + relations: (relation, _query, _file, _filename) => ({ + linkTemplate: + relation('linkTemplate'), + }), + + data: (query, file, filename) => ({ + filename, + + // Kinda jank, but eh. + path: + (query.index >= 0 + ? file.paths.at(query.index) + : null), + }), + + generate: (data, relations) => + relations.linkTemplate.slots({ + path: data.path, + content: data.filename, + }), +}; diff --git a/src/content/dependencies/linkAlbumAdditionalFile.js b/src/content/dependencies/linkAlbumAdditionalFile.js deleted file mode 100644 index 39e7111e..00000000 --- a/src/content/dependencies/linkAlbumAdditionalFile.js +++ /dev/null @@ -1,24 +0,0 @@ -export default { - contentDependencies: ['linkTemplate'], - - relations(relation) { - return { - linkTemplate: relation('linkTemplate'), - }; - }, - - data(album, file) { - return { - albumDirectory: album.directory, - file, - }; - }, - - generate(data, relations) { - return relations.linkTemplate - .slots({ - path: ['media.albumAdditionalFile', data.albumDirectory, data.file], - content: data.file, - }); - }, -}; diff --git a/src/content/dependencies/linkAlbumDynamically.js b/src/content/dependencies/linkAlbumDynamically.js index 3adc64df..45f8c2a9 100644 --- a/src/content/dependencies/linkAlbumDynamically.js +++ b/src/content/dependencies/linkAlbumDynamically.js @@ -1,14 +1,61 @@ +import {empty} from '#sugar'; + export default { - contentDependencies: ['linkAlbumGallery', 'linkAlbum'], - extraDependencies: ['pagePath'], + contentDependencies: [ + 'linkAlbumCommentary', + 'linkAlbumGallery', + 'linkAlbum', + ], + + extraDependencies: ['html', 'pagePath'], relations: (relation, album) => ({ - galleryLink: relation('linkAlbumGallery', album), - infoLink: relation('linkAlbum', album), + galleryLink: + relation('linkAlbumGallery', album), + + infoLink: + relation('linkAlbum', album), + + commentaryLink: + relation('linkAlbumCommentary', album), }), - generate: (relations, {pagePath}) => - (pagePath[0] === 'albumGallery' + data: (album) => ({ + albumDirectory: + album.directory, + + albumHasCommentary: + !empty(album.commentary), + }), + + slots: { + linkCommentaryPages: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots, {pagePath}) => + // When linking to an album *from* an album commentary page, + // if the link is to the *same* album, then the effective target + // of the link is really the album's commentary, so scroll to it. + (pagePath[0] === 'albumCommentary' && + pagePath[1] === data.albumDirectory && + data.albumHasCommentary + ? relations.infoLink.slots({ + anchor: true, + hash: 'album-commentary', + }) + + // When linking to *another* album from an album commentary page, + // the target is (by default) still just the album (its info page). + // But this can be customized per-link! + : pagePath[0] === 'albumCommentary' && + slots.linkCommentaryPages + ? relations.commentaryLink + + : pagePath[0] === 'albumGallery' ? relations.galleryLink + : relations.infoLink), }; diff --git a/src/content/dependencies/linkAlbumReferencedArtworks.js b/src/content/dependencies/linkAlbumReferencedArtworks.js new file mode 100644 index 00000000..ba51b5e3 --- /dev/null +++ b/src/content/dependencies/linkAlbumReferencedArtworks.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, album) => + ({link: relation('linkThing', 'localized.albumReferencedArtworks', album)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkAlbumReferencingArtworks.js b/src/content/dependencies/linkAlbumReferencingArtworks.js new file mode 100644 index 00000000..4d5e799d --- /dev/null +++ b/src/content/dependencies/linkAlbumReferencingArtworks.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, album) => + ({link: relation('linkThing', 'localized.albumReferencingArtworks', album)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkAnythingMan.js b/src/content/dependencies/linkAnythingMan.js index d4697403..e408c1b2 100644 --- a/src/content/dependencies/linkAnythingMan.js +++ b/src/content/dependencies/linkAnythingMan.js @@ -1,6 +1,7 @@ export default { contentDependencies: [ 'linkAlbum', + 'linkArtwork', 'linkFlash', 'linkTrack', ], @@ -13,6 +14,8 @@ export default { link: (query.referenceType === 'album' ? relation('linkAlbum', thing) + : query.referenceType === 'artwork' + ? relation('linkArtwork', thing) : query.referenceType === 'flash' ? relation('linkFlash', thing) : query.referenceType === 'track' diff --git a/src/content/dependencies/linkArtTagDynamically.js b/src/content/dependencies/linkArtTagDynamically.js new file mode 100644 index 00000000..964258e1 --- /dev/null +++ b/src/content/dependencies/linkArtTagDynamically.js @@ -0,0 +1,14 @@ +export default { + contentDependencies: ['linkArtTagGallery', 'linkArtTagInfo'], + extraDependencies: ['pagePath'], + + relations: (relation, artTag) => ({ + galleryLink: relation('linkArtTagGallery', artTag), + infoLink: relation('linkArtTagInfo', artTag), + }), + + generate: (relations, {pagePath}) => + (pagePath[0] === 'artTagInfo' + ? relations.infoLink + : relations.galleryLink), +}; diff --git a/src/content/dependencies/linkArtTagGallery.js b/src/content/dependencies/linkArtTagGallery.js new file mode 100644 index 00000000..a92b69c1 --- /dev/null +++ b/src/content/dependencies/linkArtTagGallery.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, artTag) => + ({link: relation('linkThing', 'localized.artTagGallery', artTag)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkArtTag.js b/src/content/dependencies/linkArtTagInfo.js index 7ddb7786..409cb3c0 100644 --- a/src/content/dependencies/linkArtTag.js +++ b/src/content/dependencies/linkArtTagInfo.js @@ -2,7 +2,7 @@ export default { contentDependencies: ['linkThing'], relations: (relation, artTag) => - ({link: relation('linkThing', 'localized.tag', artTag)}), + ({link: relation('linkThing', 'localized.artTagInfo', artTag)}), generate: (relations) => relations.link, }; diff --git a/src/content/dependencies/linkArtwork.js b/src/content/dependencies/linkArtwork.js new file mode 100644 index 00000000..8cd6f359 --- /dev/null +++ b/src/content/dependencies/linkArtwork.js @@ -0,0 +1,20 @@ +export default { + contentDependencies: ['linkAlbum', 'linkTrack'], + + query: (artwork) => ({ + referenceType: + artwork.thing.constructor[Symbol.for('Thing.referenceType')], + }), + + relations: (relation, query, artwork) => ({ + link: + (query.referenceType === 'album' + ? relation('linkAlbum', artwork.thing) + : query.referenceType === 'track' + ? relation('linkTrack', artwork.thing) + : null), + }), + + generate: (relations) => + relations.link, +}; diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js index 26f0b2d7..c658d461 100644 --- a/src/content/dependencies/linkContribution.js +++ b/src/content/dependencies/linkContribution.js @@ -19,16 +19,20 @@ export default { }), data: (contribution) => ({ - contribution: contribution.annotation, + annotation: contribution.annotation, urls: contribution.artist.urls, }), slots: { - showContribution: {type: 'boolean', default: false}, + showAnnotation: {type: 'boolean', default: false}, showExternalLinks: {type: 'boolean', default: false}, showChronology: {type: 'boolean', default: false}, + trimAnnotation: {type: 'boolean', default: false}, + preventWrapping: {type: 'boolean', default: true}, + preventTooltip: {type: 'boolean', default: false}, + chronologyKind: {type: 'string'}, }, @@ -42,6 +46,9 @@ export default { language.encapsulate('misc.artistLink', workingCapsule => { const workingOptions = {}; + // Filling slots early is necessary to actually give the tooltip + // content. Otherwise, the coming-up html.isBlank() always reports + // the tooltip as blank! relations.tooltip.setSlots({ showExternalLinks: slots.showExternalLinks, showChronology: slots.showChronology, @@ -49,7 +56,7 @@ export default { }); workingOptions.artist = - (html.isBlank(relations.tooltip) + (html.isBlank(relations.tooltip) || slots.preventTooltip ? relations.artistLink : relations.textWithTooltip.slots({ customInteractionCue: true, @@ -60,17 +67,17 @@ export default { }), tooltip: - relations.tooltip.slots({ - showExternalLinks: slots.showExternalLinks, - showChronology: slots.showChronology, - chronologyKind: slots.chronologyKind, - }), + relations.tooltip, })); - if (slots.showContribution && data.contribution) { + const annotation = + (slots.trimAnnotation + ? data.annotation?.replace(/^edits for wiki(: )?/, '') + : data.annotation); + + if (slots.showAnnotation && annotation) { workingCapsule += '.withContribution'; - workingOptions.contrib = - data.contribution; + workingOptions.contrib = annotation; } return language.formatString(workingCapsule, workingOptions); diff --git a/src/content/dependencies/linkExternal.js b/src/content/dependencies/linkExternal.js index f6b47db7..45c08a08 100644 --- a/src/content/dependencies/linkExternal.js +++ b/src/content/dependencies/linkExternal.js @@ -11,6 +11,11 @@ export default { mutable: false, }, + suffixNormalContent: { + type: 'html', + mutable: false, + }, + style: { // This awkward syntax is because the slot descriptor validator can't // differentiate between a function that returns a validator (the usual @@ -34,6 +39,11 @@ export default { default: false, }, + disableBrowserTooltip: { + type: 'boolean', + default: false, + }, + tab: { validate: v => v.is('default', 'separate'), default: 'default', @@ -45,7 +55,7 @@ export default { try { new URL(data.url); urlIsValid = true; - } catch (error) { + } catch { urlIsValid = false; } @@ -106,7 +116,9 @@ export default { linkAttributes.add('class', 'indicate-external'); let titleText; - if (slots.tab === 'separate') { + if (slots.disableBrowserTooltip) { + titleText = null; + } else if (slots.tab === 'separate') { if (html.isBlank(slots.content)) { titleText = language.$('misc.external.opensInNewTab.annotation'); @@ -131,6 +143,16 @@ export default { linkAttributes.set('target', '_blank'); } + if (!html.isBlank(slots.suffixNormalContent)) { + linkContent = + html.tags([ + linkContent, + + html.tag('span', {class: 'normal-content'}, + slots.suffixNormalContent), + ], {[html.joinChildren]: ''}); + } + return html.tag('a', linkAttributes, linkContent); }, }; diff --git a/src/content/dependencies/linkFlashAct.js b/src/content/dependencies/linkFlashAct.js index fbb819ed..82c23325 100644 --- a/src/content/dependencies/linkFlashAct.js +++ b/src/content/dependencies/linkFlashAct.js @@ -1,14 +1,22 @@ export default { - contentDependencies: ['linkThing'], - extraDependencies: ['html'], + contentDependencies: ['generateUnsafeMunchy', 'linkThing'], - relations: (relation, flashAct) => - ({link: relation('linkThing', 'localized.flashActGallery', flashAct)}), + relations: (relation, flashAct) => ({ + unsafeMunchy: + relation('generateUnsafeMunchy'), - data: (flashAct) => - ({name: flashAct.name}), + link: + relation('linkThing', 'localized.flashActGallery', flashAct), + }), - generate: (data, relations, {html}) => - relations.link - .slot('content', new html.Tag(null, null, data.name)), + data: (flashAct) => ({ + name: flashAct.name, + }), + + generate: (data, relations) => + relations.link.slots({ + content: + relations.unsafeMunchy + .slot('contentSource', data.name), + }), }; diff --git a/src/content/dependencies/linkFlashSide.js b/src/content/dependencies/linkFlashSide.js new file mode 100644 index 00000000..b77ca65a --- /dev/null +++ b/src/content/dependencies/linkFlashSide.js @@ -0,0 +1,22 @@ +export default { + contentDependencies: ['linkFlashAct'], + + relations: (relation, flashSide) => ({ + link: + relation('linkFlashAct', flashSide.acts[0]), + }), + + data: (flashSide) => ({ + name: + flashSide.name, + + color: + flashSide.color, + }), + + generate: (data, relations) => + relations.link.slots({ + content: data.name, + color: data.color, + }), +}; diff --git a/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js b/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js new file mode 100644 index 00000000..ec856631 --- /dev/null +++ b/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js @@ -0,0 +1,62 @@ +import {sortAlbumsTracksChronologically, sortContributionsChronologically} + from '#sort'; +import {chunkArtistTrackContributions} from '#wiki-data'; + +export default { + contentDependencies: ['generateColorStyleAttribute'], + extraDependencies: ['html', 'language'], + + query(track, artist) { + const relevantInfoPageChunkingContributions = + track.allReleases + .flatMap(release => [ + ...release.artistContribs, + ...release.contributorContribs, + ]) + .filter(c => c.artist === artist); + + sortContributionsChronologically( + relevantInfoPageChunkingContributions, + sortAlbumsTracksChronologically); + + const contributionChunks = + chunkArtistTrackContributions(relevantInfoPageChunkingContributions); + + const trackChunks = + contributionChunks + .map(chunksInAlbum => chunksInAlbum + .map(chunksInTrack => chunksInTrack[0].thing)); + + const trackChunksForThisAlbum = + trackChunks + .filter(tracks => tracks[0].album === track.album); + + const containingChunkIndex = + trackChunksForThisAlbum + .findIndex(tracks => tracks.includes(track)); + + return {containingChunkIndex}; + }, + + relations: (relation, _query, track, _artist) => ({ + colorStyle: + relation('generateColorStyleAttribute', track.album.color), + }), + + data: (query, track, _artist) => ({ + albumName: + track.album.name, + + albumDirectory: + track.album.directory, + + containingChunkIndex: + query.containingChunkIndex, + }), + + generate: (data, relations, {html, language}) => + html.tag('a', + {href: `#tracks-${data.albumDirectory}-${data.containingChunkIndex}`}, + relations.colorStyle.slot('context', 'primary-only'), + language.sanitize(data.albumName)), +}; diff --git a/src/content/dependencies/linkPathFromMedia.js b/src/content/dependencies/linkPathFromMedia.js index 34a2b857..d71c69f8 100644 --- a/src/content/dependencies/linkPathFromMedia.js +++ b/src/content/dependencies/linkPathFromMedia.js @@ -1,13 +1,64 @@ +import {empty} from '#sugar'; + export default { contentDependencies: ['linkTemplate'], + extraDependencies: [ + 'checkIfImagePathHasCachedThumbnails', + 'getDimensionsOfImagePath', + 'getSizeOfMediaFile', + 'getThumbnailsAvailableForDimensions', + 'html', + 'to', + ], + relations: (relation) => ({link: relation('linkTemplate')}), data: (path) => ({path}), - generate: (data, relations) => - relations.link - .slot('path', ['media.path', data.path]), + generate(data, relations, { + checkIfImagePathHasCachedThumbnails, + getDimensionsOfImagePath, + getSizeOfMediaFile, + getThumbnailsAvailableForDimensions, + html, + to, + }) { + const attributes = html.attributes(); + + if (checkIfImagePathHasCachedThumbnails(data.path)) { + const dimensions = getDimensionsOfImagePath(data.path); + const availableThumbs = getThumbnailsAvailableForDimensions(dimensions); + const fileSize = getSizeOfMediaFile(data.path); + + const embedSrc = + to('thumb.path', data.path.replace(/\.(png|jpg)$/, '.tack.jpg')); + + attributes.add([ + {class: 'image-media-link'}, + + {'data-embed-src': embedSrc}, + + fileSize && + {'data-original-size': fileSize}, + + {'data-dimensions': dimensions.join('x')}, + + !empty(availableThumbs) && + {'data-thumbs': + availableThumbs + .map(([name, size]) => `${name}:${size}`) + .join(' ')}, + ]); + } + + relations.link.setSlots({ + attributes, + path: ['media.path', data.path], + }); + + return relations.link; + }, }; diff --git a/src/content/dependencies/linkReferencedArtworks.js b/src/content/dependencies/linkReferencedArtworks.js new file mode 100644 index 00000000..c456b808 --- /dev/null +++ b/src/content/dependencies/linkReferencedArtworks.js @@ -0,0 +1,24 @@ +import Thing from '#thing'; + +export default { + contentDependencies: [ + 'linkAlbumReferencedArtworks', + 'linkTrackReferencedArtworks', + ], + + query: (artwork) => ({ + referenceType: + artwork.thing.constructor[Thing.referenceType], + }), + + relations: (relation, query, artwork) => ({ + link: + (query.referenceType === 'album' + ? relation('linkAlbumReferencedArtworks', artwork.thing) + : query.referenceType === 'track' + ? relation('linkTrackReferencedArtworks', artwork.thing) + : null), + }), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkReferencingArtworks.js b/src/content/dependencies/linkReferencingArtworks.js new file mode 100644 index 00000000..0cfca4db --- /dev/null +++ b/src/content/dependencies/linkReferencingArtworks.js @@ -0,0 +1,24 @@ +import Thing from '#thing'; + +export default { + contentDependencies: [ + 'linkAlbumReferencingArtworks', + 'linkTrackReferencingArtworks', + ], + + query: (artwork) => ({ + referenceType: + artwork.thing.constructor[Thing.referenceType], + }), + + relations: (relation, query, artwork) => ({ + link: + (query.referenceType === 'album' + ? relation('linkAlbumReferencingArtworks', artwork.thing) + : query.referenceType === 'track' + ? relation('linkTrackReferencingArtworks', artwork.thing) + : null), + }), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index 63cc82e8..4f853dc4 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -26,6 +26,11 @@ export default { type: 'html', mutable: false, }, + + suffixNormalContent: { + type: 'html', + mutable: false, + }, }, generate(slots, { @@ -61,13 +66,22 @@ export default { attributes.set('title', slots.tooltip); } - const content = + const mainContent = (html.isBlank(slots.content) ? language.$('misc.missingLinkContent') - : striptags(html.resolve(slots.content, {normalize: 'string'}), { - disallowedTags: new Set(['a']), - })); + : striptags( + html.resolve(slots.content, {normalize: 'string'}), + {disallowedTags: new Set(['a'])})); + + const allContent = + (html.isBlank(slots.suffixNormalContent) + ? mainContent + : html.tags([ + mainContent, + html.tag('span', {class: 'normal-content'}, + slots.suffixNormalContent), + ], {[html.joinChildren]: ''})); - return html.tag('a', attributes, content); + return html.tag('a', attributes, allContent); }, } diff --git a/src/content/dependencies/linkTrackDynamically.js b/src/content/dependencies/linkTrackDynamically.js index 242cd4cb..bbcf1c34 100644 --- a/src/content/dependencies/linkTrackDynamically.js +++ b/src/content/dependencies/linkTrackDynamically.js @@ -1,3 +1,5 @@ +import {empty} from '#sugar'; + export default { contentDependencies: ['linkTrack'], extraDependencies: ['pagePath'], @@ -14,7 +16,7 @@ export default { track.album.directory, trackHasCommentary: - !!track.commentary, + !empty(track.commentary), }), generate(data, relations, {pagePath}) { diff --git a/src/content/dependencies/linkTrackReferencedArtworks.js b/src/content/dependencies/linkTrackReferencedArtworks.js new file mode 100644 index 00000000..b4cb08fe --- /dev/null +++ b/src/content/dependencies/linkTrackReferencedArtworks.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, track) => + ({link: relation('linkThing', 'localized.trackReferencedArtworks', track)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkTrackReferencingArtworks.js b/src/content/dependencies/linkTrackReferencingArtworks.js new file mode 100644 index 00000000..c9c9f4d1 --- /dev/null +++ b/src/content/dependencies/linkTrackReferencingArtworks.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, track) => + ({link: relation('linkThing', 'localized.trackReferencingArtworks', track)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkWikiHome.js b/src/content/dependencies/linkWikiHomepage.js index d8d3d0a0..d8d3d0a0 100644 --- a/src/content/dependencies/linkWikiHome.js +++ b/src/content/dependencies/linkWikiHomepage.js diff --git a/src/content/dependencies/listAllAdditionalFilesTemplate.js b/src/content/dependencies/listAllAdditionalFilesTemplate.js index e33ad7b5..8ec69f1d 100644 --- a/src/content/dependencies/listAllAdditionalFilesTemplate.js +++ b/src/content/dependencies/listAllAdditionalFilesTemplate.js @@ -1,209 +1,44 @@ import {sortChronologically} from '#sort'; -import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { contentDependencies: [ 'generateListingPage', - 'generateListAllAdditionalFilesChunk', - 'linkAlbum', - 'linkTrack', - 'linkAlbumAdditionalFile', + 'generateListAllAdditionalFilesAlbumSection', ], - extraDependencies: ['html', 'language', 'wikiData'], + extraDependencies: ['html', 'wikiData'], sprawl: ({albumData}) => ({albumData}), - query(sprawl, spec, property) { - const albums = - sortChronologically(sprawl.albumData.slice()); + query: (sprawl, spec, property) => ({ + spec, + property, - const tracks = - albums - .map(album => album.tracks.slice()); - - // Get additional file objects from albums and their tracks. - // There's a possibility that albums and tracks don't both implement - // the same additional file fields - in this case, just treat them - // as though they do implement those fields, but don't have any - // additional files of that type. - - const albumAdditionalFileObjects = - albums - .map(album => album[property] ?? []); - - const trackAdditionalFileObjects = - tracks - .map(byAlbum => byAlbum - .map(track => track[property] ?? [])); - - // Filter out tracks that don't have any additional files. - - stitchArrays({tracks, trackAdditionalFileObjects}) - .forEach(({tracks, trackAdditionalFileObjects}) => { - filterMultipleArrays(tracks, trackAdditionalFileObjects, - (track, trackAdditionalFileObjects) => !empty(trackAdditionalFileObjects)); - }); - - // Filter out albums that don't have any tracks, - // nor any additional files of their own. - - filterMultipleArrays(albums, albumAdditionalFileObjects, tracks, trackAdditionalFileObjects, - (album, albumAdditionalFileObjects, tracks, trackAdditionalFileObjects) => - !empty(albumAdditionalFileObjects) || - !empty(trackAdditionalFileObjects)); - - // Map additional file objects into titles and lists of file names. - - const albumAdditionalFileTitles = - albumAdditionalFileObjects - .map(byAlbum => byAlbum - .map(({title}) => title)); - - const albumAdditionalFileFiles = - albumAdditionalFileObjects - .map(byAlbum => byAlbum - .map(({files}) => files ?? [])); - - const trackAdditionalFileTitles = - trackAdditionalFileObjects - .map(byAlbum => byAlbum - .map(byTrack => byTrack - .map(({title}) => title))); - - const trackAdditionalFileFiles = - trackAdditionalFileObjects - .map(byAlbum => byAlbum - .map(byTrack => byTrack - .map(({files}) => files ?? []))); - - return { - spec, - albums, - tracks, - albumAdditionalFileTitles, - albumAdditionalFileFiles, - trackAdditionalFileTitles, - trackAdditionalFileFiles, - }; - }, + albums: + sortChronologically(sprawl.albumData.slice()), + }), relations: (relation, query) => ({ page: relation('generateListingPage', query.spec), - albumLinks: - query.albums - .map(album => relation('linkAlbum', album)), - - trackLinks: - query.tracks - .map(byAlbum => byAlbum - .map(track => relation('linkTrack', track))), - - albumChunks: - query.albums - .map(() => relation('generateListAllAdditionalFilesChunk')), - - trackChunks: - query.tracks - .map(byAlbum => byAlbum - .map(() => relation('generateListAllAdditionalFilesChunk'))), - - albumAdditionalFileLinks: - stitchArrays({ - album: query.albums, - files: query.albumAdditionalFileFiles, - }).map(({album, files: byAlbum}) => - byAlbum.map(files => files - .map(file => - relation('linkAlbumAdditionalFile', album, file)))), - - trackAdditionalFileLinks: - stitchArrays({ - album: query.albums, - files: query.trackAdditionalFileFiles, - }).map(({album, files: byAlbum}) => - byAlbum - .map(byTrack => byTrack - .map(files => files - .map(file => relation('linkAlbumAdditionalFile', album, file))))), - }), - - data: (query) => ({ - albumAdditionalFileTitles: query.albumAdditionalFileTitles, - trackAdditionalFileTitles: query.trackAdditionalFileTitles, - albumAdditionalFileFiles: query.albumAdditionalFileFiles, - trackAdditionalFileFiles: query.trackAdditionalFileFiles, + albumSections: + query.albums.map(album => + relation('generateListAllAdditionalFilesAlbumSection', + album, + query.property)), }), slots: { stringsKey: {type: 'string'}, }, - generate: (data, relations, slots, {html, language}) => + generate: (relations, slots) => relations.page.slots({ type: 'custom', content: - stitchArrays({ - albumLink: relations.albumLinks, - trackLinks: relations.trackLinks, - albumChunk: relations.albumChunks, - trackChunks: relations.trackChunks, - albumAdditionalFileTitles: data.albumAdditionalFileTitles, - trackAdditionalFileTitles: data.trackAdditionalFileTitles, - albumAdditionalFileLinks: relations.albumAdditionalFileLinks, - trackAdditionalFileLinks: relations.trackAdditionalFileLinks, - albumAdditionalFileFiles: data.albumAdditionalFileFiles, - trackAdditionalFileFiles: data.trackAdditionalFileFiles, - }).map(({ - albumLink, - trackLinks, - albumChunk, - trackChunks, - albumAdditionalFileTitles, - trackAdditionalFileTitles, - albumAdditionalFileLinks, - trackAdditionalFileLinks, - albumAdditionalFileFiles, - trackAdditionalFileFiles, - }) => [ - html.tag('h3', {class: 'content-heading'}, albumLink), - - html.tag('dl', [ - albumChunk.slots({ - title: - language.$('listingPage', slots.stringsKey, 'albumFiles'), - - additionalFileTitles: albumAdditionalFileTitles, - additionalFileLinks: albumAdditionalFileLinks, - additionalFileFiles: albumAdditionalFileFiles, - - stringsKey: slots.stringsKey, - }), - - stitchArrays({ - trackLink: trackLinks, - trackChunk: trackChunks, - trackAdditionalFileTitles, - trackAdditionalFileLinks, - trackAdditionalFileFiles, - }).map(({ - trackLink, - trackChunk, - trackAdditionalFileTitles, - trackAdditionalFileLinks, - trackAdditionalFileFiles, - }) => - trackChunk.slots({ - title: trackLink, - additionalFileTitles: trackAdditionalFileTitles, - additionalFileLinks: trackAdditionalFileLinks, - additionalFileFiles: trackAdditionalFileFiles, - stringsKey: slots.stringsKey, - })), - ]), - ]), + relations.albumSections.map(section => + section.slot('stringsKey', slots.stringsKey)), }), }; diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js index b3a54747..93dd4ce8 100644 --- a/src/content/dependencies/listArtTagNetwork.js +++ b/src/content/dependencies/listArtTagNetwork.js @@ -1 +1,366 @@ -export default {generate() {}}; +import {sortAlphabetically} from '#sort'; +import {empty, stitchArrays, unique} from '#sugar'; + +export default { + contentDependencies: ['generateListingPage', 'linkArtTagInfo'], + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl({artTagData}) { + return {artTagData}; + }, + + query(sprawl, spec) { + const artTags = + sprawl.artTagData.filter(artTag => !artTag.isContentWarning); + + const rootArtTags = + artTags + .filter(artTag => !empty(artTag.directDescendantArtTags)) + .filter(artTag => + empty(artTag.directAncestorArtTags) || + artTag.directAncestorArtTags.length >= 2); + + sortAlphabetically(rootArtTags); + + rootArtTags.sort( + ({directAncestorArtTags: ancestorsA}, + {directAncestorArtTags: ancestorsB}) => + ancestorsA.length - ancestorsB.length); + + const getStats = (artTag) => ({ + directUses: + artTag.directlyFeaturedInArtworks.length, + + // Not currently displayed + directAndIndirectUses: + unique([ + ...artTag.indirectlyFeaturedInArtworks, + ...artTag.directlyFeaturedInArtworks, + ]).length, + + totalUses: + [ + ...artTag.directlyFeaturedInArtworks, + ... + artTag.allDescendantArtTags + .flatMap(artTag => artTag.directlyFeaturedInArtworks), + ].length, + + descendants: + artTag.allDescendantArtTags.length, + + leaves: + (empty(artTag.directDescendantArtTags) + ? null + : artTag.allDescendantArtTags + .filter(artTag => empty(artTag.directDescendantArtTags)) + .length), + }); + + const recursive = (artTag, depth) => { + const descendantNodes = + (empty(artTag.directDescendantArtTags) + ? null + : depth > 0 && artTag.directAncestorArtTags.length >= 2 + ? null + : artTag.directDescendantArtTags + .map(artTag => recursive(artTag, depth + 1))); + + descendantNodes?.sort( + ({descendantNodes: descendantNodesA}, + {descendantNodes: descendantNodesB}) => + (descendantNodesA ? 1 : 0) + - (descendantNodesB ? 1 : 0)); + + const recursiveGetRootAncestor = ancestorArtTag => + (ancestorArtTag.directAncestorArtTags.length === 1 + ? recursiveGetRootAncestor(ancestorArtTag.directAncestorArtTags[0]) + : ancestorArtTag); + + const ancestorRootArtTags = + (depth === 0 && !empty(artTag.directAncestorArtTags) + ? unique(artTag.directAncestorArtTags.map(recursiveGetRootAncestor)) + : null); + + const stats = getStats(artTag); + + return { + artTag, + stats, + descendantNodes, + ancestorRootArtTags, + }; + }; + + const uppermostRootTags = + artTags + .filter(artTag => !empty(artTag.directDescendantArtTags)) + .filter(artTag => empty(artTag.directAncestorArtTags)); + + const orphanArtTags = + artTags + .filter(artTag => empty(artTag.directDescendantArtTags)) + .filter(artTag => empty(artTag.directAncestorArtTags)); + + return { + spec, + + rootNodes: + rootArtTags + .map(artTag => recursive(artTag, 0)), + + uppermostRootTags, + orphanArtTags, + }; + }, + + relations(relation, query) { + const recursive = queryNode => ({ + artTagLink: + relation('linkArtTagInfo', queryNode.artTag), + + ancestorTagLinks: + queryNode.ancestorRootArtTags + ?.map(artTag => relation('linkArtTagInfo', artTag)) + ?? null, + + descendantNodes: + queryNode.descendantNodes + ?.map(recursive) + ?? null, + }); + + return { + page: + relation('generateListingPage', query.spec), + + rootNodes: + query.rootNodes.map(recursive), + + uppermostRootTagLinks: + query.uppermostRootTags + .map(artTag => relation('linkArtTagInfo', artTag)), + + orphanArtTagLinks: + query.orphanArtTags + .map(artTag => relation('linkArtTagInfo', artTag)), + }; + }, + + data(query) { + const rootArtTags = query.rootNodes.map(({artTag}) => artTag); + + const recursive = queryNode => ({ + directory: + queryNode.artTag.directory, + + directUses: + queryNode.stats.directUses, + + totalUses: + queryNode.stats.totalUses, + + descendants: + queryNode.stats.descendants, + + leaves: + queryNode.stats.leaves, + + representsRoot: + rootArtTags.includes(queryNode.artTag), + + ancestorTagDirectories: + queryNode.ancestorRootArtTags + ?.map(artTag => artTag.directory) + ?? null, + + descendantNodes: + queryNode.descendantNodes + ?.map(recursive) + ?? null, + }); + + return { + rootNodes: + query.rootNodes.map(recursive), + + uppermostRootTagDirectories: + query.uppermostRootTags + .map(artTag => artTag.directory), + }; + }, + + generate(data, relations, {html, language}) { + const prefix = `listingPage.listArtTags.network`; + + const wrapTagWithJumpTo = (dataNode, relationsNode, depth) => + (depth === 0 + ? relationsNode.artTagLink + : dataNode.representsRoot + ? language.$(prefix, 'tag.jumpToRoot', { + tag: + relationsNode.artTagLink.slots({ + anchor: true, + hash: dataNode.directory, + }), + }) + : relationsNode.artTagLink); + + const wrapTagWithStats = (dataNode, relationsNode, depth) => [ + html.tag('span', {class: 'network-tag'}, + language.$(prefix, 'tag', { + tag: + wrapTagWithJumpTo(dataNode, relationsNode, depth), + })), + + html.tag('span', {class: 'network-tag'}, + {class: 'with-stat'}, + {style: 'display: none'}, + + language.$(prefix, 'tag.withStat', { + tag: + wrapTagWithJumpTo(dataNode, relationsNode, depth), + + stat: + html.tag('span', {class: 'network-tag-stat'}, + language.$(prefix, 'tag.withStat.stat', { + stat: [ + html.tag('span', {class: 'network-tag-direct-uses-stat'}, + dataNode.directUses.toString()), + + html.tag('span', {class: 'network-tag-total-uses-stat'}, + dataNode.totalUses.toString()), + + html.tag('span', {class: 'network-tag-descendants-stat'}, + dataNode.descendants.toString()), + + html.tag('span', {class: 'network-tag-leaves-stat'}, + (dataNode.leaves === null + ? language.$(prefix, 'tag.withStat.notApplicable') + : dataNode.leaves.toString())), + ], + })), + })) + ]; + + const recursive = (dataNode, relationsNode, depth) => [ + html.tag('dt', + { + id: depth === 0 && dataNode.directory, + class: depth % 2 === 0 ? 'even' : 'odd', + }, + + (depth === 0 + ? (relationsNode.ancestorTagLinks + ? language.$(prefix, 'root.withAncestors', { + tag: + wrapTagWithStats(dataNode, relationsNode, depth), + + ancestors: + language.formatUnitList( + stitchArrays({ + link: relationsNode.ancestorTagLinks, + directory: dataNode.ancestorTagDirectories, + }).map(({link, directory}) => + link.slots({ + anchor: true, + hash: directory, + }))), + }) + : language.$(prefix, 'root.jumpToTop', { + tag: + wrapTagWithStats(dataNode, relationsNode, depth), + + link: + html.tag('a', {href: '#top'}, + language.$(prefix, 'root.jumpToTop.link')), + })) + : wrapTagWithStats(dataNode, relationsNode, depth))), + + dataNode.descendantNodes && + relationsNode.descendantNodes && + html.tag('dd', + {class: depth % 2 === 0 ? 'even' : 'odd'}, + html.tag('dl', + stitchArrays({ + dataNode: dataNode.descendantNodes, + relationsNode: relationsNode.descendantNodes, + }).map(({dataNode, relationsNode}) => + recursive(dataNode, relationsNode, depth + 1)))), + ]; + + return relations.page.slots({ + type: 'custom', + + content: [ + html.tag('p', {id: 'network-stat-line'}, + language.$(prefix, 'statLine', { + stat: [ + html.tag('a', {id: 'network-stat-none'}, + {href: '#'}, + language.$(prefix, 'statLine.none')), + + html.tag('a', {id: 'network-stat-total-uses'}, + {href: '#'}, + {style: 'display: none'}, + language.$(prefix, 'statLine.totalUses')), + + html.tag('a', {id: 'network-stat-direct-uses'}, + {href: '#'}, + {style: 'display: none'}, + language.$(prefix, 'statLine.directUses')), + + html.tag('a', {id: 'network-stat-descendants'}, + {href: '#'}, + {style: 'display: none'}, + language.$(prefix, 'statLine.descendants')), + + html.tag('a', {id: 'network-stat-leaves'}, + {href: '#'}, + {style: 'display: none'}, + language.$(prefix, 'statLine.leaves')), + ], + })), + + html.tag('dl', {id: 'network-top-dl'}, [ + html.tag('dt', {id: 'top'}, + language.$(prefix, 'jumpToRoot.title')), + + html.tag('dd', + html.tag('ul', + stitchArrays({ + link: relations.uppermostRootTagLinks, + directory: data.uppermostRootTagDirectories, + }).map(({link, directory}) => + html.tag('li', + language.$(prefix, 'jumpToRoot.item', { + tag: + link.slots({ + anchor: true, + hash: directory, + }), + }))))), + + stitchArrays({ + dataNode: data.rootNodes, + relationsNode: relations.rootNodes, + }).map(({dataNode, relationsNode}) => + recursive(dataNode, relationsNode, 0)), + + !empty(relations.orphanArtTagLinks) && [ + html.tag('dt', + language.$(prefix, 'orphanArtTags.title')), + + html.tag('dd', + html.tag('ul', + relations.orphanArtTagLinks.map(orphanArtTagLink => + html.tag('li', + language.$(prefix, 'orphanArtTags.item', { + tag: orphanArtTagLink, + }))))), + ], + ]), + ], + }); + }, +}; diff --git a/src/content/dependencies/listTagsByName.js b/src/content/dependencies/listArtTagsByName.js index d7022a55..1df9dfff 100644 --- a/src/content/dependencies/listTagsByName.js +++ b/src/content/dependencies/listArtTagsByName.js @@ -1,8 +1,8 @@ import {sortAlphabetically} from '#sort'; -import {stitchArrays} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtTag'], + contentDependencies: ['generateListingPage', 'linkArtTagGallery'], extraDependencies: ['language', 'wikiData'], sprawl({artTagData}) { @@ -16,7 +16,7 @@ export default { artTags: sortAlphabetically( artTagData - .filter(tag => !tag.isContentWarning)), + .filter(artTag => !artTag.isContentWarning)), }; }, @@ -26,15 +26,18 @@ export default { artTagLinks: query.artTags - .map(tag => relation('linkArtTag', tag)), + .map(artTag => relation('linkArtTagGallery', artTag)), }; }, data(query) { return { counts: - query.artTags - .map(tag => tag.taggedInThings.length), + query.artTags.map(artTag => + unique([ + ...artTag.indirectlyFeaturedInArtworks, + ...artTag.directlyFeaturedInArtworks, + ]).length), }; }, diff --git a/src/content/dependencies/listArtTagsByUses.js b/src/content/dependencies/listArtTagsByUses.js new file mode 100644 index 00000000..eca7f1c6 --- /dev/null +++ b/src/content/dependencies/listArtTagsByUses.js @@ -0,0 +1,54 @@ +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays, unique} from '#sugar'; + +export default { + contentDependencies: ['generateListingPage', 'linkArtTagGallery'], + extraDependencies: ['language', 'wikiData'], + + sprawl: ({artTagData}) => + ({artTagData}), + + query({artTagData}, spec) { + const artTags = + sortAlphabetically( + artTagData + .filter(artTag => !artTag.isContentWarning)); + + const counts = + artTags.map(artTag => + unique([ + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, + ]).length); + + filterByCount(artTags, counts); + sortByCount(artTags, counts, {greatestFirst: true}); + + return {spec, artTags, counts}; + }, + + relations: (relation, query) => ({ + page: + relation('generateListingPage', query.spec), + + artTagLinks: + query.artTags + .map(artTag => relation('linkArtTagGallery', artTag)), + }), + + data: (query) => + ({counts: query.counts}), + + generate: (data, relations, {language}) => + relations.page.slots({ + type: 'rows', + rows: + stitchArrays({ + link: relations.artTagLinks, + count: data.counts, + }).map(({link, count}) => ({ + tag: link, + timesUsed: language.countTimesUsed(count, {unit: true}), + })), + }), +}; diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js index 41944959..99f19764 100644 --- a/src/content/dependencies/listArtistsByContributions.js +++ b/src/content/dependencies/listArtistsByContributions.js @@ -1,13 +1,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; - -import { - accumulateSum, - empty, - filterByCount, - filterMultipleArrays, - stitchArrays, - unique, -} from '#sugar'; +import {empty, filterByCount, filterMultipleArrays, stitchArrays} + from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], @@ -41,37 +34,46 @@ export default { query[countsKey] = counts; }; + const countContributions = (artist, keys) => { + const contribs = + keys + .flatMap(key => artist[key]) + .filter(contrib => contrib.countInContributionTotals); + + const things = + new Set(contribs.map(contrib => contrib.thing)); + + return things.size; + }; + queryContributionInfo( 'artistsByTrackContributions', 'countsByTrackContributions', artist => - (unique( - ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - ]).flat() - .map(({thing}) => thing) - )).length); + countContributions(artist, [ + 'trackArtistContributions', + 'trackContributorContributions', + ])); queryContributionInfo( 'artistsByArtworkContributions', 'countsByArtworkContributions', artist => - accumulateSum( - [ - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, - artist.trackCoverArtistContributions, - ], - contribs => contribs.length)); + countContributions(artist, [ + 'albumCoverArtistContributions', + 'albumWallpaperArtistContributions', + 'albumBannerArtistContributions', + 'trackCoverArtistContributions', + ])); if (sprawl.enableFlashesAndGames) { queryContributionInfo( 'artistsByFlashContributions', 'countsByFlashContributions', artist => - artist.flashContributorContributions.length); + countContributions(artist, [ + 'flashContributorContributions', + ])); } return query; diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js index 0bf9dd2d..17096cfc 100644 --- a/src/content/dependencies/listArtistsByGroup.js +++ b/src/content/dependencies/listArtistsByGroup.js @@ -37,20 +37,25 @@ export default { ([ (unique( ([ - artist.albumArtistContributions, - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, + artist.albumArtistContributions + .map(contrib => contrib.thing), + artist.albumCoverArtistContributions + .map(contrib => contrib.thing.thing), + artist.albumWallpaperArtistContributions + .map(contrib => contrib.thing.thing), + artist.albumBannerArtistContributions + .map(contrib => contrib.thing.thing), ]).flat() - .map(({thing}) => thing) )).map(album => album.groups), (unique( ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - artist.trackCoverArtistContributions, + artist.trackArtistContributions + .map(contrib => contrib.thing), + artist.trackContributorContributions + .map(contrib => contrib.thing), + artist.trackCoverArtistContributions + .map(contrib => contrib.thing.thing), ]).flat() - .map(({thing}) => thing) )).map(track => track.album.groups), ]).flat() .map(groups => groups diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js index 27a2faa3..2a8d1b4c 100644 --- a/src/content/dependencies/listArtistsByLatestContribution.js +++ b/src/content/dependencies/listArtistsByLatestContribution.js @@ -98,13 +98,16 @@ export default { ])) { // Might combine later with 'track' of the same album and date. considerDate(artist, album.coverArtDate ?? album.date, album, 'artwork'); + // '?? album.date' is kept here because wallpaper and banner may + // technically be present for an album w/o cover art, therefore + // also no cover art date. } } for (const track of tracksLatestFirst) { for (const artist of getArtists(track, 'coverArtistContribs')) { // No special effect if artist already has 'artwork' for the same album and date. - considerDate(artist, track.coverArtDate ?? track.date, track.album, 'artwork'); + considerDate(artist, track.coverArtDate, track.album, 'artwork'); } for (const artist of new Set([ diff --git a/src/content/dependencies/listGroupsByDuration.js b/src/content/dependencies/listGroupsByDuration.js index da2f26db..c79e1bc4 100644 --- a/src/content/dependencies/listGroupsByDuration.js +++ b/src/content/dependencies/listGroupsByDuration.js @@ -16,7 +16,7 @@ export default { groups.map(group => getTotalDuration( group.albums.flatMap(album => album.tracks), - {originalReleasesOnly: true})); + {mainReleasesOnly: true})); filterByCount(groups, durations); sortByCount(groups, durations, {greatestFirst: true}); diff --git a/src/content/dependencies/listTagsByUses.js b/src/content/dependencies/listTagsByUses.js deleted file mode 100644 index 00c700a5..00000000 --- a/src/content/dependencies/listTagsByUses.js +++ /dev/null @@ -1,59 +0,0 @@ -import {sortAlphabetically, sortByCount} from '#sort'; -import {filterByCount, stitchArrays} from '#sugar'; - -export default { - contentDependencies: ['generateListingPage', 'linkArtTag'], - extraDependencies: ['language', 'wikiData'], - - sprawl({artTagData}) { - return {artTagData}; - }, - - query({artTagData}, spec) { - const artTags = - sortAlphabetically( - artTagData - .filter(tag => !tag.isContentWarning)); - - const counts = - artTags - .map(tag => tag.taggedInThings.length); - - filterByCount(artTags, counts); - sortByCount(artTags, counts, {greatestFirst: true}); - - return {spec, artTags, counts}; - }, - - relations(relation, query) { - return { - page: relation('generateListingPage', query.spec), - - artTagLinks: - query.artTags - .map(tag => relation('linkArtTag', tag)), - }; - }, - - data(query) { - return { - counts: - query.artTags - .map(tag => tag.taggedInThings.length), - }; - }, - - generate(data, relations, {language}) { - return relations.page.slots({ - type: 'rows', - rows: - stitchArrays({ - link: relations.artTagLinks, - count: data.counts, - }).map(({link, count}) => ({ - tag: link, - timesUsed: language.countTimesUsed(count, {unit: true}), - })), - }); - }, -}; diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js index 0a2bfd6c..dcfaeaf0 100644 --- a/src/content/dependencies/listTracksByDate.js +++ b/src/content/dependencies/listTracksByDate.js @@ -5,49 +5,54 @@ export default { contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], extraDependencies: ['language', 'wikiData'], - sprawl({trackData}) { - return {trackData}; - }, + sprawl: ({trackData}) => ({trackData}), query({trackData}, spec) { - return { - spec, + const query = {spec}; + + query.tracks = + sortAlbumsTracksChronologically( + trackData.filter(track => track.date)); + + query.chunks = + chunkByProperties(query.tracks, ['album', 'date']); - chunks: - chunkByProperties( - sortAlbumsTracksChronologically( - trackData.filter(track => track.date)), - ['album', 'date']), - }; + return query; }, - relations(relation, query) { - return { - page: relation('generateListingPage', query.spec), + relations: (relation, query) => ({ + page: + relation('generateListingPage', query.spec), - albumLinks: - query.chunks - .map(({album}) => relation('linkAlbum', album)), + albumLinks: + query.chunks + .map(({album}) => relation('linkAlbum', album)), - trackLinks: - query.chunks - .map(({chunk}) => chunk - .map(track => relation('linkTrack', track))), - }; - }, + trackLinks: + query.chunks + .map(({chunk}) => chunk + .map(track => relation('linkTrack', track))), + }), - data(query) { - return { - dates: - query.chunks - .map(({date}) => date), + data: (query) => ({ + dates: + query.chunks + .map(({date}) => date), - rereleases: - query.chunks.map(({chunk}) => - chunk.map(track => - track.originalReleaseTrack !== null)), - }; - }, + rereleases: + query.chunks + .map(({chunk}) => chunk + .map(track => + // Check if the index of this track... + query.tracks.indexOf(track) > + // ...is greater than the *smallest* index + // of any of this track's *other* releases. + // (It won't be greater than its own index, + // so we can use otherReleases here, rather + // than allReleases.) + Math.min(... + track.otherReleases.map(t => query.tracks.indexOf(t))))), + }), generate(data, relations, {language}) { return relations.page.slots({ @@ -79,7 +84,7 @@ export default { data.rereleases.map(rereleases => rereleases.map(rerelease => (rerelease - ? {class: 'rerelease'} + ? {class: 'rerelease-line'} : null))), }); }, diff --git a/src/content/dependencies/listTracksWithLyrics.js b/src/content/dependencies/listTracksWithLyrics.js index a13a76f0..e6ab9d7d 100644 --- a/src/content/dependencies/listTracksWithLyrics.js +++ b/src/content/dependencies/listTracksWithLyrics.js @@ -2,7 +2,7 @@ export default { contentDependencies: ['listTracksWithExtra'], relations: (relation, spec) => - ({page: relation('listTracksWithExtra', spec, 'lyrics', 'truthy')}), + ({page: relation('listTracksWithExtra', spec, 'lyrics', 'array')}), generate: (relations) => relations.page, diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 5f803a3b..e9a75744 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -1,11 +1,30 @@ +import {basename} from 'node:path'; + +import {logWarn} from '#cli'; import {bindFind} from '#find'; -import {replacerSpec, parseInput} from '#replacer'; +import {replacerSpec, parseContentNodes} from '#replacer'; import {Marked} from 'marked'; +import striptags from 'striptags'; const commonMarkedOptions = { headerIds: false, mangle: false, + + tokenizer: { + url(src) { + // Don't link emails + const cap = this.rules.inline.url.exec(src); + if (cap?.[2] === '@') return; + + // Use normal tokenizer url behavior otherwise + // Note that super.url doesn't work here because marked is binding or + // applying this function on the tokenizer instance - super.prop would + // just read the prototype of the containing object literal, not the + // rebound tokenizer. (Thanks MDN.) + return Object.getPrototypeOf(this).url.call(this, src); + }, + }, }; const multilineMarked = new Marked({ @@ -30,24 +49,44 @@ function getPlaceholder(node, content) { return {type: 'text', data: content.slice(node.i, node.iEnd)}; } +function getArg(node, argKey) { + return ( + node.data.args + ?.find(({key}) => key.data === argKey) + ?.value ?? + null); +} + export default { contentDependencies: [ ...( Object.values(replacerSpec) .map(description => description.link) .filter(Boolean)), + 'image', + 'generateTextWithTooltip', + 'generateTooltip', 'linkExternal', ], - extraDependencies: ['html', 'language', 'to', 'wikiData'], + extraDependencies: [ + 'html', + 'language', + 'niceShowAggregate', + 'to', + 'wikiData', + ], sprawl(wikiData, content) { - const find = bindFind(wikiData); + const find = bindFind(wikiData, {mode: 'quiet'}); - const parsedNodes = parseInput(content ?? ''); + const {result: parsedNodes, error} = + parseContentNodes(content ?? '', {errorMode: 'return'}); return { + error, + nodes: parsedNodes .map(node => { if (node.type !== 'tag') { @@ -118,6 +157,30 @@ export default { return {i: node.i, iEnd: node.iEnd, type: 'internal-link', data}; } + if (replacerKey === 'tooltip') { + // TODO: Again, no recursive nodes. Sorry! + // const enteredLabel = node.data.label && transformNode(node.data.label, opts); + const enteredLabel = node.data.label?.data; + + return { + i: node.i, + iEnd: node.iEnd, + type: 'tooltip', + data: { + tooltip: + replacerValue ?? '(empty tooltip...)', + + label: + enteredLabel ?? '(tooltip without label)', + + link: + (getArg(node, 'link') + ? getArg(node, 'link')[0].data + : null), + }, + }; + } + // This will be another {type: 'tag'} node which gets processed in // generate. Extract replacerKey and replacerValue now, since it'd // be a pain to deal with later. @@ -137,6 +200,9 @@ export default { return { content, + error: + sprawl.error, + nodes: sprawl.nodes .map(node => { @@ -169,10 +235,18 @@ export default { link: relation(name, arg), label: node.data.label, hash: node.data.hash, + name: arg?.name, + shortName: arg?.shortName ?? arg?.nameShort, } : getPlaceholder(node, content)); return { + textWithTooltip: + relation('generateTextWithTooltip'), + + tooltip: + relation('generateTooltip'), + internalLinks: nodes .filter(({type}) => type === 'internal-link') @@ -191,11 +265,15 @@ export default { externalLinks: nodes .filter(({type}) => type === 'external-link') - .map(node => { - const {href} = node.data; + .map(({data: {href}}) => + relation('linkExternal', href)), - return relation('linkExternal', href); - }), + externalLinksForTooltipNodes: + nodes + .filter(({type}) => type === 'tooltip') + .filter(({data}) => data.link) + .map(({data: {link: href}}) => + relation('linkExternal', href)), images: nodes @@ -221,22 +299,61 @@ export default { default: true, }, + absorbPunctuationFollowingExternalLinks: { + type: 'boolean', + default: true, + }, + + textOnly: { + type: 'boolean', + default: false, + }, + thumb: { validate: v => v.is('small', 'medium', 'large'), default: 'large', }, }, - generate(data, relations, slots, {html, language, to}) { + generate(data, relations, slots, {html, language, niceShowAggregate, to}) { + if (data.error) { + logWarn`Error in content text.`; + niceShowAggregate(data.error); + } + let imageIndex = 0; let internalLinkIndex = 0; let externalLinkIndex = 0; + let externalLinkForTooltipNodeIndex = 0; + + let offsetTextNode = 0; const contentFromNodes = - data.nodes.map(node => { + data.nodes.map((node, index) => { + const nextNode = data.nodes[index + 1]; + + const absorbFollowingPunctuation = template => { + if (nextNode?.type !== 'text') { + return; + } + + const text = nextNode.data; + const match = text.match(/^[.,;:?!…]+(?=[^\n]*[a-z])/i); + const suffix = match?.[0]; + if (suffix) { + template.setSlot('suffixNormalContent', suffix); + offsetTextNode = suffix.length; + } + }; + switch (node.type) { - case 'text': - return {type: 'text', data: node.data}; + case 'text': { + const text = node.data.slice(offsetTextNode); + + offsetTextNode = 0; + + return {type: 'text', data: text}; + } case 'image': { const src = @@ -262,9 +379,8 @@ export default { height && {height}, style && {style}, - align === 'center' && - !link && - {class: 'align-center'}, + align && !link && + {class: 'align-' + align}, pixelate && {class: 'pixelate'}); @@ -275,8 +391,8 @@ export default { {href: link}, {target: '_blank'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, {title: language.encapsulate('misc.external.opensInNewTab', capsule => @@ -326,20 +442,115 @@ export default { inline: false, data: html.tag('div', {class: 'content-image-container'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, image), }; } + case 'video': { + const src = + (node.src.startsWith('media/') + ? to('media.path', node.src.slice('media/'.length)) + : node.src); + + const {width, height, align, inline, pixelate} = node; + + const video = + html.tag('video', + src && {src}, + width && {width}, + height && {height}, + + {controls: true}, + + align && inline && + {class: 'align-' + align}, + + pixelate && + {class: 'pixelate'}); + + const content = + (inline + ? video + : html.tag('div', {class: 'content-video-container'}, + align && + {class: 'align-' + align}, + + video)); + + + return { + type: 'processed-video', + data: content, + }; + } + + case 'audio': { + const src = + (node.src.startsWith('media/') + ? to('media.path', node.src.slice('media/'.length)) + : node.src); + + const {align, inline, nameless} = node; + + const audio = + html.tag('audio', + src && {src}, + + align && inline && + {class: 'align-' + align}, + + {controls: true}); + + const content = + (inline + ? audio + : html.tag('div', {class: 'content-audio-container'}, + align && + {class: 'align-' + align}, + + [ + !nameless && + html.tag('a', {class: 'filename'}, + src && {href: src}, + language.sanitize(basename(node.src))), + + audio, + ])); + + return { + type: 'processed-audio', + data: content, + }; + } + case 'internal-link': { const nodeFromRelations = relations.internalLinks[internalLinkIndex++]; if (nodeFromRelations.type === 'text') { return {type: 'text', data: nodeFromRelations.data}; } - const {link, label, hash} = nodeFromRelations; + // TODO: This is a bit hacky, like the stuff below, + // but since we dressed it up in a utility function + // maybe it's okay... + const link = + html.resolve( + nodeFromRelations.link, + {slots: ['content', 'hash']}); + + const {label, hash, shortName, name} = nodeFromRelations; + + if (slots.textOnly) { + if (label) { + return {type: 'text', data: label}; + } else if (slots.preferShortLinkNames) { + return {type: 'text', data: shortName ?? name}; + } else { + return {type: 'text', data: name}; + } + } // These are removed from the typical combined slots({})-style // because we don't want to override slots that were already set @@ -353,7 +564,7 @@ export default { try { link.getSlotDescription('preferShortName'); hasPreferShortNameSlot = true; - } catch (error) { + } catch { hasPreferShortNameSlot = false; } @@ -366,7 +577,7 @@ export default { try { link.getSlotDescription('tooltipStyle'); hasTooltipStyleSlot = true; - } catch (error) { + } catch { hasTooltipStyleSlot = false; } @@ -374,6 +585,18 @@ export default { link.setSlot('tooltipStyle', 'none'); } + let doTheAbsorbyThing = false; + + // TODO: This is just silly. + try { + const tag = html.resolve(link, {normalize: 'tag'}); + doTheAbsorbyThing ||= tag.attributes.has('class', 'image-media-link'); + } catch {} + + if (doTheAbsorbyThing) { + absorbFollowingPunctuation(link); + } + return {type: 'processed-internal-link', data: link}; } @@ -381,11 +604,19 @@ export default { const {label} = node.data; const externalLink = relations.externalLinks[externalLinkIndex++]; + if (slots.textOnly) { + return {type: 'text', data: label}; + } + externalLink.setSlots({ content: label, fromContent: true, }); + if (slots.absorbPunctuationFollowingExternalLinks) { + absorbFollowingPunctuation(externalLink); + } + if (slots.indicateExternalLinks) { externalLink.setSlots({ indicateExternal: true, @@ -397,6 +628,52 @@ export default { return {type: 'processed-external-link', data: externalLink}; } + case 'tooltip': { + const {label, link, tooltip: tooltipContent} = node.data; + + const externalLink = + (link + ? relations.externalLinksForTooltipNodes + .at(externalLinkForTooltipNodeIndex++) + : null); + + if (externalLink) { + externalLink.setSlots({ + content: label, + fromContent: true, + }); + + if (slots.indicateExternalLinks) { + externalLink.setSlots({ + indicateExternal: true, + disableBrowserTooltip: true, + tab: 'separate', + style: 'platform', + }); + } + } + + const textWithTooltip = relations.textWithTooltip.clone(); + const tooltip = relations.tooltip.clone(); + + tooltip.setSlots({ + attributes: {class: 'content-tooltip'}, + content: tooltipContent, // Not sanitized! + }); + + textWithTooltip.setSlots({ + attributes: [ + {class: 'content-tooltip-guy'}, + externalLink && {class: 'has-link'}, + ], + + text: externalLink ?? label, + tooltip, + }); + + return {type: 'processed-tooltip', data: textWithTooltip}; + } + case 'tag': { const {replacerKey, replacerValue} = node.data; @@ -413,12 +690,19 @@ export default { ? valueFn(replacerValue) : replacerValue); - const contents = + const content = (htmlFn ? htmlFn(value, {html, language}) : value); - return {type: 'text', data: contents.toString()}; + const contentText = + html.resolve(content, {normalize: 'string'}); + + if (slots.textOnly) { + return {type: 'text', data: striptags(contentText)}; + } else { + return {type: 'text', data: contentText}; + } } default: @@ -494,15 +778,19 @@ export default { const attributes = html.parseAttributes(match[1]); - // Images that were all on their own line need to be removed from - // the surrounding <p> tag that marked generates. The HTML parser - // treats a <div> that starts inside a <p> as a Crocker-class - // misgiving, and will treat you very badly if you feed it that. - if (attributes.get('data-type') === 'processed-image') { - if (!attributes.get('data-inline')) { - tags[tags.length - 1] = tags[tags.length - 1].replace(/<p>$/, ''); - deleteParagraph = true; - } + // Images (or videos) that were all on their own line need to be + // removed from the surrounding <p> tag that marked generates. + // The HTML parser treats a <div> that starts inside a <p> as a + // Crocker-class misgiving, and will treat you very badly if you + // feed it that. + if ( + (attributes.get('data-type') === 'processed-image' && + !attributes.get('data-inline')) || + attributes.get('data-type') === 'processed-video' || + attributes.get('data-type') === 'processed-audio' + ) { + tags[tags.length - 1] = tags[tags.length - 1].replace(/<p>$/, ''); + deleteParagraph = true; } const nonTextNodeIndex = match[2]; @@ -542,9 +830,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(/(?<!^ *(?:-|\d\.).*|^>.*|^ .*\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(/(?<=^ *(?:-|\d\.).*)\n+(?!^ *(?:-|\d\.))/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'); @@ -576,25 +864,12 @@ export default { const markedInput = extractNonTextNodes({ - getTextNodeContents(node, index) { - // First, replace line breaks that follow text content with - // <br> tags. - let content = node.data.replace(/(?!^)\n/gm, '<br>\n'); - - // Scrap line breaks that are at the end of a verse. - content = content.replace(/<br>$(?=\n\n)/gm, ''); - - // If the node started with a line break, and it's not the - // very first node, then whatever came before it was inline. - // (This is an assumption based on text links being basically - // the only tag that shows up in lyrics.) Since this text is - // following content that was already inline, restore that - // initial line break. - if (node.data[0] === '\n' && index !== 0) { - content = '<br>' + content; - } - - return content; + getTextNodeContents(node) { + // Just insert <br> before every line break. The resulting + // text will appear all in one paragraph - this is expected + // for lyrics, and allows for multiple lines of proportional + // space between stanzas. + return node.data.replace(/\n/g, '<br>\n'); }, }); |