diff options
Diffstat (limited to 'src/content/dependencies')
231 files changed, 13473 insertions, 6840 deletions
diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js index 92948c7a..7e05b5b5 100644 --- a/src/content/dependencies/generateAdditionalFilesList.js +++ b/src/content/dependencies/generateAdditionalFilesList.js @@ -1,97 +1,22 @@ -import {empty} from '#sugar'; - -function validateFileMapping(v, validateValue) { - return value => { - v.isObject(value); - - const valueErrors = []; - for (const [fileKey, fileValue] of Object.entries(value)) { - if (fileValue === null) { - continue; - } - - try { - validateValue(fileValue); - } catch (error) { - error.message = `(${fileKey}) ` + error.message; - valueErrors.push(error); - } - } - - if (!empty(valueErrors)) { - throw new AggregateError(valueErrors, `Errors validating values`); - } - }; -} - export default { - extraDependencies: ['html', 'language'], + contentDependencies: ['generateAdditionalFilesListChunk'], + extraDependencies: ['html'], - data(additionalFiles) { - return { - // Additional files are already a serializable format. - additionalFiles, - }; - }, + relations: (relation, additionalFiles) => ({ + chunks: + additionalFiles + .map(file => relation('generateAdditionalFilesListChunk', file)), + }), slots: { - fileLinks: { - validate: v => validateFileMapping(v, v.isHTML), - }, - - fileSizes: { - validate: v => validateFileMapping(v, v.isWholeNumber), - }, + showFileSizes: {type: 'boolean', default: true}, }, - generate(data, slots, {html, language}) { - if (!slots.fileLinks) { - return html.blank(); - } + generate: (relations, slots, {html}) => + html.tag('ul', {class: 'additional-files-list'}, + {[html.onlyIfContent]: true}, - const filesWithLinks = new Set( - Object.entries(slots.fileLinks) - .filter(([key, value]) => value) - .map(([key]) => key)); - - if (empty(filesWithLinks)) { - return html.blank(); - } - - const filteredFileGroups = data.additionalFiles - .map(({title, description, files}) => ({ - title, - description, - files: files.filter(f => filesWithLinks.has(f)), - })) - .filter(({files}) => !empty(files)); - - if (empty(filteredFileGroups)) { - return html.blank(); - } - - return html.tag('dl', - filteredFileGroups.flatMap(({title, description, files}) => [ - html.tag('dt', - (description - ? language.$('releaseInfo.additionalFiles.entry.withDescription', { - title, - description, - }) - : language.$('releaseInfo.additionalFiles.entry', {title}))), - - html.tag('dd', - html.tag('ul', - files.map(file => - html.tag('li', - (slots.fileSizes?.[file] - ? language.$('releaseInfo.additionalFiles.file.withSize', { - file: slots.fileLinks[file], - size: language.formatFileSize(slots.fileSizes[file]), - }) - : language.$('releaseInfo.additionalFiles.file', { - file: slots.fileLinks[file], - })))))), - ])); - }, + relations.chunks.map(chunk => chunk.slots({ + showFileSizes: slots.showFileSizes, + }))), }; diff --git a/src/content/dependencies/generateAdditionalFilesListChunk.js b/src/content/dependencies/generateAdditionalFilesListChunk.js new file mode 100644 index 00000000..3cac851b --- /dev/null +++ b/src/content/dependencies/generateAdditionalFilesListChunk.js @@ -0,0 +1,81 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['linkAdditionalFile', 'transformContent'], + extraDependencies: ['getSizeOfMediaFile', 'html', 'language', 'urls'], + + relations: (relation, file) => ({ + description: + relation('transformContent', file.description), + + links: + file.filenames + .map(filename => relation('linkAdditionalFile', file, filename)), + }), + + data: (file) => ({ + title: + file.title, + + paths: + file.paths, + }), + + slots: { + showFileSizes: { + type: 'boolean', + }, + }, + + generate: (data, relations, slots, {getSizeOfMediaFile, html, language, urls}) => + language.encapsulate('releaseInfo.additionalFiles', capsule => + html.tag('li', + html.tag('details', + html.isBlank(relations.links) && + {open: true}, + + [ + html.tag('summary', + html.tag('span', + language.$(capsule, 'entry', { + title: + html.tag('b', data.title), + }))), + + html.tag('ul', [ + html.tag('li', {class: 'entry-description'}, + {[html.onlyIfContent]: true}, + + relations.description.slot('mode', 'inline')), + + (html.isBlank(relations.links) + ? html.tag('li', + 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/generateAdditionalFilesShortcut.js b/src/content/dependencies/generateAdditionalFilesShortcut.js deleted file mode 100644 index 9e119bce..00000000 --- a/src/content/dependencies/generateAdditionalFilesShortcut.js +++ /dev/null @@ -1,27 +0,0 @@ -import {empty} from '#sugar'; - -export default { - extraDependencies: ['html', 'language'], - - data(additionalFiles) { - return { - titles: additionalFiles.map(fileGroup => fileGroup.title), - }; - }, - - generate(data, {html, language}) { - if (empty(data.titles)) { - return html.blank(); - } - - return language.$('releaseInfo.additionalFiles.shortcut', { - anchorLink: - html.tag('a', - {href: '#additional-files'}, - language.$('releaseInfo.additionalFiles.shortcut.anchorLink')), - - titles: - language.formatUnitList(data.titles), - }); - }, -} diff --git a/src/content/dependencies/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 23f32bf5..00000000 --- a/src/content/dependencies/generateAlbumAdditionalFilesList.js +++ /dev/null @@ -1,59 +0,0 @@ -export default { - contentDependencies: [ - 'generateAdditionalFilesList', - 'linkAlbumAdditionalFile', - ], - - extraDependencies: [ - 'getSizeOfAdditionalFile', - 'html', - 'urls', - ], - - data(album, additionalFiles) { - return { - albumDirectory: album.directory, - fileLocations: additionalFiles.flatMap(({files}) => files), - }; - }, - - relations(relation, album, additionalFiles) { - return { - additionalFilesList: - relation('generateAdditionalFilesList', additionalFiles), - - additionalFileLinks: - Object.fromEntries( - additionalFiles - .flatMap(({files}) => files) - .map(file => [ - file, - relation('linkAlbumAdditionalFile', album, file), - ])), - }; - }, - - slots: { - showFileSizes: {type: 'boolean', default: true}, - }, - - generate(data, relations, slots, { - getSizeOfAdditionalFile, - urls, - }) { - return relations.additionalFilesList - .slots({ - fileLinks: relations.additionalFileLinks, - fileSizes: - Object.fromEntries(data.fileLocations.map(file => [ - file, - (slots.showFileSizes - ? getSizeOfAdditionalFile( - urls - .from('media.root') - .to('media.albumAdditionalFile', data.albumDirectory, file)) - : 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 5a7142e5..3529c4dc 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -2,13 +2,13 @@ import {empty, stitchArrays} from '#sugar'; export default { contentDependencies: [ - 'generateAlbumCoverArtwork', + 'generateAlbumCommentarySidebar', 'generateAlbumNavAccent', - 'generateAlbumSidebarTrackSection', - 'generateAlbumStyleRules', + 'generateAlbumSecondaryNav', + 'generateAlbumStyleTags', 'generateCommentaryEntry', 'generateContentHeading', - 'generateTrackCoverArtwork', + 'generateCoverArtwork', 'generatePageLayout', 'linkAlbum', 'linkExternal', @@ -17,14 +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.albumStyleRules = - relation('generateAlbumStyleRules', album, null); + relations.secondaryNav = + relation('generateAlbumSecondaryNav', album); + + relations.sidebar = + relation('generateAlbumCommentarySidebar', album); + + relations.albumStyleTags = + relation('generateAlbumStyleTags', album, null); relations.albumLink = relation('linkAlbum', album); @@ -32,7 +53,7 @@ export default { relations.albumNavAccent = relation('generateAlbumNavAccent', album, null); - if (album.commentary) { + if (!empty(album.commentary)) { relations.albumCommentaryHeading = relation('generateContentHeading'); @@ -44,7 +65,7 @@ export default { if (album.hasCoverArt) { relations.albumCommentaryCover = - relation('generateAlbumCoverArtwork', album); + relation('generateCoverArtwork', album.coverArtworks[0]); } relations.albumCommentaryEntries = @@ -52,80 +73,65 @@ 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))); - relations.sidebarAlbumLink = - relation('linkAlbum', album); - - relations.sidebarTrackSections = - album.trackSections.map(trackSection => - relation('generateAlbumSidebarTrackSection', album, null, trackSection)); - 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 @@ -134,60 +140,90 @@ export default { return data; }, - generate(data, relations, {html, language}) { - return relations.layout - .slots({ + generate: (data, relations, {html, language}) => + language.encapsulate('albumCommentaryPage', pageCapsule => + relations.layout.slots({ title: - language.$('albumCommentaryPage.title', { + language.$(pageCapsule, 'title', { album: data.name, }), headingMode: 'sticky', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['long-content'], mainContent: [ html.tag('p', - language.$('albumCommentaryPage.infoLine', { - words: - html.tag('b', - language.formatWordCount(data.wordCount, {unit: true})), - - entries: - html.tag('b', - language.countCommentaryEntries(data.entryCount, {unit: true})), - })), - - relations.albumCommentaryEntries && [ - relations.albumCommentaryHeading.slots({ - tag: 'h3', - color: data.color, - - title: - language.$('albumCommentaryPage.entry.title.albumCommentary', { - album: relations.albumCommentaryLink, + {[html.joinChildren]: html.tag('br')}, + + [ + data.date && + data.entryCount >= 1 && + language.$('releaseInfo.albumReleased', { + date: + html.tag('b', + language.formatDate(data.date)), }), - accent: - !empty(relations.albumCommentaryListeningLinks) && - language.$('albumCommentaryPage.entry.title.albumCommentary.accent', { - listeningLinks: - language.formatUnitList( - relations.albumCommentaryListeningLinks - .map(link => link.slots({ - context: 'album', - tab: 'separate', - }))), - }), - }), - - relations.albumCommentaryCover - ?.slots({mode: 'commentary'}), - - relations.albumCommentaryEntries, - ], + 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: + language.$(titleCapsule, { + album: relations.albumCommentaryLink, + }), + + stickyTitle: + language.$(titleCapsule, 'sticky', { + album: data.name, + }), + + accent: + language.$(titleCapsule, 'accent', { + [language.onlyIfOptions]: ['listeningLinks'], + listeningLinks: + language.formatUnitList( + relations.albumCommentaryListeningLinks + .map(link => link.slots({ + context: 'album', + tab: 'separate', + }))), + }), + })), + + relations.albumCommentaryCover + ?.slots({mode: 'commentary'}), + + relations.albumCommentaryEntries, + ]), stitchArrays({ heading: relations.trackCommentaryHeadings, @@ -197,6 +233,7 @@ export default { cover: relations.trackCommentaryCovers, entries: relations.trackCommentaryEntries, color: data.trackCommentaryColors, + trackDate: data.trackCommentaryTrackDates, }).map(({ heading, link, @@ -205,31 +242,44 @@ export default { cover, entries, color, - }) => [ - heading.slots({ - tag: 'h3', - id: directory, - color, - - title: - language.$('albumCommentaryPage.entry.title.trackCommentary', { - track: link, - }), - - accent: - !empty(listeningLinks) && - language.$('albumCommentaryPage.entry.title.trackCommentary.accent', { - listeningLinks: - language.formatUnitList( - listeningLinks.map(link => - link.slot('tab', 'separate'))), - }), + trackDate, + }) => + language.encapsulate(pageCapsule, 'entry', entryCapsule => [ + language.encapsulate(entryCapsule, 'title.trackCommentary', titleCapsule => + heading.slots({ + tag: 'h3', + attributes: {id: directory}, + color, + + title: + language.$(titleCapsule, { + track: link, + }), + + accent: + language.$(titleCapsule, 'accent', { + [language.onlyIfOptions]: ['listeningLinks'], + listeningLinks: + language.formatUnitList( + listeningLinks.map(link => + link.slot('tab', 'separate'))), + }), + })), + + cover?.slots({ + mode: 'commentary', + color: true, }), - cover?.slots({mode: 'commentary'}), + trackDate && + trackDate !== data.date && + html.tag('p', {class: 'track-info'}, + language.$('releaseInfo.trackReleased', { + date: language.formatDate(trackDate), + })), entries.map(entry => entry.slot('color', color)), - ]), + ])), ], navLinkStyle: 'hierarchical', @@ -249,17 +299,11 @@ export default { }, ], - leftSidebarStickyMode: 'column', - leftSidebarClass: 'commentary-track-list-sidebar-box', - leftSidebarContent: [ - html.tag('h1', relations.sidebarAlbumLink), - relations.sidebarTrackSections.map(section => - section.slots({ - anchor: true, - open: true, - mode: 'commentary', - })), - ], - }); - }, + secondaryNav: + relations.secondaryNav.slots({ + alwaysVisible: true, + }), + + leftSidebar: relations.sidebar, + })), }; diff --git a/src/content/dependencies/generateAlbumCommentarySidebar.js b/src/content/dependencies/generateAlbumCommentarySidebar.js new file mode 100644 index 00000000..9ecec66d --- /dev/null +++ b/src/content/dependencies/generateAlbumCommentarySidebar.js @@ -0,0 +1,73 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generateAlbumSidebarTrackSection', + 'generatePageSidebar', + 'generatePageSidebarBox', + 'linkAlbum', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, album) => ({ + sidebar: + relation('generatePageSidebar'), + + sidebarBox: + relation('generatePageSidebarBox'), + + albumLink: + relation('linkAlbum', album), + + trackSections: + album.trackSections.map(trackSection => + relation('generateAlbumSidebarTrackSection', + album, + null, + trackSection)), + }), + + 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 ce8cde21..00000000 --- a/src/content/dependencies/generateAlbumCoverArtwork.js +++ /dev/null @@ -1,22 +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, - }), - - generate: (data, relations) => - relations.coverArtwork.slots({ - path: data.path, - color: data.color, - }), -}; 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 f61b1983..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,176 +20,130 @@ 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.who)); - - 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(({who: 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(({who: artist}) => artist.name); - } + albumGrid: + relation('generateAlbumGalleryAlbumGrid', album), - return null; - }), - ]; + trackGrids: + query.recurringTrackArtworkLabels.map(label => + relation('generateAlbumGalleryTrackGrid', album, label)), + }), - data.paths = [ - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null), + data: (query, album) => ({ + trackGridLabels: + query.recurringTrackArtworkLabels, - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : null)), - ]; + trackGridIDs: + query.recurringTrackArtworkLabels.map(label => + 'track-grid-' + + (label + ? getKebabCase(label) + : 'no-label')), - return data; - }, + name: + album.name, + + color: + album.color, + }), - generate(data, relations, {language}) { - return relations.layout - .slots({ + generate: (data, relations, {html, language}) => + language.encapsulate('albumGalleryPage', pageCapsule => + relations.layout.slots({ title: - language.$('albumGalleryPage.title', { + language.$(pageCapsule, 'title', { album: data.name, }), 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, - name: data.names, - }).map(({image, path, name}) => - image.slots({ - path, - 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', @@ -209,6 +163,5 @@ export default { ], secondaryNav: relations.secondaryNav, - }); - }, + })), }; 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 5853f115..1664c788 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -1,176 +1,119 @@ -import {sortAlbumsTracksChronologically} from '#sort'; import {empty} from '#sugar'; -import getChronologyRelations from '../util/getChronologyRelations.js'; - export default { contentDependencies: [ - 'generateAdditionalFilesShortcut', - 'generateAlbumAdditionalFilesList', + 'generateAdditionalFilesList', + 'generateAdditionalNamesBox', + 'generateAlbumArtworkColumn', 'generateAlbumBanner', - 'generateAlbumCoverArtwork', 'generateAlbumNavAccent', 'generateAlbumReleaseInfo', 'generateAlbumSecondaryNav', 'generateAlbumSidebar', 'generateAlbumSocialEmbed', - 'generateAlbumStyleRules', + 'generateAlbumStyleTags', 'generateAlbumTrackList', - 'generateChronologyLinks', - 'generateCommentarySection', + 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generatePageLayout', - 'linkAlbum', 'linkAlbumCommentary', 'linkAlbumGallery', - 'linkArtist', - 'linkTrack', - 'transformContent', ], extraDependencies: ['html', 'language'], - relations(relation, album) { - const relations = {}; - const sections = relations.sections = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); - - relations.socialEmbed = - relation('generateAlbumSocialEmbed', album); - - relations.coverArtistChronologyContributions = - getChronologyRelations(album, { - contributions: album.coverArtistContribs ?? [], - - linkArtist: artist => relation('linkArtist', artist), - - linkThing: trackOrAlbum => - (trackOrAlbum.album - ? relation('linkTrack', trackOrAlbum) - : relation('linkAlbum', trackOrAlbum)), - - getThings(artist) { - const getDate = thing => thing.coverArtDate ?? thing.date; - - const things = [ - ...artist.albumsAsCoverArtist, - ...artist.tracksAsCoverArtist, - ].filter(getDate); - - return sortAlbumsTracksChronologically(things, {getDate}); - }, - }); - - relations.albumNavAccent = - relation('generateAlbumNavAccent', album, null); - - relations.chronologyLinks = - relation('generateChronologyLinks'); - - relations.secondaryNav = - relation('generateAlbumSecondaryNav', album); - - relations.sidebar = - relation('generateAlbumSidebar', album, null); - - if (album.hasCoverArt) { - relations.cover = - relation('generateAlbumCoverArtwork', album); - } + relations: (relation, album) => ({ + layout: + relation('generatePageLayout'), - if (album.hasBannerArt) { - relations.banner = - relation('generateAlbumBanner', album); - } + albumStyleTags: + relation('generateAlbumStyleTags', album, null), - // Section: Release info + socialEmbed: + relation('generateAlbumSocialEmbed', album), - relations.releaseInfo = - relation('generateAlbumReleaseInfo', album); + albumNavAccent: + relation('generateAlbumNavAccent', album, null), - // Section: Extra links + secondaryNav: + relation('generateAlbumSecondaryNav', album), - const extra = sections.extra = {}; + sidebar: + relation('generateAlbumSidebar', album, null), - if (album.tracks.some(t => t.hasUniqueCoverArt)) { - extra.galleryLink = - relation('linkAlbumGallery', album); - } + additionalNamesBox: + relation('generateAdditionalNamesBox', album.additionalNames), - if (album.commentary || album.tracks.some(t => t.commentary)) { - extra.commentaryLink = - relation('linkAlbumCommentary', album); - } + artworkColumn: + relation('generateAlbumArtworkColumn', album), - if (!empty(album.additionalFiles)) { - extra.additionalFilesShortcut = - relation('generateAdditionalFilesShortcut', album.additionalFiles); - } + banner: + (album.hasBannerArt + ? relation('generateAlbumBanner', album) + : null), - // Section: Track list + contentHeading: + relation('generateContentHeading'), - relations.trackList = - relation('generateAlbumTrackList', album); + contentContentHeading: + relation('generateContentContentHeading', album), - // Section: Additional files + releaseInfo: + relation('generateAlbumReleaseInfo', album), - if (!empty(album.additionalFiles)) { - const additionalFiles = sections.additionalFiles = {}; + galleryLink: + (album.tracks.some(t => t.hasUniqueCoverArt) + ? relation('linkAlbumGallery', album) + : null), - additionalFiles.heading = - relation('generateContentHeading'); + commentaryLink: + ([album, ...album.tracks].some(({commentary}) => !empty(commentary)) + ? relation('linkAlbumCommentary', album) + : null), - additionalFiles.additionalFilesList = - relation('generateAlbumAdditionalFilesList', album, album.additionalFiles); - } + trackList: + relation('generateAlbumTrackList', album), - // Section: Artist commentary + additionalFilesList: + relation('generateAdditionalFilesList', album.additionalFiles), - if (album.commentary) { - sections.artistCommentary = - relation('generateCommentarySection', album.commentary); - } + artistCommentaryEntries: + album.commentary + .map(entry => relation('generateCommentaryEntry', entry)), - return relations; - }, + creditSourceEntries: + album.creditingSources + .map(entry => relation('generateCommentaryEntry', entry)), + }), - data(album) { - const data = {}; + data: (album) => ({ + name: + album.name, - data.name = album.name; - data.color = album.color; + color: + album.color, - if (!empty(album.additionalFiles)) { - data.numAdditionalFiles = album.additionalFiles.length; - } + dateAddedToWiki: + album.dateAddedToWiki, + }), - data.dateAddedToWiki = album.dateAddedToWiki; - - return data; - }, - - generate(data, relations, {html, language}) { - const {sections: sec} = relations; + generate: (data, relations, {html, language}) => + language.encapsulate('albumPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + album: data.name, + }), - return relations.layout - .slots({ - title: language.$('albumPage.title', {album: data.name}), + color: data.color, headingMode: 'sticky', + styleTags: relations.albumStyleTags, - color: data.color, - styleRules: [relations.albumStyleRules], + additionalNames: relations.additionalNamesBox, - cover: - relations.cover - ?.slots({ - alt: language.$('misc.alt.albumCover'), - }) - ?? null, + artworkColumnContent: + relations.artworkColumn, mainContent: [ relations.releaseInfo, @@ -179,33 +122,53 @@ export default { {[html.onlyIfContent]: true}, {[html.joinChildren]: html.tag('br')}, - [ - sec.extra.additionalFilesShortcut, - - sec.extra.galleryLink && sec.extra.commentaryLink && - language.$('releaseInfo.viewGalleryOrCommentary', { - gallery: - sec.extra.galleryLink - .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.gallery')), - commentary: - sec.extra.commentaryLink - .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.commentary')), - }), - - sec.extra.galleryLink && !sec.extra.commentaryLink && - language.$('releaseInfo.viewGallery', { - link: - sec.extra.galleryLink - .slot('content', language.$('releaseInfo.viewGallery.link')), + language.encapsulate('releaseInfo', capsule => [ + !html.isBlank(relations.additionalFilesList) && + language.$(capsule, 'additionalFiles.shortcut', { + link: html.tag('a', + {href: '#additional-files'}, + language.$(capsule, 'additionalFiles.shortcut.link')), }), - !sec.extra.galleryLink && sec.extra.commentaryLink && - language.$('releaseInfo.viewCommentary', { - link: - sec.extra.commentaryLink - .slot('content', language.$('releaseInfo.viewCommentary.link')), - }), - ]), + (relations.galleryLink && relations.commentaryLink + ? language.encapsulate(capsule, 'viewGalleryOrCommentary', capsule => + language.$(capsule, { + gallery: + relations.galleryLink + .slot('content', language.$(capsule, 'gallery')), + + commentary: + relations.commentaryLink + .slot('content', language.$(capsule, 'commentary')), + })) + + : relations.galleryLink + ? language.encapsulate(capsule, 'viewGallery', capsule => + language.$(capsule, { + link: + relations.galleryLink + .slot('content', language.$(capsule, 'link')), + })) + + : relations.commentaryLink + ? language.encapsulate(capsule, 'viewCommentary', capsule => + language.$(capsule, { + link: + relations.commentaryLink + .slot('content', language.$(capsule, 'link')), + })) + + : html.blank()), + + !html.isBlank(relations.creditSourceEntries) && + language.encapsulate(capsule, 'readCreditingSources', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#crediting-sources'}, + language.$(capsule, 'link')), + })), + ])), relations.trackList, @@ -213,28 +176,48 @@ export default { {[html.onlyIfContent]: true}, {[html.joinChildren]: html.tag('br')}, - [ - data.dateAddedToWiki && - language.$('releaseInfo.addedToWiki', { - date: language.formatDate(data.dateAddedToWiki), + language.encapsulate('releaseInfo', capsule => [ + language.$(capsule, 'addedToWiki', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.dateAddedToWiki), + }), + ])), + + (!html.isBlank(relations.artistCommentaryEntries) || + !html.isBlank(relations.creditSourceEntries)) + && + html.tag('hr', {class: 'main-separator'}), + + language.encapsulate('releaseInfo.additionalFiles', capsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'additional-files'}, + title: language.$(capsule, 'heading'), }), - ]), - sec.additionalFiles && [ - sec.additionalFiles.heading + relations.additionalFilesList, + ])), + + html.tags([ + relations.contentContentHeading.clone() .slots({ - id: 'additional-files', - title: - language.$('releaseInfo.additionalFiles.heading', { - additionalFiles: - language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}), - }), + attributes: {id: 'artist-commentary'}, + string: 'misc.artistCommentary', }), - sec.additionalFiles.additionalFilesList, - ], + relations.artistCommentaryEntries, + ]), - sec.artistCommentary, + html.tags([ + relations.contentContentHeading.clone() + .slots({ + attributes: {id: 'crediting-sources'}, + string: 'misc.creditingSources', + }), + + relations.creditSourceEntries, + ]), ], navLinkStyle: 'hierarchical', @@ -250,24 +233,13 @@ export default { }, ], - navContent: - relations.chronologyLinks.slots({ - chronologyInfoSets: [ - { - headingString: 'misc.chronology.heading.coverArt', - contributions: relations.coverArtistChronologyContributions, - }, - ], - }), - banner: relations.banner ?? null, bannerPosition: 'top', secondaryNav: relations.secondaryNav, - ...relations.sidebar, + leftSidebar: relations.sidebar, socialEmbed: relations.socialEmbed, - }); - }, + })), }; diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index 121af439..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}, @@ -62,51 +85,58 @@ export default { }, generate(data, relations, slots, {html, language}) { - const {content: extraLinks = []} = - slots.showExtraLinks && - {content: [ - (!data.galleryIsStub || slots.currentExtra === 'gallery') && - relations.albumGalleryLink?.slots({ - attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('albumPage.nav.gallery'), - }), - - relations.albumCommentaryLink?.slots({ - attributes: {class: slots.currentExtra === 'commentary' && 'current'}, - content: language.$('albumPage.nav.commentary'), - }), - ]}; - - const {content: previousNextLinks = []} = - slots.showTrackNavigation && + const albumNavCapsule = language.encapsulate('albumPage.nav'); + const trackNavCapsule = language.encapsulate('trackPage.nav'); + + 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'}, {href: '#', 'data-random': 'track-in-sidebar'}, (data.isTrackPage - ? language.$('trackPage.nav.random') - : language.$('albumPage.nav.randomTrack'))); + ? language.$(trackNavCapsule, 'random') + : language.$(albumNavCapsule, 'randomTrack'))); + + return relations.switcher.slots({ + links: [ + slots.showTrackNavigation && + previousLink, + + slots.showTrackNavigation && + nextLink, - const allLinks = [ - ...previousNextLinks, - ...extraLinks, - randomLink, - ].filter(Boolean); + 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 5128fbac..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,20 +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); - - if (!empty(album.urls)) { - relations.externalLinks = - album.urls.map(url => - relation('linkExternal', url)); - } + relations.listenLine = + relation('generateReleaseInfoListenLine', album); return relations; }, @@ -43,71 +31,65 @@ export default { data.coverArtDate = album.coverArtDate; } - data.duration = accumulateSum(album.tracks, track => track.duration); - data.durationApproximate = album.tracks.length > 1; + const durationTerms = + album.tracks + .map(track => track.duration) + .filter(value => value > 0); + + if (empty(durationTerms)) { + data.duration = null; + data.durationApproximate = null; + } else { + data.duration = accumulateSum(durationTerms); + data.durationApproximate = album.tracks.length > 1; + } data.numTracks = album.tracks.length; return data; }, - generate(data, relations, {html, language}) { - return html.tags([ - html.tag('p', - {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, - - [ - relations.artistContributionsLine - .slots({stringKey: 'releaseInfo.by'}), - - relations.coverArtistContributionsLine - .slots({stringKey: 'releaseInfo.coverArtBy'}), - - relations.wallpaperArtistContributionsLine - .slots({stringKey: 'releaseInfo.wallpaperArtBy'}), - - relations.bannerArtistContributionsLine - .slots({stringKey: 'releaseInfo.bannerArtBy'}), - - data.date && - language.$('releaseInfo.released', { - date: language.formatDate(data.date), + generate: (data, relations, {html, language}) => + language.encapsulate('releaseInfo', capsule => + html.tags([ + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + relations.artistContributionsLine.slots({ + stringKey: capsule + '.by', + featuringStringKey: capsule + '.by.featuring', + chronologyKind: 'album', }), - data.coverArtDate && - language.$('releaseInfo.artReleased', { - date: language.formatDate(data.coverArtDate), + language.$(capsule, 'released', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.date), }), - data.duration && - language.$('releaseInfo.duration', { + language.$(capsule, 'duration', { + [language.onlyIfOptions]: ['duration'], duration: language.formatDuration(data.duration, { approximate: data.durationApproximate, }), }), - ]), + ]), - relations.externalLinks && html.tag('p', - language.$('releaseInfo.listenOn', { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => - link.slots({ - context: [ - 'album', - (data.numTracks === 0 - ? 'albumNoTracks' - : data.numTracks === 1 - ? 'albumOneTrack' - : 'albumMultipleTracks'), - ], - style: 'normal', - }))), + {[html.onlyIfContent]: true}, + + 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 400420ba..bfa48f03 100644 --- a/src/content/dependencies/generateAlbumSecondaryNav.js +++ b/src/content/dependencies/generateAlbumSecondaryNav.js @@ -1,168 +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.secondaryNav = - relation('generateSecondaryNav'); - - relations.groupLinks = - album.groups - .map(group => relation('linkGroup', group)); - - relations.colorStyles = - album.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; - }, + relations: (relation, query, _sprawl, album) => ({ + 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'), + + groupParts: + query.groups + .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 = + generate(relations, slots, {html}) { + const groupConnectedParts = 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 = - 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}, index) => - html.tag('span', {class: 'nav-link'}, - index > 0 && - {class: 'has-divider'}, + 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, + ]; - colorStyle.slot('context', 'primary-only'), + return relations.secondaryNav.slots({ + alwaysVisible: slots.alwaysVisible, - content)); + attributes: [ + {class: 'album-secondary-nav'}, - return relations.secondaryNav.slots({ - class: 'nav-links-groups', - content: navLinks, + 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 5ef4501b..7cf689cc 100644 --- a/src/content/dependencies/generateAlbumSidebar.js +++ b/src/content/dependencies/generateAlbumSidebar.js @@ -1,79 +1,171 @@ +import {sortAlbumsTracksChronologically} from '#sort'; +import {stitchArrays, transposeArrays} from '#sugar'; + export default { contentDependencies: [ 'generateAlbumSidebarGroupBox', - 'generateAlbumSidebarTrackSection', - 'linkAlbum', + 'generateAlbumSidebarSeriesBox', + 'generateAlbumSidebarTrackListBox', + 'generatePageSidebar', + 'generatePageSidebarConjoinedBox', + 'generateTrackReleaseBox', ], - extraDependencies: ['html'], + extraDependencies: ['html', 'wikiData'], - relations(relation, album, track) { - const relations = {}; + sprawl: ({groupData}) => ({ + // TODO: Series aren't their own things, so we access them weirdly. + seriesData: + groupData.flatMap(group => group.serieses), + }), - relations.albumLink = - relation('linkAlbum', album); + query(sprawl, album, track) { + const query = {}; - relations.groupBoxes = - album.groups.map(group => - relation('generateAlbumSidebarGroupBox', album, group)); + query.groups = + album.groups; - relations.trackSections = - album.trackSections.map(trackSection => - relation('generateAlbumSidebarTrackSection', album, track, trackSection)); + query.groupSerieses = + query.groups + .map(group => + group.serieses + .filter(series => series.albums.includes(album))); - return relations; - }, + 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); - data(album, track) { - return {isAlbumPage: !track}; + 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'), + + conjoinedBox: + relation('generatePageSidebarConjoinedBox'), + + trackListBox: + relation('generateAlbumSidebarTrackListBox', album, track), + + groupBoxes: + 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: (_query, _sprawl, _album, track) => ({ + isAlbumPage: !track, + isTrackPage: !!track, + }), + generate(data, relations, {html}) { - const trackListBox = { - class: 'track-list-sidebar-box', - content: - html.tags([ - html.tag('h1', relations.albumLink), - relations.trackSections, - ]), - }; - - if (data.isAlbumPage) { - const groupBoxes = - relations.groupBoxes - .map(content => ({ - class: 'individual-group-sidebar-box', - content: content.slot('mode', 'album'), - })); - - return { - leftSidebarMultiple: [ - ...groupBoxes, - trackListBox, - ], - }; + for (const box of [ + ...relations.groupBoxes, + ...relations.seriesBoxes.flat(), + ...relations.disconnectedSeriesBoxes, + ]) { + box.setSlot('mode', + data.isAlbumPage ? 'album' : 'track'); } - const conjoinedGroupBox = { - class: 'conjoined-group-sidebar-box', - content: - relations.groupBoxes - .flatMap((content, i, {length}) => [ - content.slot('mode', 'track'), - i < length - 1 && - html.tag('hr', { - style: `border-color: var(--primary-color); border-style: none none dotted none` - }), - ]) - .filter(Boolean), - }; - - return { - // leftSidebarStickyMode: 'column', - leftSidebarMultiple: [ - trackListBox, - conjoinedGroupBox, + return relations.sidebar.slots({ + boxes: [ + 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.isTrackPage && + relations.laterTrackReleaseBoxes, + + data.isTrackPage && + relations.conjoinedBox.slots({ + attributes: {class: 'conjoined-group-sidebar-box'}, + boxes: + ([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/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js index 93ebf5d4..f3be74f7 100644 --- a/src/content/dependencies/generateAlbumSidebarGroupBox.js +++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js @@ -1,8 +1,9 @@ import {sortChronologically} from '#sort'; -import {atOffset, empty} from '#sugar'; +import {atOffset} from '#sugar'; export default { contentDependencies: [ + 'generatePageSidebarBox', 'linkAlbum', 'linkExternal', 'linkGroup', @@ -40,6 +41,9 @@ export default { relations(relation, query, album, group) { const relations = {}; + relations.box = + relation('generatePageSidebarBox'); + relations.groupLink = relation('linkGroup', group); @@ -72,39 +76,51 @@ export default { }, }, - generate(relations, slots, {html, language}) { - return html.tags([ - html.tag('h1', - language.$('albumSidebar.groupBox.title', { - group: relations.groupLink, - })), - - slots.mode === 'album' && - relations.description - ?.slot('mode', 'multiline'), - - !empty(relations.externalLinks) && - html.tag('p', - language.$('releaseInfo.visitOn', { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => link.slot('context', 'group'))), - })), - - slots.mode === 'album' && - relations.nextAlbumLink && - html.tag('p', {class: 'group-chronology-link'}, - language.$('albumSidebar.groupBox.next', { - album: relations.nextAlbumLink, - })), - - slots.mode === 'album' && - relations.previousAlbumLink && - html.tag('p', {class: 'group-chronology-link'}, - language.$('albumSidebar.groupBox.previous', { - album: relations.previousAlbumLink, - })), - ]); - }, + generate: (relations, slots, {html, language}) => + language.encapsulate('albumSidebar.groupBox', boxCapsule => + relations.box.slots({ + attributes: {class: 'individual-group-sidebar-box'}, + content: [ + html.tag('h1', + language.$(boxCapsule, 'title', { + group: relations.groupLink, + })), + + slots.mode === 'album' && + relations.description + ?.slot('mode', 'multiline'), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$('releaseInfo.visitOn', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatDisjunctionList( + relations.externalLinks + .map(link => link.slot('context', 'group'))), + })), + + slots.mode === 'album' && + html.tag('p', {class: 'group-chronology-link'}, + {[html.onlyIfContent]: true}, + + language.$(boxCapsule, 'next', { + [language.onlyIfOptions]: ['album'], + + album: relations.nextAlbumLink, + })), + + slots.mode === 'album' && + html.tag('p', {class: 'group-chronology-link'}, + {[html.onlyIfContent]: true}, + + language.$(boxCapsule, 'previous', { + [language.onlyIfOptions]: ['album'], + + album: relations.previousAlbumLink, + })), + ], + })), }; diff --git a/src/content/dependencies/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/generateAlbumSidebarTrackListBox.js b/src/content/dependencies/generateAlbumSidebarTrackListBox.js new file mode 100644 index 00000000..3a244e3a --- /dev/null +++ b/src/content/dependencies/generateAlbumSidebarTrackListBox.js @@ -0,0 +1,31 @@ +export default { + contentDependencies: [ + 'generateAlbumSidebarTrackSection', + 'generatePageSidebarBox', + 'linkAlbum', + ], + + extraDependencies: ['html'], + + relations: (relation, album, track) => ({ + box: + relation('generatePageSidebarBox'), + + albumLink: + relation('linkAlbum', album), + + trackSections: + album.trackSections.map(trackSection => + relation('generateAlbumSidebarTrackSection', album, track, trackSection)), + }), + + generate: (relations, {html}) => + relations.box.slots({ + attributes: {class: 'track-list-sidebar-box'}, + + content: [ + html.tag('h1', relations.albumLink), + relations.trackSections, + ], + }) +}; diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index aa5c723d..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; }, @@ -55,10 +66,12 @@ export default { }, generate(data, relations, slots, {getColors, html, language}) { + const capsule = language.encapsulate('albumSidebar.trackList'); + const sectionName = - html.tag('span', {class: 'group-name'}, + html.tag('b', (data.isDefaultTrackSection - ? language.$('albumSidebar.trackList.fallbackSectionName') + ? language.$(capsule, 'fallbackSectionName') : data.name)); let colorStyle; @@ -68,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.$('albumSidebar.trackList.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 && @@ -117,14 +140,22 @@ export default { colorStyle, html.tag('span', - (data.hasTrackNumbers - ? language.$('albumSidebar.trackList.group.withRange', { - group: sectionName, - range: `${data.firstTrackNumber}–${data.lastTrackNumber}` - }) - : language.$('albumSidebar.trackList.group', { - group: sectionName, - })))), + language.encapsulate(capsule, 'group', 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 c8b123fe..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,34 +40,31 @@ export default { return data; }, - generate(data, relations, {absoluteTo, language, urls}) { - return relations.socialEmbed.slots({ - title: - language.$('albumPage.socialEmbed.title', { - album: data.albumName, - }), - - description: relations.description, - - headingContent: - (data.hasHeading - ? language.$('albumPage.socialEmbed.heading', { - group: data.headingGroupName, - }) - : null), - - headingLink: - (data.hasHeading - ? absoluteTo('localized.groupGallery', data.headingGroupDirectory) - : null), - - imagePath: - (data.hasImage - ? '/' + - urls - .from('shared.root') - .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension) - : null), - }); - }, + generate: (data, relations, {absoluteTo, language}) => + language.encapsulate('albumPage.socialEmbed', embedCapsule => + relations.socialEmbed.slots({ + title: + language.$(embedCapsule, 'title', { + album: data.albumName, + }), + + description: relations.description, + + headingContent: + (data.hasHeading + ? language.$(embedCapsule, 'heading', { + group: data.headingGroupName, + }) + : null), + + headingLink: + (data.hasHeading + ? absoluteTo('localized.groupGallery', data.headingGroupDirectory) + : null), + + imagePath: + (data.hasImage + ? 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 ee06b9e6..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,43 +141,59 @@ 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, }) => [ - heading.slots({ - tag: 'dt', - title: - (duration === 0 - ? language.$('trackList.section', { - section: name, - }) - : language.$('trackList.section.withDuration', { - section: name, - duration: + language.encapsulate('trackList.section', capsule => + heading.slots({ + tag: 'dt', + + title: + language.encapsulate(capsule, capsule => { + const options = {section: name}; + + if (duration !== 0) { + capsule += '.withDuration'; + options.duration = language.formatDuration(duration, { approximate: durationApproximate, - }), - })), - }), + }); + } + + return language.$(capsule, options); + }), + + stickyTitle: + language.$(capsule, 'sticky', { + section: name, + }), + })), + + html.tag('dd', [ + html.tag('blockquote', + {[html.onlyIfContent]: true}, + description), - html.tag('dd', 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 11b6a1b0..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); - - return query; - }, - - relations(relation, query, track) { - const relations = {}; - - if (!empty(track.artistContribs)) { - relations.contributionLinks = - track.artistContribs - .map(contrib => relation('linkContribution', contrib)); - } + section.tracks.includes(track)), - relations.trackLink = - relation('linkTrack', track); - - if (!query.trackHasDuration) { - relations.missingDuration = - relation('generateAlbumTrackListMissingDuration'); - } - - return relations; - }, + albumHasDuration: + album.tracks.some(track => track.duration), + }), - data(query, track, album) { - const data = {}; + relations: (relation, query, track) => ({ + item: + relation('generateTrackListItem', + track, + track.album.artistContribs), + }), - data.duration = query.duration; - data.trackHasDuration = query.trackHasDuration; - data.sectionHasDuration = query.sectionHasDuration; - data.albumHasDuration = query.albumHasDuration; + data: (query, track, album) => ({ + trackHasDuration: query.trackHasDuration, + sectionHasDuration: query.sectionHasDuration, + albumHasDuration: query.albumHasDuration, - if (track.color !== album.color) { - data.color = track.color; - } - - data.showArtists = - !empty(track.artistContribs) && - (empty(album.artistContribs) || - !compareArrays( - track.artistContribs.map(c => c.who), - album.artistContribs.map(c => c.who), - {checkOrder: false})); - - return data; - }, + colorize: + track.color !== album.color, + }), slots: { collapseDurationScope: { @@ -80,52 +41,22 @@ export default { }, }, - generate(data, relations, slots, {getColors, html, language}) { - let colorStyle; - if (data.color) { - const {primary} = getColors(data.color); - colorStyle = {style: `--primary-color: ${primary}`}; - } - - const parts = ['trackList.item']; - const options = {}; - - options.track = - relations.trackLink - .slot('color', false); - - const collapseDuration = - (slots.collapseDurationScope === 'track' - ? !data.trackHasDuration - : slots.collapseDurationScope === 'section' - ? !data.sectionHasDuration - : slots.collapseDurationScope === 'album' - ? !data.albumHasDuration - : false); - - if (!collapseDuration) { - parts.push('withDuration'); - - options.duration = - (data.trackHasDuration - ? language.$('trackList.item.withDuration.duration', { - duration: - language.formatDuration(data.duration), - }) - : relations.missingDuration); - } - - if (data.showArtists) { - parts.push('withArtists'); - options.by = - html.tag('span', {class: 'by'}, - language.$('trackList.item.withArtists.by', { - artists: language.formatConjunctionList(relations.contributionLinks), - })); - } - - return html.tag('li', - colorStyle, - language.formatString(...parts, options)); - }, + 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/generateAlbumTrackListMissingDuration.js b/src/content/dependencies/generateAlbumTrackListMissingDuration.js deleted file mode 100644 index 6d4a6ec8..00000000 --- a/src/content/dependencies/generateAlbumTrackListMissingDuration.js +++ /dev/null @@ -1,33 +0,0 @@ -export default { - contentDependencies: ['generateTextWithTooltip', 'generateTooltip'], - extraDependencies: ['html', 'language'], - - relations: (relation) => ({ - textWithTooltip: - relation('generateTextWithTooltip'), - - tooltip: - relation('generateTooltip'), - }), - - generate: (relations, {html, language}) => - relations.textWithTooltip.slots({ - attributes: {class: 'missing-duration'}, - customInteractionCue: true, - - text: - language.$('trackList.item.withDuration.duration', { - duration: - html.tag('span', {class: 'text-with-tooltip-interaction-cue'}, - language.$('trackList.item.withDuration.duration.missing')), - }), - - tooltip: - relations.tooltip.slots({ - attributes: {class: 'missing-duration-tooltip'}, - - content: - language.$('trackList.item.withDuration.duration.missing.info'), - }), - }), -}; 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 962f1b7f..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,128 +24,215 @@ 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.coverArtists = - query.things.map(thing => - thing.coverArtistContribs - .map(({who: artist}) => artist.name)); + data.artworkLabels = + query.allArtworks + .map(artwork => artwork.label) + + 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}) { - return relations.layout - .slots({ + generate: (data, relations, {html, language}) => + language.encapsulate('artTagGalleryPage', pageCapsule => + relations.layout.slots({ title: - language.$('tagPage.title', { + language.$(pageCapsule, 'title', { tag: data.name, }), headingMode: 'static', - color: data.color, + additionalNames: relations.additionalNamesBox, + mainClasses: ['top-index'], mainContent: [ - html.tag('p', {class: 'quick-info'}, - language.$('tagPage.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, - }).map(({image, path}) => - image.slot('path', path)), + 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.$('tagPage.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 13779159..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,83 +6,65 @@ export default { 'generateCoverGrid', 'generatePageLayout', 'image', - 'linkAlbum', - 'linkTrack', + 'linkAnythingMan', ], extraDependencies: ['html', 'language'], - query(artist) { - const things = [ - ...artist.albumsAsCoverArtist, - ...artist.tracksAsCoverArtist, - ]; - - 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.otherCoverArtists = - query.things.map(thing => - (thing.coverArtistContribs.length > 1 - ? thing.coverArtistContribs - .filter(({who}) => who !== artist) - .map(({who}) => who.name) - : null)); - - return data; - }, - - generate(data, relations, {html, language}) { - return relations.layout - .slots({ + 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 => + relations.layout.slots({ title: - language.$('artistGalleryPage.title', { + language.$(pageCapsule, 'title', { artist: data.name, }), @@ -92,31 +73,26 @@ export default { mainClasses: ['top-index'], mainContent: [ html.tag('p', {class: 'quick-info'}, - language.$('artistGalleryPage.infoLine', { - coverArts: language.countCoverArts(data.numArtworks, { - unit: true, - }), + language.$(pageCapsule, 'infoLine', { + coverArts: + language.countArtworks(data.numArtworks, { + unit: true, + }), })), relations.coverGrid .slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - }).map(({image, path}) => - image.slot('path', path)), - 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), + })), }), ], @@ -128,6 +104,5 @@ export default { currentExtra: 'gallery', }) .content, - }) - }, + })), } diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js index a51f516b..e1fa7a0b 100644 --- a/src/content/dependencies/generateArtistGroupContributionsInfo.js +++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js @@ -1,82 +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 - .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, @@ -92,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: { @@ -130,94 +144,104 @@ export default { countUnit: {validate: v => v.is('tracks', 'artworks')}, }, - generate(data, relations, slots, {html, language}) { - if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) { - return html.blank(); - } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) { - return html.blank(); - } + generate: (data, relations, slots, {html, language}) => + language.encapsulate('artistPage.groupContributions', capsule => { + if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) { + return html.blank(); + } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) { + return html.blank(); + } - const getCounts = counts => - counts.map(count => { - switch (slots.countUnit) { - case 'tracks': return language.countTracks(count, {unit: true}); - case 'artworks': return language.countArtworks(count, {unit: true}); - } - }); - - // We aren't displaying the "~" approximate symbol here for now. - // The general notion that these sums aren't going to be 100% accurate - // is made clear by the "XYZ has contributed ~1:23:45 hours of music..." - // line that's always displayed above this table. - const getDurations = (durations, approximate) => - stitchArrays({ - duration: durations, - approximate: approximate, - }).map(({duration}) => language.formatDuration(duration)); - - const topLevelClasses = [ - 'group-contributions-sorted-by-' + slots.sort, - slots.visible && 'visible', - ]; - - return html.tags([ - html.tag('dt', {class: topLevelClasses}, - (slots.showSortButton - ? language.$('artistPage.groupContributions.title.withSortButton', { - title: slots.title, - sort: - html.tag('a', {class: 'group-contributions-sort-button'}, - {href: '#'}, - - (slots.sort === 'count' - ? language.$('artistPage.groupContributions.title.sorting.count') - : language.$('artistPage.groupContributions.title.sorting.duration'))), - }) - : slots.title)), - - html.tag('dd', {class: topLevelClasses}, - html.tag('ul', {class: 'group-contributions-table'}, - {role: 'list'}, - - (slots.sort === 'count' - ? stitchArrays({ - group: relations.groupLinksSortedByCount, - count: getCounts(data.groupCountsSortedByCount), - duration: - getDurations( - data.groupDurationsSortedByCount, - data.groupDurationsApproximateSortedByCount), - }).map(({group, count, duration}) => - html.tag('li', - html.tag('div', {class: 'group-contributions-row'}, [ - group, - html.tag('span', {class: 'group-contributions-metrics'}, - // When sorting by count, duration details aren't necessarily - // available for all items. - (slots.showBothColumns && duration - ? language.$('artistPage.groupContributions.item.countDurationAccent', {count, duration}) - : language.$('artistPage.groupContributions.item.countAccent', {count}))), - ]))) - - : stitchArrays({ - group: relations.groupLinksSortedByDuration, - count: getCounts(data.groupCountsSortedByDuration), - duration: - getDurations( - data.groupDurationsSortedByDuration, - data.groupDurationsApproximateSortedByCount), - }).map(({group, count, duration}) => - html.tag('li', - html.tag('div', {class: 'group-contributions-row'}, [ - group, - html.tag('span', {class: 'group-contributions-metrics'}, - // Count details are always available, since they're just the - // number of contributions directly. And duration details are - // guaranteed for every item when sorting by duration. - (slots.showBothColumns - ? language.$('artistPage.groupContributions.item.durationCountAccent', {duration, count}) - : language.$('artistPage.groupContributions.item.durationAccent', {duration}))), - ])))))), - ]); - }, + const getCounts = counts => + counts.map(count => { + switch (slots.countUnit) { + case 'tracks': return language.countTracks(count, {unit: true}); + case 'artworks': return language.countArtworks(count, {unit: true}); + } + }); + + // We aren't displaying the "~" approximate symbol here for now. + // The general notion that these sums aren't going to be 100% accurate + // is made clear by the "XYZ has contributed ~1:23:45 hours of music..." + // line that's always displayed above this table. + const getDurations = (durations, approximate) => + stitchArrays({ + duration: durations, + approximate: approximate, + }).map(({duration}) => language.formatDuration(duration)); + + const topLevelClasses = [ + 'group-contributions-sorted-by-' + slots.sort, + slots.visible && 'visible', + ]; + + // TODO: It feels pretty awkward that this component is the only one that + // has enough knowledge to decide if the sort button is even applicable... + const switchingSortPossible = + !empty(relations.groupLinksSortedByCount) && + !empty(relations.groupLinksSortedByDuration); + + return html.tags([ + html.tag('dt', {class: topLevelClasses}, + language.encapsulate(capsule, 'title', capsule => + (switchingSortPossible && slots.showSortButton + ? language.$(capsule, 'withSortButton', { + title: slots.title, + sort: + html.tag('a', {class: 'group-contributions-sort-button'}, + {href: '#'}, + + (slots.sort === 'count' + ? language.$(capsule, 'sorting.count') + : language.$(capsule, 'sorting.duration'))), + }) + : slots.title))), + + html.tag('dd', {class: topLevelClasses}, + html.tag('ul', {class: 'group-contributions-table'}, + {role: 'list'}, + + (slots.sort === 'count' + ? stitchArrays({ + group: relations.groupLinksSortedByCount, + count: getCounts(data.groupCountsSortedByCount), + duration: + getDurations( + data.groupDurationsSortedByCount, + data.groupDurationsApproximateSortedByCount), + }).map(({group, count, duration}) => + language.encapsulate(capsule, 'item', capsule => + html.tag('li', + html.tag('div', {class: 'group-contributions-row'}, [ + group, + html.tag('span', {class: 'group-contributions-metrics'}, + // When sorting by count, duration details aren't necessarily + // available for all items. + (slots.showBothColumns && duration + ? language.$(capsule, 'countDurationAccent', {count, duration}) + : language.$(capsule, 'countAccent', {count}))), + ])))) + + : stitchArrays({ + group: relations.groupLinksSortedByDuration, + count: getCounts(data.groupCountsSortedByDuration), + duration: + getDurations( + data.groupDurationsSortedByDuration, + data.groupDurationsApproximateSortedByDuration), + }).map(({group, count, duration}) => + language.encapsulate(capsule, 'item', capsule => + html.tag('li', + html.tag('div', {class: 'group-contributions-row'}, [ + group, + html.tag('span', {class: 'group-contributions-metrics'}, + // Count details are always available, since they're just the + // number of contributions directly. And duration details are + // guaranteed for every item when sorting by duration. + (slots.showBothColumns + ? language.$(capsule, 'durationCountAccent', {duration, count}) + : language.$(capsule, 'durationAccent', {duration}))), + ]))))))), + ]); + }), }; diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index 1b85680f..1f738de4 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -1,8 +1,8 @@ -import {empty, unique} from '#sugar'; -import {getTotalDuration} from '#wiki-data'; +import {empty, stitchArrays, unique} from '#sugar'; export default { contentDependencies: [ + 'generateArtistArtworkColumn', 'generateArtistGroupContributionsInfo', 'generateArtistInfoPageArtworksChunkedList', 'generateArtistInfoPageCommentaryChunkedList', @@ -10,212 +10,256 @@ export default { 'generateArtistInfoPageTracksChunkedList', 'generateArtistNavLinks', 'generateContentHeading', - 'generateCoverArtwork', 'generatePageLayout', - 'linkAlbum', 'linkArtistGallery', 'linkExternal', 'linkGroup', - 'linkTrack', 'transformContent', ], - extraDependencies: ['html', 'language', 'wikiData'], - - sprawl({wikiInfo}) { - return { - enableFlashesAndGames: wikiInfo.enableFlashesAndGames, - }; - }, - - query(sprawl, artist) { - return { - // Even if an artist has served as both "artist" (compositional) and - // "contributor" (instruments, production, etc) on the same track, that - // track only counts as one unique contribution. - allTracks: - unique([...artist.tracksAsArtist, ...artist.tracksAsContributor]), - - // Artworks are different, though. We intentionally duplicate album data - // objects when the artist has contributed some combination of cover art, - // wallpaper, and banner - these each count as a unique contribution. - allArtworks: [ - ...artist.albumsAsCoverArtist, - ...artist.albumsAsWallpaperArtist, - ...artist.albumsAsBannerArtist, - ...artist.tracksAsCoverArtist, - ], - - // Banners and wallpapers don't show up in the artist gallery page, only - // cover art. - hasGallery: - !empty(artist.albumsAsCoverArtist) || - !empty(artist.tracksAsCoverArtist), - }; - }, - - relations(relation, query, sprawl, artist) { - const relations = {}; - const sections = relations.sections = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.artistNavLinks = - relation('generateArtistNavLinks', artist); - - if (artist.hasAvatar) { - relations.cover = - relation('generateCoverArtwork', []); - } - - if (artist.contextNotes) { - const contextNotes = sections.contextNotes = {}; - contextNotes.content = relation('transformContent', artist.contextNotes); - } - - if (!empty(artist.urls)) { - const visit = sections.visit = {}; - visit.externalLinks = - artist.urls.map(url => - relation('linkExternal', url)); - } - - if (!empty(query.allTracks)) { - const tracks = sections.tracks = {}; - tracks.heading = relation('generateContentHeading'); - tracks.list = relation('generateArtistInfoPageTracksChunkedList', artist); - tracks.groupInfo = relation('generateArtistGroupContributionsInfo', query.allTracks); - } - - if (!empty(query.allArtworks)) { - const artworks = sections.artworks = {}; - artworks.heading = relation('generateContentHeading'); - artworks.list = relation('generateArtistInfoPageArtworksChunkedList', artist); - artworks.groupInfo = - relation('generateArtistGroupContributionsInfo', query.allArtworks); - - if (query.hasGallery) { - artworks.artistGalleryLink = - relation('linkArtistGallery', artist); - } - } - - if (sprawl.enableFlashesAndGames && !empty(artist.flashesAsContributor)) { - const flashes = sections.flashes = {}; - flashes.heading = relation('generateContentHeading'); - flashes.list = relation('generateArtistInfoPageFlashesChunkedList', artist); - } - - if (!empty(artist.albumsAsCommentator) || !empty(artist.tracksAsCommentator)) { - const commentary = sections.commentary = {}; - commentary.heading = relation('generateContentHeading'); - commentary.list = relation('generateArtistInfoPageCommentaryChunkedList', artist); - } - - return relations; - }, - - data(query, sprawl, artist) { - const data = {}; - - data.name = artist.name; - data.directory = artist.directory; - - if (artist.hasAvatar) { - data.avatarFileExtension = artist.avatarFileExtension; - } - - data.totalTrackCount = query.allTracks.length; - data.totalDuration = getTotalDuration(query.allTracks, {originalReleasesOnly: true}); - - return data; - }, - - generate(data, relations, {html, language}) { - const {sections: sec} = relations; - - return relations.layout - .slots({ + extraDependencies: ['html', 'language'], + + query: (artist) => ({ + 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) => ({ + layout: + relation('generatePageLayout'), + + artistNavLinks: + relation('generateArtistNavLinks', artist), + + artworkColumn: + relation('generateArtistArtworkColumn', artist), + + contentHeading: + relation('generateContentHeading'), + + 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)), + + tracksChunkedList: + relation('generateArtistInfoPageTracksChunkedList', artist), + + tracksGroupInfo: + relation('generateArtistGroupContributionsInfo', query.trackContributions), + + artworksChunkedList: + relation('generateArtistInfoPageArtworksChunkedList', artist, false), + + editsForWikiArtworksChunkedList: + relation('generateArtistInfoPageArtworksChunkedList', artist, true), + + artworksGroupInfo: + relation('generateArtistGroupContributionsInfo', query.artworkContributions), + + artistGalleryLink: + (query.hasGallery + ? relation('linkArtistGallery', artist) + : null), + + flashesChunkedList: + relation('generateArtistInfoPageFlashesChunkedList', artist), + + commentaryChunkedList: + relation('generateArtistInfoPageCommentaryChunkedList', artist, false), + + wikiEditorCommentaryChunkedList: + relation('generateArtistInfoPageCommentaryChunkedList', artist, true), + }), + + data: (query, artist) => ({ + name: + artist.name, + + closeGroupAnnotations: + query.generalLinkedGroups + .map(({annotation}) => annotation), + + totalTrackCount: + unique( + query.trackContributions + .filter(contrib => contrib.countInContributionTotals) + .map(contrib => contrib.thing)) + .length, + + totalDuration: + artist.totalDuration, + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('artistPage', pageCapsule => + relations.layout.slots({ title: data.name, headingMode: 'sticky', - cover: - (relations.cover - ? relations.cover.slots({ - path: [ - 'media.artistAvatar', - data.directory, - data.avatarFileExtension, - ], - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ - sec.contextNotes && [ - html.tag('p', language.$('releaseInfo.note')), + html.tags([ + html.tag('p', + {[html.onlyIfSiblings]: true}, + language.$('releaseInfo.note')), + html.tag('blockquote', - sec.contextNotes.content), - ], + {[html.onlyIfContent]: true}, + relations.contextNotes), + ]), + + 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); + }))), + }); + }), - sec.visit && - html.tag('p', - language.$('releaseInfo.visitOn', { - links: - language.formatDisjunctionList( - sec.visit.externalLinks.map(link => - link.slots({ - context: 'artist', - style: 'platform', - }))), - })), - - sec.artworks?.artistGalleryLink && - html.tag('p', - language.$('artistPage.viewArtGallery', { - link: sec.artworks.artistGalleryLink.slots({ - content: language.$('artistPage.viewArtGallery.link'), - }), - })), + language.$(capsule, 'alias', { + [language.onlyIfOptions]: ['groups'], - (sec.tracks || sec.artworsk || sec.flashes || sec.commentary) && - html.tag('p', - language.$('misc.jumpTo.withLinks', { - links: language.formatUnitList( - [ - sec.tracks && - html.tag('a', - {href: '#tracks'}, - language.$('artistPage.trackList.title')), + groups: + language.formatConjunctionList(relations.aliasGroupLinks), + }), + ])), - sec.artworks && - html.tag('a', - {href: '#art'}, - language.$('artistPage.artList.title')), + html.tag('p', + {[html.onlyIfContent]: true}, - sec.flashes && - html.tag('a', - {href: '#flashes'}, - language.$('artistPage.flashList.title')), + language.$('releaseInfo.visitOn', { + [language.onlyIfOptions]: ['links'], - sec.commentary && - html.tag('a', - {href: '#commentary'}, - language.$('artistPage.commentaryList.title')), - ].filter(Boolean)), - })), + links: + language.formatDisjunctionList( + relations.visitLinks + .map(link => link.slot('context', 'artist'))), + })), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.encapsulate(pageCapsule, 'viewArtGallery', capsule => + language.$(capsule, { + [language.onlyIfOptions]: ['link'], + + link: + relations.artistGalleryLink?.slots({ + content: + language.$(capsule, 'link'), + }), + }))), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$('misc.jumpTo.withLinks', { + [language.onlyIfOptions]: ['links'], - sec.tracks && [ - sec.tracks.heading + links: + language.formatUnitList([ + !html.isBlank(relations.tracksChunkedList) && + html.tag('a', + {href: '#tracks'}, + language.$(pageCapsule, 'trackList.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.wikiEditorCommentaryChunkedList)) && + html.tag('a', + {href: '#commentary'}, + language.$(pageCapsule, 'commentaryList.title')), + ].filter(Boolean)), + })), + + html.tags([ + relations.contentHeading.clone() .slots({ tag: 'h2', - id: 'tracks', - title: language.$('artistPage.trackList.title'), + attributes: {id: 'tracks'}, + title: language.$(pageCapsule, 'trackList.title'), }), data.totalDuration > 0 && html.tag('p', - language.$('artistPage.contributedDurationLine', { + {[html.onlyIfSiblings]: true}, + + language.$(pageCapsule, 'contributedDurationLine', { artist: data.name, duration: language.formatDuration(data.totalDuration, { @@ -224,82 +268,118 @@ export default { }), })), - sec.tracks.list - .slots({ - groupInfo: [ - sec.tracks.groupInfo - .clone() + relations.tracksChunkedList.slots({ + groupInfo: + language.encapsulate(pageCapsule, 'groupContributions', capsule => [ + relations.tracksGroupInfo.clone() .slots({ - title: language.$('artistPage.groupContributions.title.music'), + title: language.$(capsule, 'title.music'), showSortButton: true, sort: 'count', countUnit: 'tracks', visible: true, }), - sec.tracks.groupInfo - .clone() + relations.tracksGroupInfo.clone() .slots({ - title: language.$('artistPage.groupContributions.title.music'), + title: language.$(capsule, 'title.music'), showSortButton: true, sort: 'duration', countUnit: 'tracks', visible: false, }), - ], - }), - ], + ]), + }), + ]), - sec.artworks && [ - sec.artworks.heading + html.tags([ + relations.contentHeading.clone() .slots({ tag: 'h2', - id: 'art', - title: language.$('artistPage.artList.title'), + attributes: {id: 'art'}, + title: language.$(pageCapsule, 'artList.title'), }), - sec.artworks.artistGalleryLink && - html.tag('p', - language.$('artistPage.viewArtGallery.orBrowseList', { - link: sec.artworks.artistGalleryLink.slots({ - content: language.$('artistPage.viewArtGallery.link'), - }), - })), + html.tag('p', + {[html.onlyIfContent]: true}, + + language.encapsulate(pageCapsule, 'viewArtGallery', capsule => + language.$(capsule, 'orBrowseList', { + [language.onlyIfOptions]: ['link'], + + link: + relations.artistGalleryLink?.slots({ + content: language.$(capsule, 'link'), + }), + }))), - sec.artworks.list + relations.artworksChunkedList .slots({ groupInfo: - sec.artworks.groupInfo - .slots({ - title: language.$('artistPage.groupContributions.title.artworks'), - showBothColumns: false, - sort: 'count', - countUnit: 'artworks', - }), + language.encapsulate(pageCapsule, 'groupContributions', capsule => + relations.artworksGroupInfo + .slots({ + title: language.$(capsule, 'title.artworks'), + showBothColumns: false, + sort: 'count', + countUnit: 'artworks', + })), }), - ], - sec.flashes && [ - sec.flashes.heading + html.tags([ + 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([ + relations.contentHeading.clone() .slots({ tag: 'h2', - id: 'flashes', - title: language.$('artistPage.flashList.title'), + attributes: {id: 'flashes'}, + title: language.$(pageCapsule, 'flashList.title'), }), - sec.flashes.list, - ], + relations.flashesChunkedList, + ]), - sec.commentary && [ - sec.commentary.heading + html.tags([ + relations.contentHeading.clone() .slots({ tag: 'h2', - id: 'commentary', - title: language.$('artistPage.commentaryList.title'), + attributes: {id: 'commentary'}, + title: language.$(pageCapsule, 'commentaryList.title'), }), - sec.commentary.list, - ], + relations.commentaryChunkedList, + + 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, + ]), + ]), ], navLinkStyle: 'hierarchical', @@ -309,6 +389,5 @@ export default { showExtraLinks: true, }) .content, - }); - }, + })), }; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js new file mode 100644 index 00000000..66e4204a --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js @@ -0,0 +1,50 @@ +export default { + contentDependencies: [ + 'generateArtistInfoPageChunk', + 'generateArtistInfoPageArtworksChunkItem', + 'linkAlbum', + ], + + extraDependencies: ['html'], + + relations: (relation, album, contribs) => ({ + template: + relation('generateArtistInfoPageChunk'), + + albumLink: + relation('linkAlbum', album), + + items: + contribs + .map(contrib => + relation('generateArtistInfoPageArtworksChunkItem', contrib)), + }), + + data: (_album, contribs) => ({ + dates: + contribs + .map(contrib => contrib.date), + }), + + slots: { + filterEditsForWiki: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots) => + relations.template.slots({ + mode: 'album', + albumLink: relations.albumLink, + + 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 new file mode 100644 index 00000000..cb436b0f --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js @@ -0,0 +1,111 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generateArtistInfoPageChunkItem', + 'generateArtistInfoPageOtherArtistLinks', + 'linkTrack', + 'transformContent', + ], + + extraDependencies: ['html', 'language'], + + query: (contrib) => ({ + kind: + (contrib.isBannerArtistContribution + ? 'banner' + : contrib.isWallpaperArtistContribution + ? 'wallpaper' + : contrib.isForAlbum + ? 'album-cover' + : 'track-cover'), + }), + + relations: (relation, query, contrib) => ({ + template: + relation('generateArtistInfoPageChunkItem'), + + trackLink: + (query.kind === 'track-cover' + ? relation('linkTrack', contrib.thing.thing) + : null), + + otherArtistLinks: + relation('generateArtistInfoPageOtherArtistLinks', [contrib]), + + originDetails: + relation('transformContent', contrib.thing.originDetails), + }), + + data: (query, contrib) => ({ + kind: + query.kind, + + annotation: + contrib.annotation, + + label: + contrib.thing.label, + }), + + slots: { + filterEditsForWiki: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots, {html, language}) => + relations.template.slots({ + otherArtistLinks: relations.otherArtistLinks, + + 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 => + (data.kind === 'track-cover' + ? language.$(capsule, 'track', { + track: relations.trackLink, + }) + : html.tag('i', + language.encapsulate(capsule, 'album', capsule => + (data.kind === 'wallpaper' + ? language.$(capsule, 'wallpaperArt') + : data.kind === 'banner' + ? language.$(capsule, 'bannerArt') + : language.$(capsule, 'coverArt')))))), + + originDetails: + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }), + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index 0beeb271..75a4aa5a 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -1,241 +1,72 @@ -import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort'; -import {chunkByProperties, stitchArrays} from '#sugar'; +import {sortAlbumsTracksChronologically, sortContributionsChronologically} + from '#sort'; +import {chunkByConditions, stitchArrays} from '#sugar'; export default { contentDependencies: [ - 'generateArtistInfoPageChunk', 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageOtherArtistLinks', - 'linkAlbum', - 'linkTrack', + 'generateArtistInfoPageArtworksChunk', ], - extraDependencies: ['html', 'language'], + query(artist, filterEditsForWiki) { + const query = {}; - query(artist) { - // TODO: Add and integrate wallpaper and banner date fields (#90) - // This will probably only happen once all artworks follow a standard - // shape (#70) and get their own sorting function. Read for more info: - // https://github.com/hsmusic/hsmusic-wiki/issues/90#issuecomment-1607422961 - - const processEntry = ({thing, type, track, album, contribs}) => ({ - thing: thing, - entry: { - type: type, - track: track, - album: album, - contribs: contribs, - date: thing.coverArtDate ?? thing.date, - }, - }); - - const processAlbumEntry = ({type, album, contribs}) => - processEntry({ - thing: album, - type: type, - track: null, - album: album, - contribs: contribs, - }); - - const processTrackEntry = ({type, track, contribs}) => - processEntry({ - thing: track, - type: type, - track: track, - album: track.album, - contribs: contribs, - }); - - const processAlbumEntries = ({type, albums, contribs}) => - stitchArrays({ - album: albums, - contribs: contribs, - }).map(entry => - processAlbumEntry({type, ...entry})); - - const processTrackEntries = ({type, tracks, contribs}) => - stitchArrays({ - track: tracks, - contribs: contribs, - }).map(entry => - processTrackEntry({type, ...entry})); - - const { - albumsAsCoverArtist, - albumsAsWallpaperArtist, - albumsAsBannerArtist, - tracksAsCoverArtist, - } = artist; - - const albumsAsCoverArtistContribs = - albumsAsCoverArtist - .map(album => album.coverArtistContribs); - - const albumsAsWallpaperArtistContribs = - albumsAsWallpaperArtist - .map(album => album.wallpaperArtistContribs); - - const albumsAsBannerArtistContribs = - albumsAsBannerArtist - .map(album => album.bannerArtistContribs); - - const tracksAsCoverArtistContribs = - tracksAsCoverArtist - .map(track => track.coverArtistContribs); - - const albumsAsCoverArtistEntries = - processAlbumEntries({ - type: 'albumCover', - albums: albumsAsCoverArtist, - contribs: albumsAsCoverArtistContribs, - }); - - const albumsAsWallpaperArtistEntries = - processAlbumEntries({ - type: 'albumWallpaper', - albums: albumsAsWallpaperArtist, - contribs: albumsAsWallpaperArtistContribs, - }); - - const albumsAsBannerArtistEntries = - processAlbumEntries({ - type: 'albumBanner', - albums: albumsAsBannerArtist, - contribs: albumsAsBannerArtistContribs, - }); - - const tracksAsCoverArtistEntries = - processTrackEntries({ - type: 'trackCover', - tracks: tracksAsCoverArtist, - contribs: tracksAsCoverArtistContribs, - }); - - const entries = [ - ...albumsAsCoverArtistEntries, - ...albumsAsWallpaperArtistEntries, - ...albumsAsBannerArtistEntries, - ...tracksAsCoverArtistEntries, + const allContributions = [ + ...artist.albumCoverArtistContributions, + ...artist.albumWallpaperArtistContributions, + ...artist.albumBannerArtistContributions, + ...artist.trackCoverArtistContributions, ]; - sortEntryThingPairs(entries, - things => sortAlbumsTracksChronologically(things, { - getDate: thing => thing.coverArtDate ?? thing.date, - })); - - const chunks = - chunkByProperties( - entries.map(({entry}) => entry), - ['album', 'date']); - - return {chunks}; - }, - - relations(relation, query, artist) { - return { - chunkedList: - relation('generateArtistInfoPageChunkedList'), - - chunks: - query.chunks.map(() => relation('generateArtistInfoPageChunk')), - - albumLinks: - query.chunks.map(({album}) => relation('linkAlbum', album)), - - items: - query.chunks.map(({chunk}) => - chunk.map(() => relation('generateArtistInfoPageChunkItem'))), - - itemTrackLinks: - query.chunks.map(({chunk}) => - chunk.map(({track}) => track ? relation('linkTrack', track) : null)), - - itemOtherArtistLinks: - query.chunks.map(({chunk}) => - chunk.map(({contribs}) => relation('generateArtistInfoPageOtherArtistLinks', contribs, artist))), - }; + const filteredContributions = + allContributions + .filter(({annotation}) => + (filterEditsForWiki + ? annotation?.startsWith(`edits for wiki`) + : !annotation?.startsWith(`edits for wiki`))); + + sortContributionsChronologically( + filteredContributions, + sortAlbumsTracksChronologically, + {getThing: contrib => contrib.thing.thing}); + + query.contribs = + chunkByConditions(filteredContributions, [ + ({date: date1}, {date: date2}) => + +date1 !== +date2, + ({thing: {thing: thing1}}, {thing: {thing: thing2}}) => + (thing1.album ?? thing1) !== + (thing2.album ?? thing2), + ]); + + query.albums = + query.contribs + .map(contribs => contribs[0].thing.thing) + .map(thing => thing.album ?? thing); + + return query; }, - data(query, artist) { - return { - chunkDates: - query.chunks.map(({date}) => date), + relations: (relation, query, _artist, _filterEditsForWiki) => ({ + chunkedList: + relation('generateArtistInfoPageChunkedList'), - itemTypes: - query.chunks.map(({chunk}) => - chunk.map(({type}) => type)), - - itemContributions: - query.chunks.map(({chunk}) => - chunk.map(({contribs}) => - contribs - .find(({who}) => who === artist) - .what)), - }; - }, - - generate(data, relations, {html, language}) { - return relations.chunkedList.slots({ + chunks: + stitchArrays({ + album: query.albums, + contribs: query.contribs, + }).map(({album, contribs}) => + relation('generateArtistInfoPageArtworksChunk', album, contribs)), + }), + + data: (_query, _artist, filterEditsForWiki) => ({ + filterEditsForWiki, + }), + + generate: (data, relations) => + relations.chunkedList.slots({ chunks: - stitchArrays({ - chunk: relations.chunks, - albumLink: relations.albumLinks, - date: data.chunkDates, - - items: relations.items, - itemTrackLinks: relations.itemTrackLinks, - itemOtherArtistLinks: relations.itemOtherArtistLinks, - itemTypes: data.itemTypes, - itemContributions: data.itemContributions, - }).map(({ - chunk, - albumLink, - date, - - items, - itemTrackLinks, - itemOtherArtistLinks, - itemTypes, - itemContributions, - }) => - chunk.slots({ - mode: 'album', - albumLink, - date, - - items: - stitchArrays({ - item: items, - trackLink: itemTrackLinks, - otherArtistLinks: itemOtherArtistLinks, - type: itemTypes, - contribution: itemContributions, - }).map(({ - item, - trackLink, - otherArtistLinks, - type, - contribution, - }) => - item.slots({ - otherArtistLinks, - annotation: contribution, - - content: - (type === 'trackCover' - ? language.$('artistPage.creditList.entry.track', { - track: trackLink, - }) - : html.tag('i', - language.$('artistPage.creditList.entry.album.' + { - albumWallpaper: 'wallpaperArt', - albumBanner: 'bannerArt', - albumCover: 'coverArt', - }[type]))), - })), - })), - }); - }, + relations.chunks.map(chunk => + chunk.slot('filterEditsForWiki', data.filterEditsForWiki)), + }), }; diff --git a/src/content/dependencies/generateArtistInfoPageChunk.js b/src/content/dependencies/generateArtistInfoPageChunk.js index 40943914..fce68a7d 100644 --- a/src/content/dependencies/generateArtistInfoPageChunk.js +++ b/src/content/dependencies/generateArtistInfoPageChunk.js @@ -1,3 +1,5 @@ +import {empty} from '#sugar'; + export default { extraDependencies: ['html', 'language'], @@ -6,6 +8,8 @@ export default { validate: v => v.is('flash', 'album'), }, + id: {type: 'string'}, + albumLink: { type: 'html', mutable: false, @@ -21,15 +25,33 @@ export default { mutable: false, }, - date: {validate: v => v.isDate}, - dateRangeStart: {validate: v => v.isDate}, - dateRangeEnd: {validate: v => v.isDate}, + dates: { + validate: v => v.sparseArrayOf(v.isDate), + }, duration: {validate: v => v.isDuration}, durationApproximate: {type: 'boolean'}, }, generate(slots, {html, language}) { + let earliestDate = null; + let latestDate = null; + let onlyDate = null; + + if (!empty(slots.dates)) { + earliestDate = + slots.dates + .reduce((a, b) => a <= b ? a : b); + + latestDate = + slots.dates + .reduce((a, b) => a <= b ? b : a); + + if (+earliestDate === +latestDate) { + onlyDate = earliestDate; + } + } + let accentedLink; accent: { @@ -40,9 +62,9 @@ export default { const options = {album: accentedLink}; const parts = ['artistPage.creditList.album']; - if (slots.date) { + if (onlyDate) { parts.push('withDate'); - options.date = language.formatDate(slots.date); + options.date = language.formatDate(onlyDate); } if (slots.duration) { @@ -63,16 +85,13 @@ export default { const options = {act: accentedLink}; const parts = ['artistPage.creditList.flashAct']; - if ( - slots.dateRangeStart && - slots.dateRangeEnd && - slots.dateRangeStart !== slots.dateRangeEnd - ) { - parts.push('withDateRange'); - options.dateRange = language.formatDateRange(slots.dateRangeStart, slots.dateRangeEnd); - } else if (slots.dateRangeStart || slots.date) { + if (onlyDate) { parts.push('withDate'); - options.date = language.formatDate(slots.dateRangeStart ?? slots.date); + options.date = language.formatDate(onlyDate); + } else if (earliestDate && latestDate) { + parts.push('withDateRange'); + options.dateRange = + language.formatDateRange(earliestDate, latestDate); } accentedLink = language.formatString(...parts, options); @@ -82,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 b6f40727..c80aeab7 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js @@ -1,6 +1,14 @@ +import {empty} from '#sugar'; + export default { + contentDependencies: ['generateTextWithTooltip'], extraDependencies: ['html', 'language'], + relations: (relation) => ({ + textWithTooltip: + relation('generateTextWithTooltip'), + }), + slots: { content: { type: 'html', @@ -16,45 +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}) { - let accentedContent = slots.content; + generate: (relations, slots, {html, language}) => + language.encapsulate('artistPage.creditList.entry', entryCapsule => + html.tag('li', + slots.rerelease && {class: 'rerelease'}, - accent: { - if (slots.rerelease) { - accentedContent = - language.$('artistPage.creditList.entry.rerelease', { - entry: accentedContent, - }); + html.tags([ + language.encapsulate(entryCapsule, workingCapsule => { + const workingOptions = {entry: slots.content}; - break accent; - } + if (!html.isBlank(slots.rereleaseTooltip)) { + workingCapsule += '.rerelease'; + workingOptions.rerelease = + relations.textWithTooltip.slots({ + attributes: {class: 'rerelease'}, + text: language.$(entryCapsule, 'rerelease.term'), + tooltip: slots.rereleaseTooltip, + }); - const parts = ['artistPage.creditList.entry']; - const options = {entry: accentedContent}; + return language.$(workingCapsule, workingOptions); + } - if (slots.otherArtistLinks) { - parts.push('withArtists'); - options.artists = language.formatConjunctionList(slots.otherArtistLinks); - } + if (!html.isBlank(slots.firstReleaseTooltip)) { + workingCapsule += '.firstRelease'; + workingOptions.firstRelease = + relations.textWithTooltip.slots({ + attributes: {class: 'first-release'}, + text: language.$(entryCapsule, 'firstRelease.term'), + tooltip: slots.firstReleaseTooltip, + }); - if (!html.isBlank(slots.annotation)) { - parts.push('withAnnotation'); - options.annotation = slots.annotation; - } + return language.$(workingCapsule, workingOptions); + } - if (parts.length === 1) { - break accent; - } + let anyAccent = false; - accentedContent = language.formatString(...parts, options); - } + if (!empty(slots.otherArtistLinks)) { + anyAccent = true; + workingCapsule += '.withArtists'; + workingOptions.artists = + language.formatConjunctionList(slots.otherArtistLinks); + } - return ( - html.tag('li', - slots.rerelease && {class: 'rerelease'}, - accentedContent)); - }, + 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/generateArtistInfoPageChunkedList.js b/src/content/dependencies/generateArtistInfoPageChunkedList.js index 8503d014..e7915ab7 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageChunkedList.js @@ -13,11 +13,8 @@ export default { }, }, - generate(slots, {html}) { - return ( - html.tag('dl', [ - slots.groupInfo, - slots.chunks, - ])); - }, + generate: (slots, {html}) => + html.tag('dl', + {[html.onlyIfContent]: true}, + [slots.groupInfo, slots.chunks]), }; diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js index 0bcadc7c..88c5ed54 100644 --- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js @@ -1,184 +1,283 @@ -import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort'; import {chunkByProperties, stitchArrays} from '#sugar'; +import { + sortAlbumsTracksChronologically, + sortByDate, + sortEntryThingPairs, +} from '#sort'; + export default { contentDependencies: [ 'generateArtistInfoPageChunk', 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageOtherArtistLinks', 'linkAlbum', + 'linkFlash', + 'linkFlashAct', 'linkTrack', 'transformContent', ], extraDependencies: ['html', 'language'], - query(artist) { - const processEntry = ({thing, entry, type, track, album}) => ({ + query(artist, filterWikiEditorCommentary) { + const processEntry = ({ + thing, + entry, + + chunkType, + itemType, + + album = null, + track = null, + flashAct = null, + flash = null, + }) => ({ thing: thing, entry: { - type: type, - track: track, - album: album, + chunkType, + itemType, + + album, + track, + flashAct, + flash, + annotation: entry.annotation, + annotationParts: entry.annotationParts, }, }); - const processAlbumEntry = ({type, album, entry}) => + const processAlbumEntry = ({thing: album, entry}) => processEntry({ thing: album, entry: entry, - type: type, + + chunkType: 'album', + itemType: 'album', + album: album, track: null, }); - const processTrackEntry = ({type, track, entry}) => + const processTrackEntry = ({thing: track, entry}) => processEntry({ thing: track, entry: entry, - type: type, + + chunkType: 'album', + itemType: 'track', + album: track.album, track: track, }); + const processFlashEntry = ({thing: flash, entry}) => + processEntry({ + thing: flash, + entry: entry, + + chunkType: 'flash-act', + itemType: 'flash', + + flashAct: flash.act, + flash: flash, + }); + const processEntries = ({things, processEntry}) => things .flatMap(thing => thing.commentary .filter(entry => entry.artists.includes(artist)) + + .filter(entry => + (filterWikiEditorCommentary + ? entry.isWikiEditorCommentary + : !entry.isWikiEditorCommentary)) + .map(entry => processEntry({thing, entry}))); - const processAlbumEntries = ({type, albums}) => + const processAlbumEntries = ({albums}) => processEntries({ things: albums, - processEntry: ({thing, entry}) => - processAlbumEntry({ - type: type, - album: thing, - entry: entry, - }), + processEntry: processAlbumEntry, }); - const processTrackEntries = ({type, tracks}) => + const processTrackEntries = ({tracks}) => processEntries({ things: tracks, - processEntry: ({thing, entry}) => - processTrackEntry({ - type: type, - track: thing, - entry: entry, - }), + processEntry: processTrackEntry, }); - const {albumsAsCommentator, tracksAsCommentator} = artist; - - const trackEntries = - processTrackEntries({ - type: 'track', - tracks: tracksAsCommentator, + const processFlashEntries = ({flashes}) => + processEntries({ + things: flashes, + processEntry: processFlashEntry, }); + const { + albumsAsCommentator, + tracksAsCommentator, + flashesAsCommentator, + } = artist; + const albumEntries = processAlbumEntries({ - type: 'album', albums: albumsAsCommentator, }); - const entries = [ - ...albumEntries, - ...trackEntries, - ]; + const trackEntries = + processTrackEntries({ + tracks: tracksAsCommentator, + }); + + const flashEntries = + processFlashEntries({ + flashes: flashesAsCommentator, + }) - sortEntryThingPairs(entries, sortAlbumsTracksChronologically); + const albumTrackEntries = + sortEntryThingPairs( + [...albumEntries, ...trackEntries], + sortAlbumsTracksChronologically); + + const allEntries = + sortEntryThingPairs( + [...albumTrackEntries, ...flashEntries], + sortByDate); const chunks = chunkByProperties( - entries.map(({entry}) => entry), - ['album']); + allEntries.map(({entry}) => entry), + ['chunkType', 'album', 'flashAct']); return {chunks}; }, - relations(relation, query) { - return { - chunks: - query.chunks.map(() => relation('generateArtistInfoPageChunk')), + relations: (relation, query, _artist, filterWikiEditorCommentary) => ({ + chunks: + query.chunks + .map(() => relation('generateArtistInfoPageChunk')), - albumLinks: - query.chunks.map(({album}) => relation('linkAlbum', album)), + chunkLinks: + query.chunks + .map(({chunkType, album, flashAct}) => + (chunkType === 'album' + ? relation('linkAlbum', album) + : chunkType === 'flash-act' + ? relation('linkFlashAct', flashAct) + : null)), - items: - query.chunks.map(({chunk}) => - chunk.map(() => relation('generateArtistInfoPageChunkItem'))), + items: + query.chunks + .map(({chunk}) => chunk + .map(() => relation('generateArtistInfoPageChunkItem'))), - itemTrackLinks: - query.chunks.map(({chunk}) => - chunk.map(({track}) => + itemLinks: + query.chunks + .map(({chunk}) => chunk + .map(({track, flash}) => (track ? relation('linkTrack', track) + : flash + ? relation('linkFlash', flash) : null))), - itemAnnotations: - query.chunks.map(({chunk}) => - chunk.map(({annotation}) => - (annotation - ? relation('transformContent', annotation) - : null))), - }; - }, + itemAnnotations: + query.chunks + .map(({chunk}) => chunk + .map(entry => + relation('transformContent', + (filterWikiEditorCommentary + ? entry.annotationParts + .filter(part => part !== 'wiki editor') + .join(', ') + : entry.annotation)))), + }), - data(query) { - return { - itemTypes: - query.chunks.map(({chunk}) => - chunk.map(({type}) => type)), - }; - }, + data: (query, _artist, _filterWikiEditorCommentary) => ({ + chunkTypes: + query.chunks + .map(({chunkType}) => chunkType), + + itemTypes: + query.chunks + .map(({chunk}) => chunk + .map(({itemType}) => itemType)), + }), + + generate: (data, relations, {html, language}) => + html.tag('dl', + {[html.onlyIfContent]: true}, - generate(data, relations, {html, language}) { - return html.tag('dl', stitchArrays({ chunk: relations.chunks, - albumLink: relations.albumLinks, + chunkLink: relations.chunkLinks, + chunkType: data.chunkTypes, items: relations.items, - itemTrackLinks: relations.itemTrackLinks, + itemLinks: relations.itemLinks, itemAnnotations: relations.itemAnnotations, itemTypes: data.itemTypes, }).map(({ chunk, - albumLink, + chunkLink, + chunkType, items, - itemTrackLinks, + itemLinks, itemAnnotations, itemTypes, }) => - chunk.slots({ - mode: 'album', - albumLink, - items: - stitchArrays({ - item: items, - trackLink: itemTrackLinks, - annotation: itemAnnotations, - type: itemTypes, - }).map(({item, trackLink, annotation, type}) => - item.slots({ - annotation: - (annotation - ? annotation.slot('mode', 'inline') - : null), - - content: - (type === 'album' - ? html.tag('i', - language.$('artistPage.creditList.entry.album.commentary')) - : language.$('artistPage.creditList.entry.track', { - track: trackLink, - })), - })), - }))); - }, + language.encapsulate('artistPage.creditList.entry', capsule => + (chunkType === 'album' + ? chunk.slots({ + mode: 'album', + albumLink: chunkLink, + items: + stitchArrays({ + item: items, + link: itemLinks, + annotation: itemAnnotations, + type: itemTypes, + }).map(({item, link, annotation, type}) => + item.slots({ + annotation: + annotation.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }), + + content: + (type === 'album' + ? html.tag('i', + language.$(capsule, 'album.commentary')) + : language.$(capsule, 'track', {track: link})), + })), + }) + : chunkType === 'flash-act' + ? chunk.slots({ + mode: 'flash', + flashActLink: chunkLink, + items: + stitchArrays({ + item: items, + link: itemLinks, + annotation: itemAnnotations, + }).map(({item, link, annotation}) => + item.slots({ + annotation: + (annotation + ? annotation.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }) + : null), + + content: + language.$(capsule, 'flash', { + flash: link, + }), + })), + }) + : null)))), }; 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/generateArtistInfoPageFlashesChunk.js b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js new file mode 100644 index 00000000..8aa7223a --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js @@ -0,0 +1,34 @@ +export default { + contentDependencies: [ + 'generateArtistInfoPageChunk', + 'generateArtistInfoPageFlashesChunkItem', + 'linkFlashAct', + ], + + relations: (relation, flashAct, contribs) => ({ + template: + relation('generateArtistInfoPageChunk'), + + flashActLink: + relation('linkFlashAct', flashAct), + + items: + contribs + .map(contrib => + relation('generateArtistInfoPageFlashesChunkItem', contrib)), + }), + + data: (_flashAct, contribs) => ({ + dates: + contribs + .map(contrib => contrib.date), + }), + + generate: (data, relations) => + relations.template.slots({ + mode: 'flash', + flashActLink: relations.flashActLink, + dates: data.dates, + items: relations.items, + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js new file mode 100644 index 00000000..e4908bf9 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js @@ -0,0 +1,34 @@ +export default { + contentDependencies: ['generateArtistInfoPageChunkItem', 'linkFlash'], + + extraDependencies: ['language'], + + relations: (relation, contrib) => ({ + // Flashes and games can list multiple contributors as collaborative + // credits, but we don't display these on the artist page, since they + // usually involve many artists crediting a larger team where collaboration + // isn't as relevant (without more particular details that aren't tracked + // on the wiki). + + template: + relation('generateArtistInfoPageChunkItem'), + + flashLink: + relation('linkFlash', contrib.thing), + }), + + data: (contrib) => ({ + annotation: + contrib.annotation, + }), + + generate: (data, relations, {language}) => + relations.template.slots({ + annotation: data.annotation, + + content: + language.$('artistPage.creditList.entry.flash', { + flash: relations.flashLink, + }), + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js index 88a97af2..b347faf5 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js @@ -1,149 +1,62 @@ -import {sortEntryThingPairs, sortFlashesChronologically} from '#sort'; -import {chunkByProperties, stitchArrays} from '#sugar'; +import {sortContributionsChronologically, sortFlashesChronologically} + from '#sort'; +import {chunkByConditions, stitchArrays} from '#sugar'; export default { contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageChunkItem', - 'linkFlash', + 'generateArtistInfoPageChunkedList', + 'generateArtistInfoPageFlashesChunk', ], - extraDependencies: ['html', 'language'], + extraDependencies: ['wikiData'], - query(artist) { - const processFlashEntry = ({flash, contribs}) => ({ - thing: flash, - entry: { - flash: flash, - act: flash.act, - contribs: contribs, - }, - }); + sprawl: ({wikiInfo}) => ({ + enableFlashesAndGames: + wikiInfo.enableFlashesAndGames, + }), - const processFlashEntries = ({flashes, contribs}) => - stitchArrays({ - flash: flashes, - contribs: contribs, - }).map(processFlashEntry); - - const {flashesAsContributor} = artist; - - const flashesAsContributorContribs = - flashesAsContributor - .map(flash => flash.contributorContribs); - - const flashesAsContributorEntries = - processFlashEntries({ - flashes: flashesAsContributor, - contribs: flashesAsContributorContribs, - }); - - const entries = [ - ...flashesAsContributorEntries, - ]; - - sortEntryThingPairs(entries, sortFlashesChronologically); - - const chunks = - chunkByProperties( - entries.map(({entry}) => entry), - ['act']); - - return {chunks}; - }, + query(sprawl, artist) { + const query = {}; - relations(relation, query) { - // Flashes and games can list multiple contributors as collaborative - // credits, but we don't display these on the artist page, since they - // usually involve many artists crediting a larger team where collaboration - // isn't as relevant (without more particular details that aren't tracked - // on the wiki). + const allContributions = + (sprawl.enableFlashesAndGames + ? [ + ...artist.flashContributorContributions, + ] + : []); - return { - chunks: - query.chunks.map(() => relation('generateArtistInfoPageChunk')), + sortContributionsChronologically( + allContributions, + sortFlashesChronologically); - actLinks: - query.chunks.map(({chunk}) => - relation('linkFlash', chunk[0].flash)), + query.contribs = + chunkByConditions(allContributions, [ + ({thing: flash1}, {thing: flash2}) => + flash1.act !== flash2.act, + ]); - items: - query.chunks.map(({chunk}) => - chunk.map(() => relation('generateArtistInfoPageChunkItem'))), + query.flashActs = + query.contribs + .map(contribs => contribs[0].thing) + .map(thing => thing.act); - itemFlashLinks: - query.chunks.map(({chunk}) => - chunk.map(({flash}) => relation('linkFlash', flash))), - }; + return query; }, - data(query, artist) { - return { - actNames: - query.chunks.map(({act}) => act.name), + relations: (relation, query, _sprawl, _artist) => ({ + chunkedList: + relation('generateArtistInfoPageChunkedList'), - firstDates: - query.chunks.map(({chunk}) => chunk[0].flash.date ?? null), - - lastDates: - query.chunks.map(({chunk}) => chunk.at(-1).flash.date ?? null), - - itemContributions: - query.chunks.map(({chunk}) => - chunk.map(({contribs}) => - contribs - .find(({who}) => who === artist) - .what)), - }; - }, - - generate(data, relations, {html, language}) { - return html.tag('dl', + chunks: stitchArrays({ - chunk: relations.chunks, - actLink: relations.actLinks, - actName: data.actNames, - firstDate: data.firstDates, - lastDate: data.lastDates, - - items: relations.items, - itemFlashLinks: relations.itemFlashLinks, - itemContributions: data.itemContributions, - }).map(({ - chunk, - actLink, - actName, - firstDate, - lastDate, - - items, - itemFlashLinks, - itemContributions, - }) => - chunk.slots({ - mode: 'flash', - flashActLink: actLink.slot('content', actName), - dateRangeStart: firstDate, - dateRangeEnd: lastDate, - - items: - stitchArrays({ - item: items, - flashLink: itemFlashLinks, - contribution: itemContributions, - }).map(({ - item, - flashLink, - contribution, - }) => - item.slots({ - annotation: contribution, - - content: - language.$('artistPage.creditList.entry.flash', { - flash: flashLink, - }), - })), - }))); - }, + flashAct: query.flashActs, + contribs: query.contribs, + }).map(({flashAct, contribs}) => + relation('generateArtistInfoPageFlashesChunk', flashAct, contribs)), + }), + + generate: (relations) => + relations.chunkedList.slots({ + chunks: relations.chunks, + }), }; diff --git a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js index dea7742a..dcee9c00 100644 --- a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js +++ b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js @@ -1,23 +1,30 @@ -import {empty} from '#sugar'; +import {unique} from '#sugar'; export default { contentDependencies: ['linkArtist'], - relations(relation, contribs, artist) { - const otherArtistContribs = contribs.filter(({who}) => who !== artist); + query(contribs) { + const associatedContributionsByOtherArtists = + contribs + .flatMap(ownContrib => + ownContrib.associatedContributions + .filter(associatedContrib => + associatedContrib.artist !== ownContrib.artist)); - if (empty(otherArtistContribs)) { - return {}; - } + const otherArtists = + unique( + associatedContributionsByOtherArtists + .map(contrib => contrib.artist)); - const otherArtistLinks = - otherArtistContribs - .map(({who}) => relation('linkArtist', who)); - - return {otherArtistLinks}; + return {otherArtists}; }, - generate(relations) { - return relations.otherArtistLinks ?? null; - }, + relations: (relation, query) => ({ + artistLinks: + query.otherArtists + .map(artist => relation('linkArtist', artist)), + }), + + generate: (relations) => + relations.artistLinks, }; diff --git a/src/content/dependencies/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 new file mode 100644 index 00000000..f6d70901 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js @@ -0,0 +1,67 @@ +import {unique} from '#sugar'; +import {getTotalDuration} from '#wiki-data'; + +export default { + contentDependencies: [ + 'generateArtistInfoPageChunk', + 'generateArtistInfoPageTracksChunkItem', + 'linkAlbum', + ], + + relations: (relation, artist, album, trackContribLists) => ({ + template: + relation('generateArtistInfoPageChunk'), + + albumLink: + relation('linkAlbum', album), + + // Intentional mapping here: each item may be associated with + // more than one contribution. + items: + trackContribLists.map(trackContribs => + relation('generateArtistInfoPageTracksChunkItem', + artist, + trackContribs)), + }), + + data(_artist, album, trackContribLists) { + const data = {}; + + const contribs = + trackContribLists.flat(); + + data.dates = + contribs + .map(contrib => contrib.date); + + // TODO: Duration stuff should *maybe* be in proper data logic? Maaaybe? + const durationTerms = + unique( + contribs + .filter(contrib => contrib.countInDurationTotals) + .map(contrib => contrib.thing) + .filter(track => track.isMainRelease) + .filter(track => track.duration > 0)); + + data.duration = + getTotalDuration(durationTerms); + + data.durationApproximate = + durationTerms.length > 1; + + return data; + }, + + generate: (data, relations) => + relations.template.slots({ + mode: 'album', + + albumLink: relations.albumLink, + + dates: data.dates, + duration: data.duration, + durationApproximate: data.durationApproximate, + + items: relations.items, + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js new file mode 100644 index 00000000..a42d6fee --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js @@ -0,0 +1,146 @@ +import {sortChronologically} from '#sort'; +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generateArtistInfoPageChunkItem', + 'generateArtistInfoPageFirstReleaseTooltip', + 'generateArtistInfoPageOtherArtistLinks', + 'generateArtistInfoPageRereleaseTooltip', + 'linkTrack', + ], + + extraDependencies: ['html', 'language'], + + query (_artist, contribs) { + const query = {}; + + // TODO: Very mysterious what to do if the set of contributions is, + // in total, associated with more than one thing. No design yet. + query.track = + contribs[0].thing; + + const creditedAsArtist = + contribs + .some(contrib => contrib.isArtistContribution); + + const creditedAsContributor = + contribs + .some(contrib => contrib.isContributorContribution); + + const annotatedContribs = + contribs + .filter(contrib => contrib.annotation); + + const annotatedArtistContribs = + annotatedContribs + .filter(contrib => contrib.isArtistContribution); + + const annotatedContributorContribs = + annotatedContribs + .filter(contrib => contrib.isContributorContribution); + + // Don't display annotations associated with crediting in the + // Contributors field if the artist is also credited as an Artist + // *and* the Artist-field contribution is non-annotated. This is + // so that we don't misrepresent the artist - the contributor + // annotation tends to be for "secondary" and performance roles. + // For example, this avoids crediting Marcy Nabors on Renewed + // Return seemingly only for "bass clarinet" when they're also + // the one who composed and arranged Renewed Return! + if ( + creditedAsArtist && + creditedAsContributor && + empty(annotatedArtistContribs) + ) { + query.displayedContributions = null; + } else if ( + !empty(annotatedArtistContribs) || + !empty(annotatedContributorContribs) + ) { + query.displayedContributions = [ + ...annotatedArtistContribs, + ...annotatedContributorContribs, + ]; + } + + // 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; + }, + + relations: (relation, query, artist, contribs) => ({ + template: + relation('generateArtistInfoPageChunkItem'), + + trackLink: + relation('linkTrack', query.track), + + 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, + + contribAnnotations: + (query.displayedContributions + ? query.displayedContributions + .map(contrib => contrib.annotation) + : null), + }), + + generate: (data, relations, {html, language}) => + relations.template.slots({ + otherArtistLinks: relations.otherArtistLinks, + rereleaseTooltip: relations.rereleaseTooltip, + firstReleaseTooltip: relations.firstReleaseTooltip, + + annotation: + (data.contribAnnotations + ? language.formatUnitList(data.contribAnnotations) + : html.blank()), + + content: + language.encapsulate('artistPage.creditList.entry.track', workingCapsule => { + const workingOptions = {track: relations.trackLink}; + + if (data.duration) { + workingCapsule += '.withDuration'; + workingOptions.duration = + language.formatDuration(data.duration); + } + + return language.$(workingCapsule, workingOptions); + }), + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index f003779d..84eb29ac 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -1,293 +1,81 @@ -import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort'; -import {accumulateSum, chunkByProperties, empty, stitchArrays} from '#sugar'; +import {sortAlbumsTracksChronologically, sortContributionsChronologically} + from '#sort'; +import {stitchArrays} from '#sugar'; +import {chunkArtistTrackContributions} from '#wiki-data'; export default { contentDependencies: [ - 'generateArtistInfoPageChunk', 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageOtherArtistLinks', - 'linkAlbum', - 'linkTrack', + 'generateArtistInfoPageTracksChunk', ], - extraDependencies: ['html', 'language'], - query(artist) { - const processTrackEntry = ({track, contribs}) => ({ - thing: track, - entry: { - track: track, - album: track.album, - date: track.date, - contribs: contribs, - }, - }); - - const processTrackEntries = ({tracks, contribs}) => - stitchArrays({ - track: tracks, - contribs: contribs, - }).map(processTrackEntry); - - const {tracksAsArtist, tracksAsContributor} = artist; - - const tracksAsArtistAndContributor = - tracksAsArtist - .filter(track => tracksAsContributor.includes(track)); - - const tracksAsArtistOnly = - tracksAsArtist - .filter(track => !tracksAsContributor.includes(track)); - - const tracksAsContributorOnly = - tracksAsContributor - .filter(track => !tracksAsArtist.includes(track)); - - const tracksAsArtistAndContributorContribs = - tracksAsArtistAndContributor - .map(track => [ - ... - track.artistContribs - .map(contrib => ({...contrib, kind: 'artist'})), - ... - track.contributorContribs - .map(contrib => ({...contrib, kind: 'contributor'})), - ]); - - const tracksAsArtistOnlyContribs = - tracksAsArtistOnly - .map(track => track.artistContribs - .map(contrib => ({...contrib, kind: 'artist'}))); - - const tracksAsContributorOnlyContribs = - tracksAsContributorOnly - .map(track => track.contributorContribs - .map(contrib => ({...contrib, kind: 'contributor'}))); + const query = {}; - const tracksAsArtistAndContributorEntries = - processTrackEntries({ - tracks: tracksAsArtistAndContributor, - contribs: tracksAsArtistAndContributorContribs, - }); - - const tracksAsArtistOnlyEntries = - processTrackEntries({ - tracks: tracksAsArtistOnly, - contribs: tracksAsArtistOnlyContribs, - }); - - const tracksAsContributorOnlyEntries = - processTrackEntries({ - tracks: tracksAsContributorOnly, - contribs: tracksAsContributorOnlyContribs, - }); - - const entries = [ - ...tracksAsArtistAndContributorEntries, - ...tracksAsArtistOnlyEntries, - ...tracksAsContributorOnlyEntries, + const allContributions = [ + ...artist.trackArtistContributions, + ...artist.trackContributorContributions, ]; - sortEntryThingPairs(entries, sortAlbumsTracksChronologically); - - const chunks = - chunkByProperties( - entries.map(({entry}) => entry), - ['album', 'date']); - - return {chunks}; - }, - - relations(relation, query, artist) { - return { - chunkedList: - relation('generateArtistInfoPageChunkedList'), - - chunks: - query.chunks.map(() => relation('generateArtistInfoPageChunk')), - - albumLinks: - query.chunks.map(({album}) => relation('linkAlbum', album)), + sortContributionsChronologically( + allContributions, + sortAlbumsTracksChronologically); - items: - query.chunks.map(({chunk}) => - chunk.map(() => relation('generateArtistInfoPageChunkItem'))), + query.contribs = + chunkArtistTrackContributions(allContributions); - trackLinks: - query.chunks.map(({chunk}) => - chunk.map(({track}) => relation('linkTrack', track))), + query.albums = + query.contribs + .map(contribs => + contribs[0][0].thing.album); - trackOtherArtistLinks: - query.chunks.map(({chunk}) => - chunk.map(({contribs}) => relation('generateArtistInfoPageOtherArtistLinks', contribs, artist))), - }; + return query; }, - data(query, artist) { - return { - chunkDates: - query.chunks.map(({date}) => date), - - chunkDurations: - query.chunks.map(({chunk}) => - accumulateSum( - chunk - .filter(({track}) => track.duration && track.originalReleaseTrack === null) - .map(({track}) => track.duration))), - - chunkDurationsApproximate: - query.chunks.map(({chunk}) => - chunk - .filter(({track}) => track.duration && track.originalReleaseTrack === null) - .length > 1), - - trackDurations: - query.chunks.map(({chunk}) => - chunk.map(({track}) => track.duration)), - - trackContributions: - query.chunks.map(({chunk}) => - chunk - .map(({contribs}) => - contribs.filter(({who}) => who === artist)) - .map(ownContribs => ({ - creditedAsArtist: - ownContribs - .some(({kind}) => kind === 'artist'), - - creditedAsContributor: - ownContribs - .some(({kind}) => kind === 'contributor'), - - annotatedContribs: - ownContribs - .filter(({what}) => what), - })) - .map(({annotatedContribs, ...rest}) => ({ - ...rest, - - annotatedArtistContribs: - annotatedContribs - .filter(({kind}) => kind === 'artist'), - - annotatedContributorContribs: - annotatedContribs - .filter(({kind}) => kind === 'contributor'), - })) - .map(({ - creditedAsArtist, - creditedAsContributor, - annotatedArtistContribs, - annotatedContributorContribs, - }) => { - // Don't display annotations associated with crediting in the - // Contributors field if the artist is also credited as an Artist - // *and* the Artist-field contribution is non-annotated. This is - // so that we don't misrepresent the artist - the contributor - // annotation tends to be for "secondary" and performance roles. - // For example, this avoids crediting Marcy Nabors on Renewed - // Return seemingly only for "bass clarinet" when they're also - // the one who composed and arranged Renewed Return! - if ( - creditedAsArtist && - creditedAsContributor && - empty(annotatedArtistContribs) - ) { - return []; - } - - return [ - ...annotatedArtistContribs, - ...annotatedContributorContribs, - ]; - }) - .map(contribs => - contribs.map(({what}) => what)) - .map(contributions => - (empty(contributions) - ? null - : contributions))), + relations: (relation, query, artist) => ({ + chunkedList: + relation('generateArtistInfoPageChunkedList'), - trackRereleases: - query.chunks.map(({chunk}) => - chunk.map(({track}) => track.originalReleaseTrack !== null)), - }; - }, - - generate(data, relations, {html, language}) { - return relations.chunkedList.slots({ + chunks: + stitchArrays({ + album: query.albums, + contribs: query.contribs, + }).map(({album, contribs}) => + relation('generateArtistInfoPageTracksChunk', + artist, + album, + contribs)), + }), + + 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: stitchArrays({ chunk: relations.chunks, - albumLink: relations.albumLinks, - date: data.chunkDates, - duration: data.chunkDurations, - durationApproximate: data.chunkDurationsApproximate, - - items: relations.items, - trackLinks: relations.trackLinks, - trackOtherArtistLinks: relations.trackOtherArtistLinks, - trackDurations: data.trackDurations, - trackContributions: data.trackContributions, - trackRereleases: data.trackRereleases, - }).map(({ - chunk, - albumLink, - date, - duration, - durationApproximate, - - items, - trackLinks, - trackOtherArtistLinks, - trackDurations, - trackContributions, - trackRereleases, - }) => - chunk.slots({ - mode: 'album', - albumLink, - date, - duration, - durationApproximate, - - items: - stitchArrays({ - item: items, - trackLink: trackLinks, - otherArtistLinks: trackOtherArtistLinks, - duration: trackDurations, - contribution: trackContributions, - rerelease: trackRereleases, - }).map(({ - item, - trackLink, - otherArtistLinks, - duration, - contribution, - rerelease, - }) => - item.slots({ - otherArtistLinks, - rerelease, - - annotation: - (contribution - ? language.formatUnitList(contribution) - : html.blank()), - - content: - (duration - ? language.$('artistPage.creditList.entry.track.withDuration', { - track: trackLink, - duration: language.formatDuration(duration), - }) - : language.$('artistPage.creditList.entry.track', { - track: trackLink, - })), - })), - })), - }); - }, + 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 aa95dba2..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.albumsAsCoverArtist) || - !empty(artist.tracksAsCoverArtist) - ) { - 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/generateChronologyLinks.js b/src/content/dependencies/generateChronologyLinks.js deleted file mode 100644 index 8ec6ee0a..00000000 --- a/src/content/dependencies/generateChronologyLinks.js +++ /dev/null @@ -1,82 +0,0 @@ -import {accumulateSum, empty} from '#sugar'; - -export default { - extraDependencies: ['html', 'language'], - - slots: { - chronologyInfoSets: { - validate: v => - v.strictArrayOf( - v.validateProperties({ - headingString: v.isString, - contributions: v.strictArrayOf(v.validateProperties({ - index: v.isCountingNumber, - artistLink: v.isHTML, - previousLink: v.isHTML, - nextLink: v.isHTML, - })), - })), - } - }, - - generate(slots, {html, language}) { - if (empty(slots.chronologyInfoSets)) { - return html.blank(); - } - - const totalContributionCount = - accumulateSum( - slots.chronologyInfoSets, - ({contributions}) => contributions.length); - - if (totalContributionCount === 0) { - return html.blank(); - } - - if (totalContributionCount > 8) { - return html.tag('div', {class: 'chronology'}, - language.$('misc.chronology.seeArtistPages')); - } - - return html.tags( - slots.chronologyInfoSets.map(({ - headingString, - contributions, - }) => - contributions.map(({ - index, - artistLink, - previousLink, - nextLink, - }) => { - const heading = - html.tag('span', {class: 'heading'}, - language.$(headingString, { - index: language.formatIndex(index), - artist: artistLink, - })); - - const navigation = - (previousLink || nextLink) && - html.tag('span', {class: 'buttons'}, - language.formatUnitList([ - previousLink?.slots({ - tooltipStyle: 'browser', - color: false, - content: language.$('misc.nav.previous'), - }), - - nextLink?.slots({ - tooltipStyle: 'browser', - color: false, - content: language.$('misc.nav.next'), - }), - ].filter(Boolean))); - - return html.tag('div', {class: 'chronology'}, - (navigation - ? language.$('misc.chronology.withNavigation', {heading, navigation}) - : heading)); - }))); - }, -}; diff --git a/src/content/dependencies/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 069d85dd..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', }, }, @@ -32,6 +32,7 @@ export default { dim, deep, deepGhost, + lightGhost, bg, bgBlack, shadow, @@ -43,20 +44,21 @@ export default { `--dim-color: ${dim}`, `--deep-color: ${deep}`, `--deep-ghost-color: ${deepGhost}`, + `--light-ghost-color: ${lightGhost}`, `--bg-color: ${bg}`, `--bg-black-color: ${bgBlack}`, `--shadow-color: ${shadow}`, ]; - 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}`, @@ -65,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; @@ -80,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 522a0284..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,66 +34,79 @@ 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}) { - const artistsSpan = - html.tag('span', {class: 'commentary-entry-artists'}, - (relations.artistsContent - ? relations.artistsContent.slot('mode', 'inline') - : relations.artistLinks - ? language.formatConjunctionList(relations.artistLinks) - : language.$('misc.artistCommentary.entry.title.noArtists'))); - - const accentParts = ['misc.artistCommentary.entry.title.accent']; - const accentOptions = {}; - - if (relations.annotationContent) { - accentParts.push('withAnnotation'); - accentOptions.annotation = - relations.annotationContent.slot('mode', 'inline'); - } - - if (data.date) { - accentParts.push('withDate'); - accentOptions.date = - language.formatDate(data.date); - } - - const accent = - (accentParts.length > 1 - ? html.tag('span', {class: 'commentary-entry-accent'}, - language.$(...accentParts, accentOptions)) - : null); - - const titleParts = ['misc.artistCommentary.entry.title']; - const titleOptions = {artists: artistsSpan}; - - if (accent) { - titleParts.push('withAccent'); - titleOptions.accent = accent; - } - - const style = - slots.color && - relations.colorStyle.slot('color', slots.color); - - return html.tags([ - html.tag('p', {class: 'commentary-entry-heading'}, - style, - language.$(...titleParts, titleOptions)), - - html.tag('blockquote', {class: 'commentary-entry-body'}, - style, - relations.bodyContent.slot('mode', 'multiline')), - ]); - }, + generate: (relations, slots, {html, language}) => + language.encapsulate('misc.artistCommentary.entry', entryCapsule => + html.tags([ + html.tag('p', {class: 'commentary-entry-heading'}, + slots.color && + relations.colorStyle.clone() + .slot('color', slots.color), + + !html.isBlank(relations.date) && + {class: 'dated'}, + + 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); + })), + + relations.date, + ])), + + html.tag('blockquote', {class: 'commentary-entry-body'}, + slots.color && + relations.colorStyle.clone() + .slot('color', slots.color), + + relations.bodyContent.slot('mode', 'multiline')), + ])), }; diff --git a/src/content/dependencies/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/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js index 3c3504d2..d68ba42e 100644 --- a/src/content/dependencies/generateCommentaryIndexPage.js +++ b/src/content/dependencies/generateCommentaryIndexPage.js @@ -57,46 +57,48 @@ export default { }; }, - generate(data, relations, {html, language}) { - return relations.layout.slots({ - title: language.$('commentaryIndex.title'), - - headingMode: 'static', - - mainClasses: ['long-content'], - mainContent: [ - html.tag('p', language.$('commentaryIndex.infoLine', { - words: - html.tag('b', - language.formatWordCount(data.totalWordCount, {unit: true})), - - entries: - html.tag('b', - language.countCommentaryEntries(data.totalEntryCount, {unit: true})), - })), - - html.tag('p', - language.$('commentaryIndex.albumList.title')), - - html.tag('ul', - stitchArrays({ - albumLink: relations.albumLinks, - wordCount: data.wordCounts, - entryCount: data.entryCounts, - }).map(({albumLink, wordCount, entryCount}) => - html.tag('li', - language.$('commentaryIndex.albumList.item', { - album: albumLink, - words: language.formatWordCount(wordCount, {unit: true}), - entries: language.countCommentaryEntries(entryCount, {unit: true}), - })))), - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {auto: 'current'}, - ], - }); - }, + generate: (data, relations, {html, language}) => + language.encapsulate('commentaryIndex', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title'), + + headingMode: 'static', + + mainClasses: ['long-content'], + mainContent: [ + html.tag('p', language.$(pageCapsule, 'infoLine', { + words: + html.tag('b', + language.formatWordCount(data.totalWordCount, {unit: true})), + + entries: + html.tag('b', + language.countCommentaryEntries(data.totalEntryCount, {unit: true})), + })), + + language.encapsulate(pageCapsule, 'albumList', listCapsule => [ + html.tag('p', + language.$(listCapsule, 'title')), + + html.tag('ul', + stitchArrays({ + albumLink: relations.albumLinks, + wordCount: data.wordCounts, + entryCount: data.entryCounts, + }).map(({albumLink, wordCount, entryCount}) => + html.tag('li', + language.$(listCapsule, 'item', { + album: albumLink, + words: language.formatWordCount(wordCount, {unit: true}), + entries: language.countCommentaryEntries(entryCount, {unit: true}), + })))), + ]), + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {auto: 'current'}, + ], + })), }; diff --git a/src/content/dependencies/generateCommentarySection.js b/src/content/dependencies/generateCommentarySection.js deleted file mode 100644 index 8ae1b2d0..00000000 --- a/src/content/dependencies/generateCommentarySection.js +++ /dev/null @@ -1,29 +0,0 @@ -export default { - contentDependencies: [ - 'transformContent', - 'generateCommentaryEntry', - 'generateContentHeading', - ], - - extraDependencies: ['html', 'language'], - - relations: (relation, entries) => ({ - heading: - relation('generateContentHeading'), - - entries: - entries.map(entry => - relation('generateCommentaryEntry', entry)), - }), - - generate: (relations, {html, language}) => - html.tags([ - relations.heading - .slots({ - id: 'artist-commentary', - title: language.$('misc.artistCommentary') - }), - - 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/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js index 469db876..f52bc043 100644 --- a/src/content/dependencies/generateContentHeading.js +++ b/src/content/dependencies/generateContentHeading.js @@ -12,23 +12,35 @@ export default { mutable: false, }, + stickyTitle: { + type: 'html', + mutable: false, + }, + accent: { type: 'html', mutable: false, }, + attributes: { + type: 'attributes', + mutable: false, + }, + color: {validate: v => v.isColor}, - id: {type: 'string'}, - tag: {type: 'string', default: 'p'}, + tag: { + type: 'string', + default: 'p', + }, }, generate: (relations, slots, {html}) => html.tag(slots.tag, {class: 'content-heading'}, {tabindex: '0'}, + {[html.onlyIfSiblings]: true}, - slots.id && - {id: slots.id}, + slots.attributes, slots.color && relations.colorStyle.slot('color', slots.color), @@ -38,6 +50,10 @@ export default { {[html.onlyIfContent]: true}, slots.title), + html.tag('template', {class: 'content-heading-sticky-title'}, + {[html.onlyIfContent]: true}, + slots.stickyTitle), + html.tag('span', {class: 'content-heading-accent'}, {[html.onlyIfContent]: true}, slots.accent), diff --git a/src/content/dependencies/generateContributionList.js b/src/content/dependencies/generateContributionList.js index 6401e65e..d1c3de0f 100644 --- a/src/content/dependencies/generateContributionList.js +++ b/src/content/dependencies/generateContributionList.js @@ -2,20 +2,28 @@ export default { contentDependencies: ['linkContribution'], extraDependencies: ['html'], - relations: (relation, contributions) => - ({contributionLinks: - contributions - .map(contrib => relation('linkContribution', contrib))}), + relations: (relation, contributions) => ({ + contributionLinks: + contributions + .map(contrib => relation('linkContribution', contrib)), + }), - generate: (relations, {html}) => + slots: { + chronologyKind: {type: 'string'}, + }, + + generate: (relations, slots, {html}) => html.tag('ul', - relations.contributionLinks.map(contributionLink => - html.tag('li', - contributionLink - .slots({ - showIcons: true, - showContribution: true, + {[html.onlyIfContent]: true}, + + relations.contributionLinks + .map(contributionLink => + html.tag('li', + contributionLink.slots({ + showAnnotation: true, + showExternalLinks: true, + showChronology: true, preventWrapping: false, - iconMode: 'tooltip', + chronologyKind: slots.chronologyKind, })))), }; diff --git a/src/content/dependencies/generateContributionTooltip.js b/src/content/dependencies/generateContributionTooltip.js new file mode 100644 index 00000000..3a31014d --- /dev/null +++ b/src/content/dependencies/generateContributionTooltip.js @@ -0,0 +1,48 @@ +export default { + contentDependencies: [ + 'generateContributionTooltipChronologySection', + 'generateContributionTooltipExternalLinkSection', + 'generateTooltip', + ], + + extraDependencies: ['html'], + + relations: (relation, contribution) => ({ + tooltip: + relation('generateTooltip'), + + externalLinkSection: + relation('generateContributionTooltipExternalLinkSection', contribution), + + chronologySection: + relation('generateContributionTooltipChronologySection', contribution), + }), + + slots: { + showExternalLinks: {type: 'boolean'}, + showChronology: {type: 'boolean'}, + + chronologyKind: {type: 'string'}, + }, + + generate: (relations, slots, {html}) => + relations.tooltip.slots({ + attributes: + {class: 'contribution-tooltip'}, + + contentAttributes: { + [html.joinChildren]: + html.tag('span', {class: 'tooltip-divider'}), + }, + + content: [ + slots.showExternalLinks && + relations.externalLinkSection, + + slots.showChronology && + relations.chronologySection.slots({ + kind: slots.chronologyKind, + }), + ], + }), +}; diff --git a/src/content/dependencies/generateContributionTooltipChronologySection.js b/src/content/dependencies/generateContributionTooltipChronologySection.js new file mode 100644 index 00000000..378c0e1c --- /dev/null +++ b/src/content/dependencies/generateContributionTooltipChronologySection.js @@ -0,0 +1,129 @@ +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'], + + query(contribution) { + let previous = contribution; + while (previous && previous.thing === contribution.thing) { + previous = previous.previousBySameArtist; + } + + let next = contribution; + while (next && next.thing === contribution.thing) { + next = next.nextBySameArtist; + } + + return {previous, next}; + }, + + relations: (relation, query, _contribution) => ({ + previousLink: + (query.previous + ? relation('linkAnythingMan', query.previous.thing) + : null), + + nextLink: + (query.next + ? relation('linkAnythingMan', query.next.thing) + : null), + }), + + data: (query, _contribution) => ({ + previousName: + getName(query.previous?.thing), + + nextName: + getName(query.next?.thing), + }), + + slots: { + kind: { + validate: v => + v.is( + 'album', + 'bannerArt', + 'coverArt', + 'flash', + 'track', + 'trackArt', + 'trackContribution', + 'wallpaperArt'), + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('misc.artistLink.chronology', capsule => + html.tags([ + html.tags([ + relations.previousLink?.slots({ + attributes: {class: 'chronology-link'}, + content: [ + html.tag('span', {class: 'chronology-symbol'}, + language.$(capsule, 'previous.symbol')), + + html.tag('span', {class: 'chronology-text'}, + language.sanitize(data.previousName)), + ], + }), + + html.tag('span', {class: 'chronology-info'}, + {[html.onlyIfSiblings]: true}, + + language.encapsulate(capsule, 'previous.info', workingCapsule => { + const workingOptions = {}; + + if (slots.kind) { + workingCapsule += '.withKind'; + workingOptions.kind = + language.$(capsule, 'kind', slots.kind); + } + + return language.$(workingCapsule, workingOptions); + })), + ]), + + html.tags([ + relations.nextLink?.slots({ + attributes: {class: 'chronology-link'}, + content: [ + html.tag('span', {class: 'chronology-symbol'}, + language.$(capsule, 'next.symbol')), + + html.tag('span', {class: 'chronology-text'}, + language.sanitize(data.nextName)), + ], + }), + + html.tag('span', {class: 'chronology-info'}, + {[html.onlyIfSiblings]: true}, + + language.encapsulate(capsule, 'next.info', workingCapsule => { + const workingOptions = {}; + + if (slots.kind) { + workingCapsule += '.withKind'; + workingOptions.kind = + language.$(capsule, 'kind', slots.kind); + } + + return language.$(workingCapsule, workingOptions); + })) + ]), + ])), +}; diff --git a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js new file mode 100644 index 00000000..4f9a23ed --- /dev/null +++ b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js @@ -0,0 +1,70 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateExternalHandle', + 'generateExternalIcon', + 'generateExternalPlatform', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, contribution) => ({ + icons: + contribution.artist.urls + .map(url => relation('generateExternalIcon', url)), + + handles: + contribution.artist.urls + .map(url => relation('generateExternalHandle', url)), + + platforms: + contribution.artist.urls + .map(url => relation('generateExternalPlatform', url)), + }), + + data: (contribution) => ({ + urls: contribution.artist.urls, + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('misc.artistLink', capsule => + html.tags( + stitchArrays({ + icon: relations.icons, + handle: relations.handles, + platform: relations.platforms, + url: data.urls, + }).map(({icon, handle, platform, url}) => { + for (const template of [icon, handle, platform]) { + template.setSlot('context', 'artist'); + } + + return [ + html.tag('a', {class: 'external-link'}, + {href: url}, + + [ + icon, + + html.tag('span', {class: 'external-handle'}, + (html.isBlank(handle) + ? platform + : handle)), + ]), + + html.tag('span', {class: 'external-platform'}, + // This is a pretty ridiculous hack, but we currently + // don't have a way of telling formatExternalLink to *not* + // use the fallback string, which just formats the URL as + // its host/domain... so is technically detectable. + (((new URL(url)) + .host + .endsWith( + html.resolve(platform, {normalize: 'string'}))) + + ? language.$(capsule, 'noExternalLinkPlatformName') + : platform)), + ]; + }))), +}; diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index d0941d2e..78a6103b 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -1,118 +1,157 @@ -import {empty, 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: { validate: v => v.is('primary', 'thumbnail', 'commentary'), default: 'primary', }, + + 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}) { - 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, - square: true, - }), - - !empty(relations.tagLinks) && - html.tag('ul', {class: 'image-details'}, - stitchArrays({ - tagLink: relations.tagLinks, - preferShortName: data.preferShortName, - }).map(({tagLink, preferShortName}) => - html.tag('li', - tagLink.slot('preferShortName', preferShortName)))), - ]); - - case 'thumbnail': - return relations.image.slots({ - path: slots.path, - alt: slots.alt, - color: slots.color, - thumb: 'small', - reveal: false, - link: false, - square: true, - }); - - case 'commentary': - return relations.image.slots({ - path: slots.path, - alt: slots.alt, - color: slots.color, - thumb: 'medium', - reveal: true, - link: true, - square: true, - lazy: true, - - attributes: - {class: 'commentary-art'}, - }); - - default: - return html.blank(); + const {image} = relations; + + image.setSlot('alt', slots.alt); + + const square = + (data.dimensions + ? data.dimensions[0] === data.dimensions[1] + : true); + + 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/generateExternalHandle.js b/src/content/dependencies/generateExternalHandle.js new file mode 100644 index 00000000..8c0368a4 --- /dev/null +++ b/src/content/dependencies/generateExternalHandle.js @@ -0,0 +1,20 @@ +import {isExternalLinkContext} from '#external-links'; + +export default { + extraDependencies: ['html', 'language'], + + data: (url) => ({url}), + + slots: { + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, slots, {language}) => + language.formatExternalLink(data.url, { + style: 'handle', + context: slots.context, + }), +}; diff --git a/src/content/dependencies/generateExternalIcon.js b/src/content/dependencies/generateExternalIcon.js new file mode 100644 index 00000000..637af658 --- /dev/null +++ b/src/content/dependencies/generateExternalIcon.js @@ -0,0 +1,26 @@ +import {isExternalLinkContext} from '#external-links'; + +export default { + extraDependencies: ['html', 'language', 'to'], + + data: (url) => ({url}), + + slots: { + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, slots, {html, language, to}) => + html.tag('span', {class: 'external-icon'}, + html.tag('svg', + html.tag('use', { + href: + to('staticMisc.icon', + language.formatExternalLink(data.url, { + style: 'icon-id', + context: slots.context, + })), + }))), +}; diff --git a/src/content/dependencies/generateExternalPlatform.js b/src/content/dependencies/generateExternalPlatform.js new file mode 100644 index 00000000..c4f63ecf --- /dev/null +++ b/src/content/dependencies/generateExternalPlatform.js @@ -0,0 +1,20 @@ +import {isExternalLinkContext} from '#external-links'; + +export default { + extraDependencies: ['html', 'language'], + + data: (url) => ({url}), + + slots: { + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, slots, {language}) => + language.formatExternalLink(data.url, { + style: 'platform', + context: slots.context, + }), +}; diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js index 8eea58bb..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,10 +8,11 @@ export default { 'generatePageLayout', 'image', 'linkFlash', + 'linkFlashAct', 'linkFlashIndex', ], - extraDependencies: ['html', 'language'], + extraDependencies: ['language'], relations: (relation, act) => ({ layout: @@ -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,48 +48,38 @@ export default { flashNames: act.flashes.map(flash => flash.name), - - flashCoverPaths: - act.flashes.map(flash => - ['media.flashArt', flash.directory, flash.coverArtFileExtension]) }), - generate(data, relations, {html, language}) { - return relations.layout.slots({ - title: - language.$('flashPage.title', { - flash: new html.Tag(null, null, data.name), - }), - - color: data.color, - headingMode: 'static', - - mainClasses: ['flash-index'], - mainContent: [ - relations.coverGrid.slots({ - links: relations.flashLinks, - names: data.flashNames, - lazy: 6, - - images: - stitchArrays({ - image: relations.coverGridImages, - path: data.flashCoverPaths, - }).map(({image, path}) => - image.slot('path', path)), - }), - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {html: relations.flashIndexLink}, - {auto: 'current'}, - ], - - navBottomRowContent: relations.flashActNavAccent, - - ...relations.sidebar, - }); - }, + generate: (data, relations, {language}) => + language.encapsulate('flashPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + flash: striptags(data.name), + }), + + color: data.color, + headingMode: 'static', + + mainClasses: ['flash-index'], + mainContent: [ + relations.coverGrid.slots({ + links: relations.flashLinks, + images: relations.coverGridImages, + names: data.flashNames, + lazy: 6, + }), + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {html: relations.flashIndexLink}, + {html: relations.flashActNavLink}, + ], + + navBottomRowContent: relations.flashActNavAccent, + + leftSidebar: relations.sidebar, + })), }; 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/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js index 0bbfa1f8..1421dde9 100644 --- a/src/content/dependencies/generateFlashActSidebar.js +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -1,216 +1,30 @@ -import find from '#find'; -import {filterMultipleArrays, stitchArrays} from '#sugar'; - export default { - contentDependencies: ['linkFlash', 'linkFlashAct', 'linkFlashIndex'], - extraDependencies: ['getColors', 'html', 'language', 'wikiData'], - - // So help me Gog, the flash sidebar is heavily hard-coded. - - sprawl: ({flashActData}) => ({flashActData}), - - query(sprawl, act, flash) { - const findFlashAct = directory => - find.flashAct(directory, sprawl.flashActData, {mode: 'quiet'}); - - const homestuckSide1 = findFlashAct('flash-act:a1'); - - const sideFirstActs = [ - sprawl.flashActData[0], - findFlashAct('flash-act:a6a1'), - findFlashAct('flash-act:hiveswap'), - findFlashAct('flash-act:cool-and-new-web-comic'), - findFlashAct('flash-act:sunday-night-strifin'), - ]; - - const sideNames = [ - (homestuckSide1 - ? `Side 1 (Acts 1-5)` - : `All flashes & games`), - `Side 2 (Acts 6-7)`, - `Additional Canon`, - `Fan Adventures`, - `Fan Games & More`, - ]; - - const sideColors = [ - (homestuckSide1 - ? '#4ac925' - : null), - '#3796c6', - '#f2a400', - '#c466ff', - '#32c7fe', - ]; - - filterMultipleArrays(sideFirstActs, sideNames, sideColors, - firstAct => firstAct); - - const sideFirstActIndexes = - sideFirstActs - .map(act => sprawl.flashActData.indexOf(act)); - - const actSideIndexes = - sprawl.flashActData - .map((act, actIndex) => actIndex) - .map(actIndex => - sideFirstActIndexes - .findIndex((firstActIndex, i) => - i === sideFirstActs.length - 1 || - firstActIndex <= actIndex && - sideFirstActIndexes[i + 1] > actIndex)); - - const sideActs = - sideNames - .map((name, sideIndex) => - stitchArrays({ - act: sprawl.flashActData, - actSideIndex: actSideIndexes, - }).filter(({actSideIndex}) => actSideIndex === sideIndex) - .map(({act}) => act)); - - const currentActFlashes = - act.flashes; - - const currentFlashIndex = - currentActFlashes.indexOf(flash); - - const currentSideIndex = - actSideIndexes[sprawl.flashActData.indexOf(act)]; - - const currentSideActs = - sideActs[currentSideIndex]; - - const currentActIndex = - currentSideActs.indexOf(act); - - const fallbackListTerminology = - (currentSideIndex <= 1 - ? 'flashesInThisAct' - : 'entriesInThisSection'); - - return { - sideNames, - sideColors, - sideActs, - - currentSideIndex, - currentSideActs, - currentActIndex, - currentActFlashes, - currentFlashIndex, + contentDependencies: [ + 'generateFlashActSidebarCurrentActBox', + 'generateFlashActSidebarSideMapBox', + 'generatePageSidebar', + ], - fallbackListTerminology, - }; - }, + relations: (relation, act, flash) => ({ + sidebar: + relation('generatePageSidebar'), - relations: (relation, query, sprawl, act, _flash) => ({ - currentActLink: - relation('linkFlashAct', act), + currentActBox: + relation('generateFlashActSidebarCurrentActBox', act, flash), - flashIndexLink: - relation('linkFlashIndex'), - - sideActLinks: - query.sideActs - .map(acts => acts - .map(act => relation('linkFlashAct', act))), - - currentActFlashLinks: - act.flashes - .map(flash => relation('linkFlash', flash)), + sideMapBox: + relation('generateFlashActSidebarSideMapBox', act, flash), }), - data: (query, sprawl, act, flash) => ({ + data: (_act, flash) => ({ isFlashActPage: !flash, - - sideColors: query.sideColors, - sideNames: query.sideNames, - - currentSideIndex: query.currentSideIndex, - currentActIndex: query.currentActIndex, - currentFlashIndex: query.currentFlashIndex, - - customListTerminology: act.listTerminology, - fallbackListTerminology: query.fallbackListTerminology, }), - generate(data, relations, {getColors, html, language}) { - const currentActBoxContent = html.tags([ - html.tag('h1', relations.currentActLink), - - html.tag('details', - (data.isFlashActPage - ? {} - : {class: 'current', open: true}), - [ - html.tag('summary', - html.tag('span', {class: 'group-name'}, - (data.customListTerminology - ? language.sanitize(data.customListTerminology) - : language.$('flashSidebar.flashList', data.fallbackListTerminology)))), - - html.tag('ul', - relations.currentActFlashLinks - .map((flashLink, index) => - html.tag('li', - index === data.currentFlashIndex && - {class: 'current'}, - - flashLink))), - ]), - ]); - - const sideMapBoxContent = html.tags([ - html.tag('h1', relations.flashIndexLink), - - stitchArrays({ - sideName: data.sideNames, - sideColor: data.sideColors, - actLinks: relations.sideActLinks, - }).map(({sideName, sideColor, actLinks}, sideIndex) => - html.tag('details', - sideIndex === data.currentSideIndex && - {class: 'current'}, - - data.isFlashActPage && - sideIndex === data.currentSideIndex && - {open: true}, - - sideColor && - {style: `--primary-color: ${getColors(sideColor).primary}`}, - - [ - html.tag('summary', - html.tag('span', {class: 'group-name'}, - sideName)), - - html.tag('ul', - actLinks.map((actLink, actIndex) => - html.tag('li', - sideIndex === data.currentSideIndex && - actIndex === data.currentActIndex && - {class: 'current'}, - - actLink))), - ])), - ]); - - const sideMapBox = { - class: 'flash-act-map-sidebar-box', - content: sideMapBoxContent, - }; - - const currentActBox = { - class: 'flash-current-act-sidebar-box', - content: currentActBoxContent, - }; - - return { - leftSidebarMultiple: + generate: (data, relations) => + relations.sidebar.slots({ + boxes: (data.isFlashActPage - ? [sideMapBox, currentActBox] - : [currentActBox, sideMapBox]), - }; - }, + ? [relations.sideMapBox, relations.currentActBox] + : [relations.currentActBox, relations.sideMapBox]), + }), }; diff --git a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js new file mode 100644 index 00000000..6d152c7c --- /dev/null +++ b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js @@ -0,0 +1,64 @@ +export default { + contentDependencies: [ + 'generatePageSidebarBox', + 'linkFlash', + 'linkFlashAct', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, act, _flash) => ({ + box: + relation('generatePageSidebarBox'), + + actLink: + relation('linkFlashAct', act), + + flashLinks: + act.flashes + .map(flash => relation('linkFlash', flash)), + }), + + data: (act, flash) => ({ + isFlashActPage: + !flash, + + currentFlashIndex: + act.flashes.indexOf(flash), + + customListTerminology: + act.listTerminology, + }), + + generate: (data, relations, {html, language}) => + relations.box.slots({ + attributes: {class: 'flash-act-map-sidebar-box'}, + + content: [ + html.tag('h1', relations.actLink), + + html.tag('details', + (data.isFlashActPage + ? {} + : {class: 'current', open: true}), + + [ + html.tag('summary', + html.tag('span', + html.tag('b', + (data.customListTerminology + ? language.sanitize(data.customListTerminology) + : language.$('flashSidebar.flashList.entriesInThisSection'))))), + + html.tag('ul', + relations.flashLinks + .map((flashLink, index) => + html.tag('li', + index === data.currentFlashIndex && + {class: 'current'}, + + flashLink))), + ]), + ], + }), +}; diff --git a/src/content/dependencies/generateFlashActSidebarSideMapBox.js b/src/content/dependencies/generateFlashActSidebarSideMapBox.js new file mode 100644 index 00000000..7b26ef31 --- /dev/null +++ b/src/content/dependencies/generateFlashActSidebarSideMapBox.js @@ -0,0 +1,85 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generateColorStyleAttribute', + 'generatePageSidebarBox', + 'linkFlashAct', + 'linkFlashIndex', + ], + + extraDependencies: ['html', 'wikiData'], + + sprawl: ({flashSideData}) => ({flashSideData}), + + relations: (relation, sprawl, _act, _flash) => ({ + box: + relation('generatePageSidebarBox'), + + flashIndexLink: + relation('linkFlashIndex'), + + sideColorStyles: + sprawl.flashSideData + .map(side => relation('generateColorStyleAttribute', side.color)), + + sideActLinks: + sprawl.flashSideData + .map(side => side.acts + .map(act => relation('linkFlashAct', act))), + }), + + data: (sprawl, act, flash) => ({ + isFlashActPage: + !flash, + + sideNames: + sprawl.flashSideData + .map(side => side.name), + + currentSideIndex: + sprawl.flashSideData.indexOf(act.side), + + currentActIndex: + act.side.acts.indexOf(act), + }), + + generate: (data, relations, {html}) => + relations.box.slots({ + attributes: {class: 'flash-act-map-sidebar-box'}, + + content: [ + html.tag('h1', relations.flashIndexLink), + + stitchArrays({ + sideName: data.sideNames, + sideColorStyle: relations.sideColorStyles, + actLinks: relations.sideActLinks, + }).map(({sideName, sideColorStyle, actLinks}, sideIndex) => + html.tag('details', + sideIndex === data.currentSideIndex && + {class: 'current'}, + + data.isFlashActPage && + sideIndex === data.currentSideIndex && + {open: true}, + + sideColorStyle.slot('context', 'primary-only'), + + [ + html.tag('summary', + html.tag('span', + html.tag('b', sideName))), + + html.tag('ul', + actLinks.map((actLink, actIndex) => + html.tag('li', + sideIndex === data.currentSideIndex && + actIndex === data.currentActIndex && + {class: 'current'}, + + actLink))), + ])), + ], + }), +}; 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 374fa3f8..00000000 --- a/src/content/dependencies/generateFlashCoverArtwork.js +++ /dev/null @@ -1,12 +0,0 @@ -export default { - contentDependencies: ['generateCoverArtwork'], - - relations: (relation) => - ({coverArtwork: relation('generateCoverArtwork')}), - - data: (flash) => - ({path: ['media.flashArt', flash.directory, flash.coverArtFileExtension]}), - - generate: (data, relations) => - relations.coverArtwork.slot('path', data.path), -}; diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js index 57072a1f..2788406c 100644 --- a/src/content/dependencies/generateFlashIndexPage.js +++ b/src/content/dependencies/generateFlashIndexPage.js @@ -1,4 +1,4 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { contentDependencies: [ @@ -20,7 +20,7 @@ export default { const jumpActs = flashActs - .filter(act => act.jump); + .filter(act => act.side.acts.indexOf(act) === 0); return {flashActs, jumpActs}; }, @@ -31,7 +31,7 @@ export default { jumpLinkColorStyles: query.jumpActs - .map(act => relation('generateColorStyleAttribute', act.jumpColor)), + .map(act => relation('generateColorStyleAttribute', act.side.color)), actColorStyles: query.flashActs @@ -53,7 +53,7 @@ export default { actCoverGridImages: query.flashActs .map(act => act.flashes - .map(() => relation('image'))), + .map(flash => relation('image', flash.coverArtwork))), }), data: (query) => ({ @@ -63,7 +63,7 @@ export default { jumpLinkLabels: query.jumpActs - .map(act => act.jump), + .map(act => act.side.name), actAnchors: query.flashActs @@ -73,82 +73,72 @@ 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}) => - relations.layout.slots({ - title: language.$('flashIndex.title'), - headingMode: 'static', - - mainClasses: ['flash-index'], - mainContent: [ - !empty(data.jumpLinkLabels) && [ - html.tag('p', {class: 'quick-info'}, - language.$('misc.jumpTo')), - - html.tag('ul', {class: 'quick-info'}, - stitchArrays({ - colorStyle: relations.jumpLinkColorStyles, - anchor: data.jumpLinkAnchors, - label: data.jumpLinkLabels, - }).map(({colorStyle, anchor, label}) => - html.tag('li', - html.tag('a', - {href: '#' + anchor}, - colorStyle, - label)))), - ], + language.encapsulate('flashIndex', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title'), + headingMode: 'static', + + mainClasses: ['flash-index'], + mainContent: [ + html.tags([ + html.tag('p', {class: 'quick-info'}, + {[html.onlyIfSiblings]: true}, + language.$('misc.jumpTo')), + + html.tag('ul', {class: 'quick-info'}, + {[html.onlyIfContent]: true}, + stitchArrays({ + colorStyle: relations.jumpLinkColorStyles, + anchor: data.jumpLinkAnchors, + label: data.jumpLinkLabels, + }).map(({colorStyle, anchor, label}) => + html.tag('li', + html.tag('a', + {href: '#' + anchor}, + colorStyle, + label)))), + ]), - stitchArrays({ - colorStyle: relations.actColorStyles, - actLink: relations.actLinks, - anchor: data.actAnchors, - - coverGrid: relations.actCoverGrids, - coverGridImages: relations.actCoverGridImages, - coverGridLinks: relations.actCoverGridLinks, - coverGridNames: data.actCoverGridNames, - coverGridPaths: data.actCoverGridPaths, - }).map(({ - colorStyle, - actLink, - anchor, - - coverGrid, - coverGridImages, - coverGridLinks, - coverGridNames, - coverGridPaths, - }, index) => [ - html.tag('h2', - {id: anchor}, + stitchArrays({ + colorStyle: relations.actColorStyles, + actLink: relations.actLinks, + anchor: data.actAnchors, + + coverGrid: relations.actCoverGrids, + coverGridImages: relations.actCoverGridImages, + coverGridLinks: relations.actCoverGridLinks, + coverGridNames: data.actCoverGridNames, + }).map(({ colorStyle, - actLink), - - coverGrid.slots({ - links: coverGridLinks, - names: coverGridNames, - lazy: index === 0 ? 4 : true, - - images: - stitchArrays({ - image: coverGridImages, - path: coverGridPaths, - }).map(({image, path}) => - image.slot('path', path)), - }), - ]), - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {auto: 'current'}, - ], - }), + actLink, + anchor, + + coverGrid, + coverGridImages, + coverGridLinks, + coverGridNames, + }, index) => [ + html.tag('h2', + {id: anchor}, + colorStyle, + actLink), + + coverGrid.slots({ + links: coverGridLinks, + images: coverGridImages, + names: coverGridNames, + lazy: index === 0 ? 4 : true, + }), + ]), + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {auto: 'current'}, + ], + })), }; diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js index c60f9696..ee043bfa 100644 --- a/src/content/dependencies/generateFlashInfoPage.js +++ b/src/content/dependencies/generateFlashInfoPage.js @@ -2,10 +2,13 @@ import {empty} from '#sugar'; export default { contentDependencies: [ + 'generateAdditionalNamesBox', + 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generateContributionList', 'generateFlashActSidebar', - 'generateFlashCoverArtwork', + 'generateFlashArtworkColumn', 'generateFlashNavAccent', 'generatePageLayout', 'generateTrackList', @@ -18,158 +21,186 @@ export default { query(flash) { const query = {}; - if (flash.page || !empty(flash.urls)) { - query.urls = []; + query.urls = []; - if (flash.page) { - query.urls.push(`https://homestuck.com/story/${flash.page}`); - } + if (flash.page) { + query.urls.push(`https://homestuck.com/story/${flash.page}`); + } - if (!empty(flash.urls)) { - query.urls.push(...flash.urls); - } + if (!empty(flash.urls)) { + query.urls.push(...flash.urls); } return query; }, - relations(relation, query, flash) { - const relations = {}; - const sections = relations.sections = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.sidebar = - relation('generateFlashActSidebar', flash.act, flash); + relations: (relation, query, flash) => ({ + layout: + relation('generatePageLayout'), - if (query.urls) { - relations.externalLinks = - query.urls.map(url => relation('linkExternal', url)); - } + sidebar: + relation('generateFlashActSidebar', flash.act, flash), - // TODO: Flashes always have cover art (#175) - /* eslint-disable-next-line no-constant-condition */ - if (true) { - relations.cover = - relation('generateFlashCoverArtwork', flash); - } + additionalNamesBox: + relation('generateAdditionalNamesBox', flash.additionalNames), - // Section: navigation bar + externalLinks: + query.urls + .map(url => relation('linkExternal', url)), - const nav = sections.nav = {}; + artworkColumn: + relation('generateFlashArtworkColumn', flash), - nav.flashActLink = - relation('linkFlashAct', flash.act); + contentHeading: + relation('generateContentHeading'), - nav.flashNavAccent = - relation('generateFlashNavAccent', flash); + contentContentHeading: + relation('generateContentContentHeading', flash), - // Section: Featured tracks + flashActLink: + relation('linkFlashAct', flash.act), - if (!empty(flash.featuredTracks)) { - const featuredTracks = sections.featuredTracks = {}; + flashNavAccent: + relation('generateFlashNavAccent', flash), - featuredTracks.heading = - relation('generateContentHeading'); + featuredTracksList: + relation('generateTrackList', flash.featuredTracks), - featuredTracks.list = - relation('generateTrackList', flash.featuredTracks); - } + contributorContributionList: + relation('generateContributionList', flash.contributorContribs), - // Section: Contributors + artistCommentaryEntries: + flash.commentary + .map(entry => relation('generateCommentaryEntry', entry)), - if (!empty(flash.contributorContribs)) { - const contributors = sections.contributors = {}; - - contributors.heading = - relation('generateContentHeading'); - - contributors.list = - relation('generateContributionList', flash.contributorContribs); - } - - return relations; - }, + creditSourceEntries: + flash.creditingSources + .map(entry => relation('generateCommentaryEntry', entry)), + }), - data(query, flash) { - const data = {}; + data: (_query, flash) => ({ + name: + flash.name, - data.name = flash.name; - data.color = flash.color; - data.date = flash.date; + color: + flash.color, - return data; - }, + date: + flash.date, + }), - generate(data, relations, {html, language}) { - const {sections: sec} = relations; + generate: (data, relations, {html, language}) => + language.encapsulate('flashPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + flash: data.name, + }), - return relations.layout.slots({ - title: - language.$('flashPage.title', { - flash: data.name, - }), + color: data.color, + headingMode: 'sticky', - color: data.color, - headingMode: 'sticky', + additionalNames: relations.additionalNamesBox, - cover: - (relations.cover - ? relations.cover.slots({ - alt: language.$('misc.alt.flashArt'), - }) - : null), + artworkColumnContent: relations.artworkColumn, - mainContent: [ - html.tag('p', - language.$('releaseInfo.released', { - date: language.formatDate(data.date), - })), + mainContent: [ + html.tag('p', + language.$('releaseInfo.released', { + date: language.formatDate(data.date), + })), - relations.externalLinks && html.tag('p', + {[html.onlyIfContent]: true}, + language.$('releaseInfo.playOn', { + [language.onlyIfOptions]: ['links'], + links: language.formatDisjunctionList( relations.externalLinks .map(link => link.slot('context', 'flash'))), })), - sec.featuredTracks && [ - sec.featuredTracks.heading - .slots({ - id: 'features', - title: - language.$('releaseInfo.tracksFeatured', { - flash: html.tag('i', data.name), - }), + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + language.encapsulate('releaseInfo', capsule => [ + !html.isBlank(relations.artistCommentaryEntries) && + language.encapsulate(capsule, 'readCommentary', capsule => + language.$(capsule, { + link: + html.tag('a', + {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([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'features'}, + title: + language.$('releaseInfo.tracksFeatured', { + flash: html.tag('i', data.name), + }), + }), + + relations.featuredTracksList, + ]), + + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'contributors'}, + title: language.$('releaseInfo.contributors'), + }), + + relations.contributorContributionList.slots({ + chronologyKind: 'flash', }), - - sec.featuredTracks.list, + ]), + + 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, + ]), ], - sec.contributors && [ - sec.contributors.heading - .slots({ - id: 'contributors', - title: language.$('releaseInfo.contributors'), - }), - - sec.contributors.list, + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {html: relations.flashActLink.slot('color', false)}, + {auto: 'current'}, ], - ], - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {html: sec.nav.flashActLink.slot('color', false)}, - {auto: 'current'}, - ], + navBottomRowContent: relations.flashNavAccent, - navBottomRowContent: sec.nav.flashNavAccent, - - ...relations.sidebar, - }); - }, + leftSidebar: relations.sidebar, + })), }; 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 b29c586f..dfdad0e8 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -1,15 +1,16 @@ 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', 'linkAlbum', 'linkListing', @@ -20,116 +21,93 @@ 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); - } + return query; + }, - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); + relations: (relation, query, sprawl, group) => ({ + layout: + relation('generatePageLayout'), - if (!empty(carouselAlbums)) { - relations.coverCarousel = - relation('generateCoverCarousel'); + navLinks: + relation('generateGroupNavLinks', group), - relations.carouselLinks = - carouselAlbums - .map(album => relation('linkAlbum', album)); + secondaryNav: + (sprawl.enableGroupUI + ? relation('generateGroupSecondaryNav', group) + : null), - relations.carouselImages = - carouselAlbums - .map(album => relation('image', album.artTags)); - } + coverCarousel: + relation('generateCoverCarousel'), - relations.coverGrid = - relation('generateCoverGrid'); + carouselLinks: + query.carouselAlbums + .map(album => relation('linkAlbum', album)), - relations.gridLinks = - albums - .map(album => relation('linkAlbum', album)); + carouselImages: + query.carouselAlbums + .map(album => relation('image', album.coverArtworks[0])), - relations.gridImages = - albums.map(album => - (album.hasCoverArt - ? relation('image', album.artTags) - : relation('image'))); + quickDescription: + relation('generateQuickDescription', group), - return relations; - }, + albumViewSwitcher: + relation('generateIntrapageDotSwitcher'), - data(sprawl, group) { - const data = {}; + albumsBySeriesView: + relation('generateGroupGalleryPageAlbumsBySeriesView', group), - data.name = group.name; - data.color = group.color; + albumsByDateView: + relation('generateGroupGalleryPageAlbumsByDateView', group), + }), - const albums = sortChronologically(group.albums.slice(), {latestFirst: true}); - const tracks = albums.flatMap((album) => album.tracks); + data: (query, _sprawl, group) => ({ + name: + group.name, - data.numAlbums = albums.length; - data.numTracks = tracks.length; - data.totalDuration = getTotalDuration(tracks, {originalReleasesOnly: true}); + color: + group.color, - data.gridNames = albums.map(album => album.name); - data.gridDurations = albums.map(album => getTotalDuration(album.tracks)); - data.gridNumTracks = albums.map(album => album.tracks.length); + numAlbums: + query.allAlbums.length, - data.gridPaths = - albums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); + numTracks: + query.allTracks.length, - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); + totalDuration: + getTotalDuration(query.allTracks, {mainReleasesOnly: true}), + }), - if (!empty(group.featuredAlbums)) { - data.carouselPaths = - carouselAlbums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); - } - - return data; - }, - - generate(data, relations, {html, language}) { - return relations.layout - .slots({ - title: language.$('groupGalleryPage.title', {group: data.name}), + generate: (data, relations, {html, language}) => + language.encapsulate('groupGalleryPage', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title', {group: data.name}), headingMode: 'static', color: data.color, 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, html.tag('p', {class: 'quick-info'}, - language.$('groupGalleryPage.infoLine', { + language.$(pageCapsule, 'infoLine', { tracks: html.tag('b', language.countTracks(data.numTracks, { @@ -149,48 +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'}, + ], + }), ], - ... - relations.sidebar - ?.slot('currentExtra', 'gallery') - ?.content, - 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 2e1d1688..7b9c2afa 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -1,218 +1,179 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { contentDependencies: [ - 'generateAbsoluteDatetimestamp', 'generateColorStyleAttribute', - 'generateContentHeading', + 'generateGroupInfoPageAlbumsSection', 'generateGroupNavLinks', 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', - 'linkAlbum', + 'linkArtist', 'linkExternal', - 'linkGroupGallery', - 'linkGroup', 'transformContent', ], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({wikiInfo}) { - return { - enableGroupUI: wikiInfo.enableGroupUI, - }; - }, - - query(sprawl, group) { - const albums = - group.albums; - - const albumGroups = - albums - .map(album => album.groups); - - const albumOtherCategory = - albumGroups - .map(groups => groups - .map(group => group.category) - .find(category => category !== group.category)); - - const albumOtherGroups = - stitchArrays({ - groups: albumGroups, - category: albumOtherCategory, - }).map(({groups, category}) => - groups - .filter(group => group.category === category)); - - return {albums, albumOtherGroups}; - }, - - relations(relation, query, sprawl, group) { - const relations = {}; - const sec = relations.sections = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.navLinks = - relation('generateGroupNavLinks', group); - - if (sprawl.enableGroupUI) { - relations.secondaryNav = - relation('generateGroupSecondaryNav', group); - - relations.sidebar = - relation('generateGroupSidebar', group); - } - - sec.info = {}; - - if (!empty(group.urls)) { - sec.info.visitLinks = - group.urls - .map(url => relation('linkExternal', url)); - } - - if (group.description) { - sec.info.description = - relation('transformContent', group.description); - } - - if (!empty(query.albums)) { - sec.albums = {}; - - sec.albums.heading = - relation('generateContentHeading'); - - sec.albums.galleryLink = - relation('linkGroupGallery', group); - - sec.albums.albumColorStyles = - query.albums - .map(album => relation('generateColorStyleAttribute', album.color)); - - sec.albums.albumLinks = - query.albums - .map(album => relation('linkAlbum', album)); - - sec.albums.otherGroupLinks = - query.albumOtherGroups - .map(groups => groups - .map(group => relation('linkGroup', group))); - - sec.albums.datetimestamps = - group.albums.map(album => - (album.date - ? relation('generateAbsoluteDatetimestamp', album.date) - : null)); - } - - return relations; - }, + 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, query, sprawl, group) => ({ + layout: + relation('generatePageLayout'), + + navLinks: + relation('generateGroupNavLinks', group), + + secondaryNav: + (sprawl.enableGroupUI + ? relation('generateGroupSecondaryNav', group) + : null), + + sidebar: + (sprawl.enableGroupUI + ? 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)), + + description: + relation('transformContent', group.description), + + albumSection: + relation('generateGroupInfoPageAlbumsSection', group), + }), + + data: (query, _sprawl, group) => ({ + name: + group.name, + + color: + group.color, + + closeArtistAnnotations: + query.generalLinkedArtists + .map(({annotation}) => annotation), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('groupInfoPage', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title', {group: data.name}), + headingMode: 'sticky', + color: data.color, - data(query, sprawl, group) { - const data = {}; + 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); + }))), + }); + }), - data.name = group.name; - data.color = group.color; + language.$(capsule, 'aliases', { + [language.onlyIfOptions]: ['aliases'], - return data; - }, + aliases: + language.formatConjunctionList( + relations.aliasArtistLinks.map(link => + link.slots({ + attributes: [relations.wikiColorAttribute], + }))), + }), + ])), - generate(data, relations, {html, language}) { - const {sections: sec} = relations; + html.tag('p', + {[html.onlyIfContent]: true}, - return relations.layout - .slots({ - title: language.$('groupInfoPage.title', {group: data.name}), - headingMode: 'sticky', - color: data.color, + language.$('releaseInfo.visitOn', { + [language.onlyIfOptions]: ['links'], - mainContent: [ - sec.info.visitLinks && - html.tag('p', - language.$('releaseInfo.visitOn', { - links: - language.formatDisjunctionList( - sec.info.visitLinks - .map(link => link.slot('context', 'group'))), - })), + links: + language.formatDisjunctionList( + relations.visitLinks + .map(link => link.slot('context', 'group'))), + })), html.tag('blockquote', {[html.onlyIfContent]: true}, - sec.info.description - ?.slot('mode', 'multiline')), - - sec.albums && [ - sec.albums.heading - .slots({ - tag: 'h2', - title: language.$('groupInfoPage.albumList.title'), - }), + relations.description.slot('mode', 'multiline')), - html.tag('p', - language.$('groupInfoPage.viewAlbumGallery', { - link: - sec.albums.galleryLink - .slot('content', language.$('groupInfoPage.viewAlbumGallery.link')), - })), - - html.tag('ul', - stitchArrays({ - albumLink: sec.albums.albumLinks, - otherGroupLinks: sec.albums.otherGroupLinks, - datetimestamp: sec.albums.datetimestamps, - albumColorStyle: sec.albums.albumColorStyles, - }).map(({ - albumLink, - otherGroupLinks, - datetimestamp, - albumColorStyle, - }) => { - const prefix = 'groupInfoPage.albumList.item'; - const parts = [prefix]; - const options = {}; - - options.album = - albumLink.slot('color', false); - - if (datetimestamp) { - parts.push('withYear'); - options.yearAccent = - language.$(prefix, 'yearAccent', { - year: - datetimestamp.slots({style: 'year', tooltip: true}), - }); - } - - if (!empty(otherGroupLinks)) { - parts.push('withOtherGroup'); - options.otherGroupAccent = - html.tag('span', {class: 'other-group-accent'}, - language.$(prefix, 'otherGroupAccent', { - groups: - language.formatConjunctionList( - otherGroupLinks.map(groupLink => - groupLink.slot('color', false))), - })); - } - - return ( - html.tag('li', - albumColorStyle, - language.$(...parts, options))); - })), - ], + relations.albumSection, ], - ...relations.sidebar?.content ?? {}, + leftSidebar: + (relations.sidebar + ? relations.sidebar + .content /* TODO: Kludge. */ + : null), navLinkStyle: 'hierarchical', navLinks: relations.navLinks.content, secondaryNav: relations.secondaryNav ?? null, - }); - }, + })), }; 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 new file mode 100644 index 00000000..0b678e9d --- /dev/null +++ b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js @@ -0,0 +1,93 @@ +export default { + contentDependencies: [ + 'generateContentHeading', + 'generateGroupInfoPageAlbumsListByDate', + 'generateGroupInfoPageAlbumsListBySeries', + 'generateIntrapageDotSwitcher', + 'linkGroupGallery', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, group) => ({ + contentHeading: + relation('generateContentHeading'), + + galleryLink: + relation('linkGroupGallery', group), + + albumsListByDate: + relation('generateGroupInfoPageAlbumsListByDate', group), + + albumsListBySeries: + relation('generateGroupInfoPageAlbumsListBySeries', group), + + viewSwitcher: + relation('generateIntrapageDotSwitcher'), + }), + + generate: (relations, {html, language}) => + language.encapsulate('groupInfoPage', pageCapsule => + language.encapsulate(pageCapsule, 'albumList', listCapsule => + html.tags([ + relations.contentHeading + .slots({ + tag: 'h2', + title: language.$(listCapsule, 'title'), + }), + + html.tag('p', + {[html.onlyIfSiblings]: true}, + + language.encapsulate(pageCapsule, 'viewAlbumGallery', viewAlbumGalleryCapsule => + language.encapsulate(viewAlbumGalleryCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.link = + relations.galleryLink + .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.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 17eb5083..c48f3142 100644 --- a/src/content/dependencies/generateGroupSecondaryNav.js +++ b/src/content/dependencies/generateGroupSecondaryNav.js @@ -1,100 +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 {content: previousNextPart} = - relations.previousNextLinks.slots({ - previousLink: relations.previousGroupLink, - nextLink: relations.nextGroupLink, - id: true, - }); - - const {categoryLink} = relations; - - categoryLink?.setSlot('content', data.categoryName); - - return relations.secondaryNav.slots({ - class: 'nav-links-groups', - content: - (relations.previousGroupLink || relations.nextGroupLink - ? 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/generateGroupSidebar.js b/src/content/dependencies/generateGroupSidebar.js index 98b288fa..0888cbbe 100644 --- a/src/content/dependencies/generateGroupSidebar.js +++ b/src/content/dependencies/generateGroupSidebar.js @@ -1,18 +1,25 @@ export default { - contentDependencies: ['generateGroupSidebarCategoryDetails'], + contentDependencies: [ + 'generateGroupSidebarCategoryDetails', + 'generatePageSidebar', + 'generatePageSidebarBox', + ], + extraDependencies: ['html', 'language', 'wikiData'], - sprawl({groupCategoryData}) { - return {groupCategoryData}; - }, + sprawl: ({groupCategoryData}) => ({groupCategoryData}), - relations(relation, sprawl, group) { - return { - categoryDetails: - sprawl.groupCategoryData.map(category => - relation('generateGroupSidebarCategoryDetails', category, group)), - }; - }, + relations: (relation, sprawl, group) => ({ + sidebar: + relation('generatePageSidebar'), + + sidebarBox: + relation('generatePageSidebarBox'), + + categoryDetails: + sprawl.groupCategoryData.map(category => + relation('generateGroupSidebarCategoryDetails', category, group)), + }), slots: { currentExtra: { @@ -20,17 +27,20 @@ export default { }, }, - generate(relations, slots, {html, language}) { - return { - leftSidebarClass: 'category-map-sidebar-box', - leftSidebarContent: [ - html.tag('h1', - language.$('groupSidebar.title')), + generate: (relations, slots, {html, language}) => + relations.sidebar.slots({ + boxes: [ + relations.sidebarBox.slots({ + attributes: {class: 'category-map-sidebar-box'}, + content: [ + html.tag('h1', + language.$('groupSidebar.title')), - relations.categoryDetails - .map(details => - details.slot('currentExtra', slots.currentExtra)), + relations.categoryDetails + .map(details => + details.slot('currentExtra', slots.currentExtra)), + ], + }), ], - }; - }, + }), }; diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js index 69de373b..208ccd07 100644 --- a/src/content/dependencies/generateGroupSidebarCategoryDetails.js +++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js @@ -46,37 +46,36 @@ export default { }, }, - generate(data, relations, slots, {html, language}) { - return html.tag('details', - data.isCurrentCategory && - {class: 'current', open: true}, - - [ - html.tag('summary', - relations.colorStyle, - - html.tag('span', - language.$('groupSidebar.groupList.category', { - category: - html.tag('span', {class: 'group-name'}, - data.name), - }))), - - html.tag('ul', - stitchArrays(({ - infoLink: relations.groupInfoLinks, - galleryLink: relations.groupGalleryLinks, - })).map(({infoLink, galleryLink}, index) => - html.tag('li', - index === data.currentGroupIndex && - {class: 'current'}, - - language.$('groupSidebar.groupList.item', { - group: - (slots.currentExtra === 'gallery' - ? galleryLink ?? infoLink - : infoLink), - })))), - ]); - }, + generate: (data, relations, slots, {html, language}) => + language.encapsulate('groupSidebar.groupList', capsule => + html.tag('details', + data.isCurrentCategory && + {class: 'current', open: true}, + + [ + html.tag('summary', + relations.colorStyle, + + html.tag('span', + language.$(capsule, 'category', { + category: + html.tag('b', data.name), + }))), + + html.tag('ul', + stitchArrays(({ + infoLink: relations.groupInfoLinks, + galleryLink: relations.groupGalleryLinks, + })).map(({infoLink, galleryLink}, index) => + html.tag('li', + index === data.currentGroupIndex && + {class: 'current'}, + + language.$(capsule, 'item', { + group: + (slots.currentExtra === 'gallery' + ? galleryLink ?? infoLink + : infoLink), + })))), + ])), }; diff --git a/src/content/dependencies/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 b046ccaf..df652efd 100644 --- a/src/content/dependencies/generateListAllAdditionalFilesChunk.js +++ b/src/content/dependencies/generateListAllAdditionalFilesChunk.js @@ -1,84 +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), - }, + stringsKey: {type: 'string'}, + }, - additionalFileLinks: { - validate: v => v.strictArrayOf(v.strictArrayOf(v.isHTML)), - }, + generate: (data, relations, slots, {html, language}) => + language.encapsulate('listingPage', slots.stringsKey, pageCapsule => + html.tags([ + html.tag('dt', + {[html.onlyIfSiblings]: true}, + slots.title), - additionalFileFiles: { - validate: v => v.strictArrayOf(v.strictArrayOf(v.isString)), - }, + html.tag('dd', + {[html.onlyIfContent]: true}, - stringsKey: {type: 'string'}, - }, + html.tag('ul', + {[html.onlyIfContent]: true}, - generate(slots, {html, language}) { - if (empty(slots.additionalFileLinks)) { - return html.blank(); - } + 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, + }), + })) - 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, - }) => - (additionalFileLinks.length === 1 - ? html.tag('li', - additionalFileLinks[0].slots({ - content: - language.$('listingPage', slots.stringsKey, 'file', { - title: additionalFileTitle, - }), - })) + : links.length === 0 + ? html.tag('li', + language.$(capsule, 'withNoFiles', { + title: title, + })) - : html.tag('li', {class: 'has-details'}, - html.tag('details', [ - html.tag('summary', - html.tag('span', - language.$('listingPage', slots.stringsKey, 'file.withMultipleFiles', { - title: - html.tag('span', {class: 'group-name'}, - additionalFileTitle), + : html.tag('li', {class: 'has-details'}, + html.tag('details', [ + html.tag('summary', + html.tag('span', + language.$(capsule, 'withMultipleFiles', { + title: + html.tag('b', title), - files: - language.countAdditionalFiles( - additionalFileLinks.length, - {unit: true}), - }))), + files: + language.countAdditionalFiles( + links.length, + {unit: true}), + }))), - html.tag('ul', - stitchArrays({ - additionalFileLink: additionalFileLinks, - additionalFileFile: additionalFileFiles, - }).map(({additionalFileLink, additionalFileFile}) => - html.tag('li', - additionalFileLink.slots({ - content: - language.$('listingPage', slots.stringsKey, 'file', { - title: additionalFileFile, - }), - })))), - ])))))), - ]); - }, + html.tag('ul', + stitchArrays({ + 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/generateListingPage.js b/src/content/dependencies/generateListingPage.js index aa661abd..5f9a99a9 100644 --- a/src/content/dependencies/generateListingPage.js +++ b/src/content/dependencies/generateListingPage.js @@ -34,13 +34,15 @@ export default { relations.sameTargetListingLinks = listing.target.listings .map(listing => relation('linkListing', listing)); + } else { + relations.sameTargetListingLinks = []; } - if (!empty(listing.seeAlso)) { - relations.seeAlsoLinks = - listing.seeAlso - .map(listing => relation('linkListing', listing)); - } + relations.seeAlsoLinks = + (!empty(listing.seeAlso) + ? listing.seeAlso + .map(listing => relation('linkListing', listing)) + : []); return relations; }, @@ -167,33 +169,37 @@ export default { headingMode: 'sticky', mainContent: [ - relations.sameTargetListingLinks && - html.tag('p', - language.$('listingPage.listingsFor', { - target: - language.$('listingPage.target', data.targetStringsKey), - - listings: - language.formatUnitList( - stitchArrays({ - link: relations.sameTargetListingLinks, - stringsKey: data.sameTargetListingStringsKeys, - }).map(({link, stringsKey}, index) => - html.tag('span', - index === data.sameTargetListingsCurrentIndex && - {class: 'current'}, - - link.slots({ - attributes: {class: 'nowrap'}, - content: language.$('listingPage', stringsKey, 'title.short'), - })))), - })), - - relations.seeAlsoLinks && - html.tag('p', - language.$('listingPage.seeAlso', { - listings: language.formatUnitList(relations.seeAlsoLinks), - })), + html.tag('p', + {[html.onlyIfContent]: true}, + language.$('listingPage.listingsFor', { + [language.onlyIfOptions]: ['listings'], + + target: + language.$('listingPage.target', data.targetStringsKey), + + listings: + language.formatUnitList( + stitchArrays({ + link: relations.sameTargetListingLinks, + stringsKey: data.sameTargetListingStringsKeys, + }).map(({link, stringsKey}, index) => + html.tag('span', + index === data.sameTargetListingsCurrentIndex && + {class: 'current'}, + + link.slots({ + attributes: {class: 'nowrap'}, + content: language.$('listingPage', stringsKey, 'title.short'), + })))), + })), + + html.tag('p', + {[html.onlyIfContent]: true}, + language.$('listingPage.seeAlso', { + [language.onlyIfOptions]: ['listings'], + listings: + language.formatUnitList(relations.seeAlsoLinks), + })), slots.content, @@ -243,7 +249,7 @@ export default { .clone() .slots({ tag: 'dt', - id, + attributes: [id && {id}], title: formatListingString({ @@ -276,7 +282,7 @@ export default { {auto: 'current'}, ], - ...relations.sidebar, + leftSidebar: relations.sidebar, }); }, }; diff --git a/src/content/dependencies/generateListingSidebar.js b/src/content/dependencies/generateListingSidebar.js index 1cdd236b..aeac05cf 100644 --- a/src/content/dependencies/generateListingSidebar.js +++ b/src/content/dependencies/generateListingSidebar.js @@ -1,21 +1,37 @@ export default { - contentDependencies: ['generateListingIndexList', 'linkListingIndex'], + contentDependencies: [ + 'generateListingIndexList', + 'generatePageSidebar', + 'generatePageSidebarBox', + 'linkListingIndex', + ], + extraDependencies: ['html'], - relations(relation, currentListing) { - return { - listingIndexLink: relation('linkListingIndex'), - listingIndexList: relation('generateListingIndexList', currentListing), - }; - }, + relations: (relation, currentListing) => ({ + sidebar: + relation('generatePageSidebar'), + + sidebarBox: + relation('generatePageSidebarBox'), + + listingIndexLink: + relation('linkListingIndex'), + + listingIndexList: + relation('generateListingIndexList', currentListing), + }), - generate(relations, {html}) { - return { - leftSidebarClass: 'listing-map-sidebar-box', - leftSidebarContent: [ - html.tag('h1', relations.listingIndexLink), - relations.listingIndexList.slot('mode', 'sidebar'), + generate: (relations, {html}) => + relations.sidebar.slots({ + boxes: [ + relations.sidebarBox.slots({ + attributes: {class: 'listing-map-sidebar-box'}, + content: [ + html.tag('h1', relations.listingIndexLink), + relations.listingIndexList.slot('mode', 'sidebar'), + ], + }), ], - }; - }, + }), }; diff --git a/src/content/dependencies/generateListingsIndexPage.js b/src/content/dependencies/generateListingsIndexPage.js index 1b1c8559..b57ebe15 100644 --- a/src/content/dependencies/generateListingsIndexPage.js +++ b/src/content/dependencies/generateListingsIndexPage.js @@ -83,7 +83,7 @@ export default { {auto: 'current'}, ], - ...relations.sidebar, + leftSidebar: relations.sidebar, }); }, }; 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 bcba7194..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,101 +30,76 @@ export default { return {previousEntry, nextEntry}; }, - relations(relation, query, sprawl, newsEntry) { - const relations = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.content = - relation('transformContent', newsEntry.content); - - relations.newsIndexLink = - relation('linkNewsIndex'); - - relations.currentEntryLink = - relation('linkNewsEntry', newsEntry); - - if (query.previousEntry || query.nextEntry) { - relations.previousNextLinks = - relation('generatePreviousNextLinks'); - - relations.readAnotherLinks = - relation('generateNewsEntryReadAnotherLinks', - newsEntry, - query.previousEntry, - query.nextEntry); - - if (query.previousEntry) { - relations.previousEntryNavLink = - relation('linkNewsEntry', query.previousEntry); - } - - if (query.nextEntry) { - relations.nextEntryNavLink = - relation('linkNewsEntry', query.nextEntry); - } - } - - 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, - }; - }, - - generate(data, relations, {html, language}) { - return relations.layout.slots({ - title: - language.$('newsEntryPage.title', { - entry: data.name, - }), - - headingMode: 'sticky', - - mainClasses: ['long-content'], - mainContent: [ - html.tag('p', - language.$('newsEntryPage.published', { - date: language.formatDate(data.date), - })), - - relations.content, - relations.readAnotherLinks, - ], - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {html: relations.newsIndexLink}, - { - auto: 'current', - accent: - (relations.previousNextLinks - ? `(${language.formatUnitList(relations.previousNextLinks.slots({ - previousLink: relations.previousEntryNavLink ?? null, - nextLink: relations.nextEntryNavLink ?? null, - }).content)})` - : null), - }, - ], - }); - }, + relations: (relation, query, sprawl, newsEntry) => ({ + layout: + relation('generatePageLayout'), + + content: + relation('transformContent', newsEntry.content), + + newsIndexLink: + relation('linkNewsIndex'), + + readAnotherLinks: + relation('generateNewsEntryReadAnotherLinks', + newsEntry, + query.previousEntry, + query.nextEntry), + + navAccent: + relation('generateNewsEntryNavAccent', + query.previousEntry, + query.nextEntry), + }), + + data: (query, sprawl, newsEntry) => ({ + 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, + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('newsEntryPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + entry: data.name, + }), + + headingMode: 'sticky', + + mainClasses: ['long-content'], + mainContent: [ + html.tag('p', + language.$(pageCapsule, 'published', { + date: language.formatDate(data.date), + })), + + relations.content, + relations.readAnotherLinks, + ], + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {html: relations.newsIndexLink}, + { + auto: 'current', + accent: relations.navAccent, + }, + ], + })), }; diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js index 539af804..02964ce8 100644 --- a/src/content/dependencies/generateNewsIndexPage.js +++ b/src/content/dependencies/generateNewsIndexPage.js @@ -57,37 +57,38 @@ export default { }; }, - generate(data, relations, {html, language}) { - return relations.layout.slots({ - title: language.$('newsIndex.title'), - headingMode: 'sticky', - - mainClasses: ['long-content', 'news-index'], - mainContent: - stitchArrays({ - entryLink: relations.entryLinks, - viewRestLink: relations.viewRestLinks, - content: relations.entryContents, - date: data.entryDates, - directory: data.entryDirectories, - }).map(({entryLink, viewRestLink, content, date, directory}) => - html.tag('article', {id: directory}, [ - html.tag('h2', [ - html.tag('time', language.formatDate(date)), - entryLink, - ]), - - content, - - viewRestLink - ?.slot('content', language.$('newsIndex.entry.viewRest')), - ])), - - navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {auto: 'current'}, - ], - }); - }, + generate: (data, relations, {html, language}) => + language.encapsulate('newsIndex', pageCapsule => + relations.layout.slots({ + title: language.$(pageCapsule, 'title'), + headingMode: 'sticky', + + mainClasses: ['long-content', 'news-index'], + mainContent: + stitchArrays({ + entryLink: relations.entryLinks, + viewRestLink: relations.viewRestLinks, + content: relations.entryContents, + date: data.entryDates, + directory: data.entryDirectories, + }).map(({entryLink, viewRestLink, content, date, directory}) => + language.encapsulate(pageCapsule, 'entry', entryCapsule => + html.tag('article', {id: directory}, [ + html.tag('h2', [ + html.tag('time', language.formatDate(date)), + entryLink, + ]), + + content, + + viewRestLink + ?.slot('content', language.$(entryCapsule, 'viewRest')), + ]))), + + navLinkStyle: 'hierarchical', + navLinks: [ + {auto: 'home'}, + {auto: 'current'}, + ], + })), }; diff --git a/src/content/dependencies/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 9e9b4615..0326f415 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -1,92 +1,42 @@ import {openAggregate} from '#aggregate'; -import {empty} from '#sugar'; - -function sidebarSlots(side) { - return { - // Content is a flat HTML array. It'll generate one sidebar section - // if specified. - [side + 'Content']: { - type: 'html', - mutable: false, - }, - - // A single class to apply to the whole sidebar. If specifying multiple - // sections, this be added to the containing sidebar-column - specify a - // class on each section if that's more suitable. - [side + 'Class']: {type: 'string'}, - - // Multiple is an array of objects, each specifying content (HTML) and - // optionally class (a string). Each of these will generate one sidebar - // section. - [side + 'Multiple']: { - validate: v => - v.sparseArrayOf( - v.validateProperties({ - class: v.optional(v.isString), - content: v.isHTML, - })), - }, - - // Sticky mode controls which sidebar section(s), if any, follow the - // scroll position, "sticking" to the top of the browser viewport. - // - // 'last' - last or only sidebar box is sticky - // 'column' - entire column, incl. multiple boxes from top, is sticky - // 'none' - sidebar not sticky at all, stays at top of page - // - // Note: This doesn't affect the content of any sidebar section, only - // the whole section's containing box (or the sidebar column as a whole). - [side + 'StickyMode']: { - validate: v => v.is('last', 'column', 'static'), - default: 'static', - }, - - // Collapsing sidebars disappear when the viewport is sufficiently - // thin. (This is the default.) Override as false to make the sidebar - // stay visible in thinner viewports, where the page layout will be - // reflowed so the sidebar is as wide as the screen and appears below - // nav, above the main content. - [side + 'Collapse']: {type: 'boolean', default: true}, - - // Wide sidebars generally take up more horizontal space in the normal - // page layout, and should be used if the content of the sidebar has - // a greater than typical focus compared to main content. - [side + 'Wide']: {type: 'boolean', defualt: false}, - }; -} +import {atOffset, empty, repeat} from '#sugar'; export default { contentDependencies: [ - 'generateColorStyleRules', + 'generateColorStyleTag', 'generateFooterLocalizationLinks', + 'generateImageOverlay', + 'generatePageSidebar', + 'generateSearchSidebarBox', + 'generateStaticURLStyleTag', 'generateStickyHeadingContainer', + 'generateWikiWallpaperStyleTag', 'transformContent', ], extraDependencies: [ - 'cachebust', 'getColors', 'html', 'language', 'pagePath', + 'pagePathStringFromRoot', 'to', 'wikiData', ], - sprawl({wikiInfo}) { - return { - 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 = {}; @@ -97,13 +47,30 @@ export default { relations.stickyHeadingContainer = relation('generateStickyHeadingContainer'); + relations.sidebar = + relation('generatePageSidebar'); + + if (sprawl.enableSearch) { + relations.searchBox = + relation('generateSearchSidebarBox'); + } + if (sprawl.footerContent) { relations.defaultFooterContent = relation('transformContent', sprawl.footerContent); } - relations.colorStyleRules = - relation('generateColorStyleRules'); + relations.colorStyleTag = + relation('generateColorStyleTag'); + + relations.staticURLStyleTag = + relation('generateStaticURLStyleTag'); + + relations.wikiWallpaperStyleTag = + relation('generateWikiWallpaperStyleTag'); + + relations.imageOverlay = + relation('generateImageOverlay'); return relations; }, @@ -115,6 +82,16 @@ export default { }, showWikiNameInTitle: { + validate: v => v.is(true, false, 'auto'), + default: 'auto', + }, + + subtitle: { + type: 'html', + mutable: false, + }, + + showSearch: { type: 'boolean', default: true, }, @@ -124,7 +101,7 @@ export default { mutable: false, }, - cover: { + artworkColumnContent: { type: 'html', mutable: false, }, @@ -138,9 +115,9 @@ export default { color: {validate: v => v.isColor}, - styleRules: { - validate: v => v.sparseArrayOf(v.isHTML), - default: [], + styleTags: { + type: 'html', + mutable: false, }, mainClasses: { @@ -162,8 +139,15 @@ export default { // Sidebars - ...sidebarSlots('leftSidebar'), - ...sidebarSlots('rightSidebar'), + leftSidebar: { + type: 'html', + mutable: true, + }, + + rightSidebar: { + type: 'html', + mutable: true, + }, // Banner @@ -256,16 +240,59 @@ export default { }, generate(data, relations, slots, { - cachebust, getColors, html, language, pagePath, + pagePathStringFromRoot, to, }) { const colors = getColors(slots.color ?? data.wikiColor); const hasSocialEmbed = !html.isBlank(slots.socialEmbed); + // Hilariously jank. Sorry! We're going to need this content later ANYWAY, + // so it's "fine" to stringify it here, but this DOES mean that we're + // stringifying (and resolving) the content without the context that it's + // e.g. going to end up in a page HTML hierarchy. Might have implications + // later, mainly for: https://github.com/hsmusic/hsmusic-wiki/issues/434 + 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 @@ -280,35 +307,59 @@ 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) { - footerContent = relations.defaultFooterContent - .slot('mode', 'multiline'); + footerContent = + relations.defaultFooterContent.slots({ + mode: 'multiline', + indicateExternalLinks: false, + }); } const mainHTML = 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, html.tag('div', {class: 'main-content-container'}, {[html.onlyIfContent]: true}, - slots.mainContent), + mainContentHTML), ]); const footerHTML = @@ -343,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 = @@ -379,98 +428,106 @@ export default { (slots.navLinkStyle === 'hierarchical' && i === slots.navLinks.length - 1); - return ( + const navLink = html.tag('span', {class: 'nav-link'}, showAsCurrent && {class: 'current'}, - i > 0 && - {class: 'has-divider'}, - [ 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}, slots.navContent), ]); - const generateSidebarHTML = (side, id) => { - const content = slots[side + 'Content']; - const topClass = slots[side + 'Class']; - const multiple = slots[side + 'Multiple']; - const stickyMode = slots[side + 'StickyMode']; - const wide = slots[side + 'Wide']; - const collapse = slots[side + 'Collapse']; - - let sidebarClasses = []; - let sidebarContent = html.blank(); - - if (!html.isBlank(content)) { - sidebarClasses = ['sidebar', topClass]; - sidebarContent = content; - } else if (multiple) { - sidebarClasses = ['sidebar-multiple', topClass]; - sidebarContent = - multiple - .filter(Boolean) - .map(box => - html.tag('div', {class: 'sidebar'}, - {[html.onlyIfContent]: true}, - {class: box.class}, - box.content)); - } + const getSidebar = (side, id, needed) => { + const sidebar = + (html.isBlank(slots[side]) + ? (needed + ? relations.sidebar.clone() + : html.blank()) + : slots[side]); - if (html.isBlank(sidebarContent)) { - return html.blank(); + if (html.isBlank(sidebar) && !needed) { + return sidebar; } - return html.tag('div', {class: 'sidebar-column'}, - {id, class: sidebarClasses}, - - wide && - {class: 'wide'}, + return sidebar.slots({ + attributes: + sidebar + .getSlotValue('attributes') + .with({id}), + }); + } - !collapse && - {class: 'no-hide'}, + const willShowSearch = + slots.showSearch && relations.searchBox; - stickyMode !== 'static' && - {class: `sticky-${stickyMode}`}, + let showingSidebarLeft; + let showingSidebarRight; + let sidebarsInContentColumn = false; - sidebarContent); - } + const leftSidebar = getSidebar('leftSidebar', 'sidebar-left', willShowSearch); + const rightSidebar = getSidebar('rightSidebar', 'sidebar-right', false); - const sidebarLeftHTML = generateSidebarHTML('leftSidebar', 'sidebar-left'); - const sidebarRightHTML = generateSidebarHTML('rightSidebar', 'sidebar-right'); + if (willShowSearch) { + if (html.isBlank(leftSidebar)) { + sidebarsInContentColumn = true; + showingSidebarLeft = true; + } - const hasSidebarLeft = !html.isBlank(sidebarLeftHTML); - const hasSidebarRight = !html.isBlank(sidebarRightHTML); + leftSidebar.setSlot( + 'boxes', + html.tags([ + relations.searchBox, + leftSidebar.getSlotValue('boxes'), + ])); + } - const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse; + const hasSidebarLeft = !html.isBlank(html.resolve(leftSidebar)); + const hasSidebarRight = !html.isBlank(html.resolve(rightSidebar)); - const hasID = (() => { - // Hilariously jank. Sorry! - const mainContentHTML = slots.mainContent.toString(); - return id => mainContentHTML.includes(`id="${id}"`); - })(); + showingSidebarLeft ??= hasSidebarLeft; + showingSidebarRight ??= hasSidebarRight; const processSkippers = skipperList => skipperList @@ -478,8 +535,11 @@ export default { (condition === undefined ? hasID(id) : condition)) + .map(({id, string}) => html.tag('span', {class: 'skipper'}, + {'data-for': id}, + html.tag('a', {href: `#${id}`}, language.$('misc.skippers', string)))); @@ -529,51 +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'}, [ - html.tag('div', {id: 'image-overlay-action-content-without-size'}, - language.$('releaseInfo.viewOriginalFile', { - link: html.tag('a', {class: 'image-overlay-view-original'}, - language.$('releaseInfo.viewOriginalFile.link')), - })), + const slottedStyleTags = + html.smush(slots.styleTags); - html.tag('div', {id: 'image-overlay-action-content-with-size'}, [ - language.$('releaseInfo.viewOriginalFile.withSize', { - link: - html.tag('a', {class: 'image-overlay-view-original'}, - language.$('releaseInfo.viewOriginalFile.link')), - - size: - html.tag('span', - {[html.joinChildren]: ''}, - [ - html.tag('span', {id: 'image-overlay-file-size-kilobytes'}, - language.$('count.fileSize.kilobytes', { - kilobytes: - html.tag('span', {class: 'image-overlay-file-size-count'}), - })), - - html.tag('span', {id: 'image-overlay-file-size-megabytes'}, - language.$('count.fileSize.megabytes', { - megabytes: - html.tag('span', {class: 'image-overlay-file-size-count'}), - })), - ]), - }), + const slottedWallpaperStyleTag = + slottedStyleTags.content + .find(tag => tag.attributes.has('class', 'wallpaper-style')); - html.tag('span', {id: 'image-overlay-file-size-warning'}, - language.$('releaseInfo.viewOriginalFile.sizeWarning')), - ]), - ]), - ])); + const fallbackWallpaperStyleTag = + (slottedWallpaperStyleTag + ? html.blank() + : relations.wikiWallpaperStyleTag); + + const usingWallpaperStyleTag = + (slottedWallpaperStyleTag + ? slottedWallpaperStyleTag + : html.resolve(fallbackWallpaperStyleTag, {normalize: 'tag'})); + + 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, @@ -583,15 +632,11 @@ export default { slots.secondaryNav, - html.tag('div', {class: 'layout-columns'}, - !collapseSidebars && - {class: 'vertical-when-thin'}, - - [ - sidebarLeftHTML, - mainHTML, - sidebarRightHTML, - ]), + html.tag('div', {class: 'layout-columns'}, [ + leftSidebar, + mainHTML, + rightSidebar, + ]), slots.bannerPosition === 'bottom' && slots.banner, @@ -614,6 +659,8 @@ export default { {'data-rebase-localized': to('localized.root')}, {'data-rebase-shared': to('shared.root')}, {'data-rebase-media': to('media.root')}, + {'data-rebase-thumb': to('thumb.root')}, + {'data-rebase-lib': to('staticLib.root')}, {'data-rebase-data': to('data.root')}, [ @@ -621,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', { @@ -660,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', { @@ -674,7 +739,6 @@ export default { hreflang: lang, href, }))), - */ hasSocialEmbed && @@ -682,37 +746,55 @@ export default { .clone() .slot('mode', 'html'), + oEmbedJSONHref && + html.tag('link', { + type: 'application/json+oembed', + href: oEmbedJSONHref, + }), + html.tag('link', { rel: 'stylesheet', - href: to('shared.staticFile', 'site6.css', cachebust), + 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'), + }), html.tag('script', { - src: to('shared.staticFile', 'lazy-loading.js', cachebust), + blocking: 'render', + src: to('staticJS.path', 'lazy-loading.js'), + }), + + html.tag('script', { + blocking: 'render', + type: 'module', + src: to('staticJS.path', 'client/index.js'), }), ]), html.tag('body', [ - html.tag('div', {id: 'page-container'}, - (hasSidebarLeft || hasSidebarRight - ? {class: 'has-one-sidebar'} - : {class: 'has-zero-sidebars'}), + wallpaperPartsHTML, - hasSidebarLeft && hasSidebarRight && - {class: 'has-two-sidebars'}, + html.tag('div', {id: 'page-container'}, + showingSidebarLeft && + {class: 'showing-sidebar-left'}, - hasSidebarLeft && - {class: 'has-sidebar-left'}, + showingSidebarRight && + {class: 'showing-sidebar-right'}, - hasSidebarRight && - {class: 'has-sidebar-right'}, + sidebarsInContentColumn && + {class: 'sidebars-in-content-column'}, [ skippersHTML, @@ -720,12 +802,7 @@ export default { ]), // infoCardHTML, - imageOverlayHTML, - - html.tag('script', { - type: 'module', - src: to('shared.staticFile', 'client3.js', cachebust), - }), + relations.imageOverlay, ]), ]) ]).toString(); diff --git a/src/content/dependencies/generatePageSidebar.js b/src/content/dependencies/generatePageSidebar.js new file mode 100644 index 00000000..d3b55580 --- /dev/null +++ b/src/content/dependencies/generatePageSidebar.js @@ -0,0 +1,90 @@ +export default { + extraDependencies: ['html'], + + slots: { + // Attributes to apply to the whole sidebar. This be added to the + // containing sidebar-column, arr - specify attributes on each section if + // that's more suitable. + attributes: { + type: 'attributes', + mutable: false, + }, + + // Content boxes to line up vertically in the sidebar. + boxes: { + type: 'html', + mutable: false, + }, + + // Sticky mode controls which sidebar sections, if any, follow the + // scroll position, "sticking" to the top of the browser viewport. + // + // 'column' - entire column, incl. multiple boxes from top, is sticky + // 'static' - sidebar not sticky at all, stays at top of page + // + // Note: This doesn't affect the content of any sidebar section, only + // the whole section's containing box (or the sidebar column as a whole). + stickyMode: { + validate: v => v.is('column', 'static'), + default: 'static', + }, + + // Wide sidebars generally take up more horizontal space in the normal + // page layout, and should be used if the content of the sidebar has + // a greater than typical focus compared to main content. + wide: { + type: 'boolean', + default: false, + }, + + // Provide to include all the HTML for the sidebar in place as usual, + // but start it out totally invisible. This is mainly so client-side + // JavaScript can show the sidebar if it needs to (and has a target + // to slot its own content into). If there are no boxes and this + // option *isn't* provided, then the sidebar will just be blank. + initiallyHidden: { + type: 'boolean', + default: false, + }, + }, + + generate(slots, {html}) { + const attributes = + html.attributes({class: [ + 'sidebar-column', + 'sidebar-multiple', + ]}); + + attributes.add(slots.attributes); + + if (slots.wide) { + attributes.add('class', 'wide'); + } + + if (slots.stickyMode !== 'static') { + attributes.add('class', `sticky-${slots.stickyMode}`); + } + + const {content: boxes} = html.smooth(slots.boxes); + + const allBoxesCollapsible = + boxes.every(box => + html.resolve(box) + .attributes + .has('class', 'collapsible')); + + if (allBoxesCollapsible) { + attributes.add('class', 'all-boxes-collapsible'); + } + + if (slots.initiallyHidden) { + attributes.add('class', 'initially-hidden'); + } + + if (html.isBlank(slots.boxes) && !slots.initiallyHidden) { + return html.blank(); + } else { + return html.tag('div', attributes, slots.boxes); + } + }, +}; diff --git a/src/content/dependencies/generatePageSidebarBox.js b/src/content/dependencies/generatePageSidebarBox.js new file mode 100644 index 00000000..26b30494 --- /dev/null +++ b/src/content/dependencies/generatePageSidebarBox.js @@ -0,0 +1,30 @@ +export default { + extraDependencies: ['html'], + + slots: { + content: { + type: 'html', + mutable: false, + }, + + attributes: { + type: 'attributes', + mutable: false, + }, + + collapsible: { + type: 'boolean', + default: true, + }, + }, + + generate: (slots, {html}) => + html.tag('div', {class: 'sidebar'}, + {[html.onlyIfContent]: true}, + + slots.collapsible && + {class: 'collapsible'}, + + slots.attributes, + slots.content), +}; diff --git a/src/content/dependencies/generatePageSidebarConjoinedBox.js b/src/content/dependencies/generatePageSidebarConjoinedBox.js new file mode 100644 index 00000000..7974c707 --- /dev/null +++ b/src/content/dependencies/generatePageSidebarConjoinedBox.js @@ -0,0 +1,38 @@ +// This component is kind of unfortunately magical. It reads the content of +// various boxes and joins them together, discarding the boxes' attributes. +// Since it requires access to the actual box *templates* (rather than those +// templates' resolved content), take care when slotting into this. + +export default { + contentDependencies: ['generatePageSidebarBox'], + extraDependencies: ['html'], + + relations: (relation) => ({ + box: + relation('generatePageSidebarBox'), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + boxes: { + validate: v => v.looseArrayOf(v.isTemplate), + }, + }, + + generate: (relations, slots, {html}) => + relations.box.slots({ + attributes: slots.attributes, + content: + slots.boxes.slice() + .map(box => box.getSlotValue('content')) + .map((content, index, {length}) => [ + content, + index < length - 1 && + 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 new file mode 100644 index 00000000..e144503e --- /dev/null +++ b/src/content/dependencies/generateQuickDescription.js @@ -0,0 +1,134 @@ +export default { + contentDependencies: ['transformContent'], + extraDependencies: ['html', 'language'], + + query: (thing) => ({ + hasDescription: + !!thing.description, + + hasLongerDescription: + thing.description && + thing.descriptionShort && + thing.descriptionShort !== thing.description, + }), + + relations: (relation, query, thing) => ({ + description: + (query.hasLongerDescription || !thing.description + ? null + : relation('transformContent', thing.description)), + + descriptionShort: + (query.hasLongerDescription + ? relation('transformContent', thing.descriptionShort) + : null), + + descriptionLong: + (query.hasLongerDescription + ? relation('transformContent', thing.description) + : null), + }), + + data: (query) => ({ + hasDescription: query.hasDescription, + hasLongerDescription: query.hasLongerDescription, + }), + + slots: { + extraReadingLinks: { + validate: v => v.sparseArrayOf(v.isHTML), + }, + }, + + generate(data, relations, slots, {html, language}) { + const prefix = 'misc.quickDescription'; + + const actionsWithoutLongerDescription = + (data.hasLongerDescription + ? null + : slots.extraReadingLinks + ? language.$(prefix, 'readMore', { + links: + language.formatDisjunctionList(slots.extraReadingLinks), + }) + : null); + + const wrapExpandCollapseLink = (expandCollapse, content) => + html.tag('a', {class: `${expandCollapse}-link`}, + {href: '#'}, + content); + + const actionsWhenCollapsed = + (data.hasLongerDescription && slots.extraReadingLinks + ? language.$(prefix, 'expandDescription.orReadMore', { + links: + language.formatDisjunctionList(slots.extraReadingLinks), + expand: + wrapExpandCollapseLink('expand', + language.$(prefix, 'expandDescription.orReadMore.expand')), + }) + : data.hasLongerDescription + ? language.$(prefix, 'expandDescription', { + expand: + wrapExpandCollapseLink('expand', + language.$(prefix, 'expandDescription.expand')), + }) + : null); + + const actionsWhenExpanded = + (data.hasLongerDescription && slots.extraReadingLinks + ? language.$(prefix, 'collapseDescription.orReadMore', { + links: + language.formatDisjunctionList(slots.extraReadingLinks), + collapse: + wrapExpandCollapseLink('collapse', + language.$(prefix, 'collapseDescription.orReadMore.collapse')), + }) + : data.hasLongerDescription + ? language.$(prefix, 'collapseDescription', { + collapse: + wrapExpandCollapseLink('collapse', + language.$(prefix, 'collapseDescription.collapse')), + }) + : null); + + const wrapActions = (attributes, children) => + html.tag('p', {class: 'quick-description-actions'}, + {[html.onlyIfContent]: true}, + attributes, + + children); + + const wrapContent = (attributes, content) => + html.tag('blockquote', {class: 'description-content'}, + {[html.onlyIfContent]: true}, + attributes, + + content?.slot('mode', 'multiline')); + + return ( + html.tag('div', {class: 'quick-description'}, + {[html.onlyIfContent]: true}, + + data.hasLongerDescription && + {class: 'collapsed'}, + + !data.hasLongerDescription && + !slots.extraReadingLinks && + {class: 'has-content-only'}, + + !data.hasDescription && + slots.extraReadingLinks && + {class: 'has-external-links-only'}, + + [ + wrapContent(null, relations.description), + wrapContent({class: 'short'}, relations.descriptionShort), + wrapContent({class: 'long'}, relations.descriptionLong), + + wrapActions(null, actionsWithoutLongerDescription), + wrapActions({class: 'when-collapsed'}, actionsWhenCollapsed), + wrapActions({class: 'when-expanded'}, actionsWhenExpanded), + ])); + }, +}; diff --git a/src/content/dependencies/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 2e6c4709..016e0a2c 100644 --- a/src/content/dependencies/generateReleaseInfoContributionsLine.js +++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js @@ -1,42 +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: { stringKey: {type: 'string'}, + featuringStringKey: {type: 'string'}, - showContribution: {type: 'boolean', default: true}, - showIcons: {type: 'boolean', default: true}, + 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, - showIcons: slots.showIcons, - iconMode: 'tooltip', - }))), - }); - }, + 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 new file mode 100644 index 00000000..308a1105 --- /dev/null +++ b/src/content/dependencies/generateSearchSidebarBox.js @@ -0,0 +1,82 @@ +export default { + contentDependencies: ['generatePageSidebarBox'], + extraDependencies: ['html', 'language'], + + relations: (relation) => ({ + sidebarBox: + relation('generatePageSidebarBox'), + }), + + generate: (relations, {html, language}) => + language.encapsulate('misc.search', capsule => + relations.sidebarBox.slots({ + attributes: {class: 'wiki-search-sidebar-box'}, + collapsible: false, + + content: [ + html.tag('label', {class: 'wiki-search-label'}, + html.tag('input', {class: 'wiki-search-input'}, + {type: 'search'}, + + { + placeholder: + language.$(capsule, 'placeholder').toString(), + })), + + html.tag('template', {class: 'wiki-search-preparing-string'}, + language.$(capsule, 'preparing')), + + html.tag('template', {class: 'wiki-search-loading-data-string'}, + language.$(capsule, 'loadingData')), + + html.tag('template', {class: 'wiki-search-searching-string'}, + language.$(capsule, 'searching')), + + html.tag('template', {class: 'wiki-search-failed-string'}, + language.$(capsule, 'failed')), + + html.tag('template', {class: 'wiki-search-no-results-string'}, + language.$(capsule, 'noResults')), + + html.tag('template', {class: 'wiki-search-current-result-string'}, + language.$(capsule, 'currentResult')), + + html.tag('template', {class: 'wiki-search-end-search-string'}, + language.$(capsule, 'endSearch')), + + language.encapsulate(capsule, 'resultKind', capsule => [ + html.tag('template', {class: 'wiki-search-album-result-kind-string'}, + language.$(capsule, 'album')), + + html.tag('template', {class: 'wiki-search-artist-result-kind-string'}, + language.$(capsule, 'artist')), + + html.tag('template', {class: 'wiki-search-group-result-kind-string'}, + language.$(capsule, 'group')), + + html.tag('template', {class: 'wiki-search-tag-result-kind-string'}, + language.$(capsule, 'artTag')), + ]), + + 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 9becfb26..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,22 +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) && - html.tag('div', {class: 'content-sticky-heading-cover-container'}, - html.tag('div', {class: 'content-sticky-heading-cover'}, - slots.cover.slot('mode', 'thumbnail'))), - ]), - - html.tag('div', {class: 'content-sticky-subheading-row'}, - html.tag('h2', {class: 'content-sticky-subheading'})), - ]), + {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'}, + {[html.onlyIfContent]: true}, + + (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'})), + ]))), + ]), }; 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 81f74aec..b09ee230 100644 --- a/src/content/dependencies/generateTooltip.js +++ b/src/content/dependencies/generateTooltip.js @@ -21,10 +21,14 @@ export default { generate: (slots, {html}) => html.tag('span', {class: 'tooltip'}, {[html.noEdgeWhitespace]: true}, + {[html.onlyIfContent]: true}, + {[html.onlyIfSiblings]: true}, slots.attributes, html.tag('span', {class: 'tooltip-content'}, {[html.noEdgeWhitespace]: true}, + {[html.onlyIfContent]: true}, slots.contentAttributes, + slots.content)), }; diff --git a/src/content/dependencies/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 6c056c9a..00000000 --- a/src/content/dependencies/generateTrackCoverArtwork.js +++ /dev/null @@ -1,28 +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, - }), - - generate: (data, relations) => - relations.coverArtwork.slots({ - path: data.path, - color: data.color, - }), -}; - diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 7b70d4ff..6c16ce27 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -1,579 +1,382 @@ -import {sortAlbumsTracksChronologically, sortFlashesChronologically} - from '#sort'; -import {empty, stitchArrays} from '#sugar'; - -import getChronologyRelations from '../util/getChronologyRelations.js'; - export default { contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateAdditionalFilesShortcut', - 'generateAlbumAdditionalFilesList', + 'generateAdditionalFilesList', + 'generateAdditionalNamesBox', 'generateAlbumNavAccent', + 'generateAlbumSecondaryNav', 'generateAlbumSidebar', - 'generateAlbumStyleRules', - 'generateChronologyLinks', - 'generateColorStyleAttribute', - 'generateCommentarySection', + 'generateAlbumStyleTags', + 'generateCommentaryEntry', + 'generateContentContentHeading', 'generateContentHeading', 'generateContributionList', + 'generateLyricsSection', 'generatePageLayout', - 'generateRelativeDatetimestamp', - 'generateTrackAdditionalNamesBox', - 'generateTrackCoverArtwork', + 'generateTrackArtistCommentarySection', + 'generateTrackArtworkColumn', + 'generateTrackInfoPageFeaturedByFlashesList', + 'generateTrackInfoPageOtherReleasesList', 'generateTrackList', 'generateTrackListDividedByGroups', + 'generateTrackNavLinks', 'generateTrackReleaseInfo', 'generateTrackSocialEmbed', 'linkAlbum', - 'linkArtist', - 'linkFlash', 'linkTrack', 'transformContent', ], - extraDependencies: ['html', 'language', 'wikiData'], - - sprawl({wikiInfo}) { - return { - divideTrackListsByGroups: wikiInfo.divideTrackListsByGroups, - enableFlashesAndGames: wikiInfo.enableFlashesAndGames, - }; - }, - - relations(relation, sprawl, track) { - const relations = {}; - const sections = relations.sections = {}; - const {album} = track; - - relations.layout = - relation('generatePageLayout'); - - relations.albumStyleRules = - relation('generateAlbumStyleRules', track.album, track); - - relations.socialEmbed = - relation('generateTrackSocialEmbed', track); - - relations.artistChronologyContributions = - getChronologyRelations(track, { - contributions: [ - ...track.artistContribs ?? [], - ...track.contributorContribs ?? [], - ], - - linkArtist: artist => relation('linkArtist', artist), - linkThing: track => relation('linkTrack', track), - - getThings(artist) { - const getDate = thing => thing.date; - - const things = [ - ...artist.tracksAsArtist, - ...artist.tracksAsContributor, - ].filter(getDate); - - return sortAlbumsTracksChronologically(things, {getDate}); - }, - }); - - relations.coverArtistChronologyContributions = - getChronologyRelations(track, { - contributions: track.coverArtistContribs ?? [], - - linkArtist: artist => relation('linkArtist', artist), - - linkThing: trackOrAlbum => - (trackOrAlbum.album - ? relation('linkTrack', trackOrAlbum) - : relation('linkAlbum', trackOrAlbum)), - - getThings(artist) { - const getDate = thing => thing.coverArtDate ?? thing.date; - - const things = [ - ...artist.albumsAsCoverArtist, - ...artist.tracksAsCoverArtist, - ].filter(getDate); - - return sortAlbumsTracksChronologically(things, {getDate}); - }, - }), - - relations.albumLink = - relation('linkAlbum', track.album); - - relations.trackLink = - relation('linkTrack', track); - - relations.albumNavAccent = - relation('generateAlbumNavAccent', track.album, track); - - relations.chronologyLinks = - relation('generateChronologyLinks'); - - relations.sidebar = - relation('generateAlbumSidebar', track.album, track); + extraDependencies: ['html', 'language'], - const additionalFilesSection = additionalFiles => ({ - heading: relation('generateContentHeading'), - list: relation('generateAlbumAdditionalFilesList', album, additionalFiles), - }); + query: (track) => ({ + mainReleaseTrack: + (track.isMainRelease + ? track + : track.mainReleaseTrack), + }), - // This'll take care of itself being blank if there's nothing to show here. - relations.additionalNamesBox = - relation('generateTrackAdditionalNamesBox', track); + relations: (relation, query, track) => ({ + layout: + relation('generatePageLayout'), - if (track.hasUniqueCoverArt || album.hasCoverArt) { - relations.cover = - relation('generateTrackCoverArtwork', track); - } + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), - // Section: Release info + socialEmbed: + relation('generateTrackSocialEmbed', track), - relations.releaseInfo = - relation('generateTrackReleaseInfo', track); + navLinks: + relation('generateTrackNavLinks', track), - // Section: Extra links + albumNavAccent: + relation('generateAlbumNavAccent', track.album, track), - const extra = sections.extra = {}; + secondaryNav: + relation('generateAlbumSecondaryNav', track.album), - if (!empty(track.additionalFiles)) { - extra.additionalFilesShortcut = - relation('generateAdditionalFilesShortcut', track.additionalFiles); - } + sidebar: + relation('generateAlbumSidebar', track.album, track), - // Section: Other releases + additionalNamesBox: + relation('generateAdditionalNamesBox', track.additionalNames), - if (!empty(track.otherReleases)) { - const otherReleases = sections.otherReleases = {}; + artworkColumn: + relation('generateTrackArtworkColumn', track), - otherReleases.heading = - relation('generateContentHeading'); + contentHeading: + relation('generateContentHeading'), - otherReleases.colorStyles = - track.otherReleases - .map(track => relation('generateColorStyleAttribute', track.color)); + contentContentHeading: + relation('generateContentContentHeading', track), - otherReleases.trackLinks = - track.otherReleases - .map(track => relation('linkTrack', track)); + releaseInfo: + relation('generateTrackReleaseInfo', track), - otherReleases.albumLinks = - track.otherReleases - .map(track => relation('linkAlbum', track.album)); + otherReleasesList: + relation('generateTrackInfoPageOtherReleasesList', track), - otherReleases.datetimestamps = - track.otherReleases.map(track2 => - (track2.date - ? (track.date - ? relation('generateRelativeDatetimestamp', - track2.date, - track.date) - : relation('generateAbsoluteDatetimestamp', - track2.date)) - : null)); + contributorContributionList: + relation('generateContributionList', track.contributorContribs), - otherReleases.items = - track.otherReleases.map(track => ({ - trackLink: relation('linkTrack', track), - albumLink: relation('linkAlbum', track.album), - })); - } + referencedTracksList: + relation('generateTrackList', track.referencedTracks), - // Section: Contributors + sampledTracksList: + relation('generateTrackList', track.sampledTracks), - if (!empty(track.contributorContribs)) { - const contributors = sections.contributors = {}; + referencedByTracksList: + relation('generateTrackListDividedByGroups', + query.mainReleaseTrack.referencedByTracks), - contributors.heading = - relation('generateContentHeading'); + sampledByTracksList: + relation('generateTrackListDividedByGroups', + query.mainReleaseTrack.sampledByTracks), - contributors.list = - relation('generateContributionList', track.contributorContribs); - } + flashesThatFeatureList: + relation('generateTrackInfoPageFeaturedByFlashesList', track), - // Section: Referenced tracks + lyricsSection: + relation('generateLyricsSection', track.lyrics), - if (!empty(track.referencedTracks)) { - const references = sections.references = {}; + sheetMusicFilesList: + relation('generateAdditionalFilesList', track.sheetMusicFiles), - references.heading = - relation('generateContentHeading'); + midiProjectFilesList: + relation('generateAdditionalFilesList', track.midiProjectFiles), - references.list = - relation('generateTrackList', track.referencedTracks); - } + additionalFilesList: + relation('generateAdditionalFilesList', track.additionalFiles), - // Section: Sampled tracks + artistCommentarySection: + relation('generateTrackArtistCommentarySection', track), - if (!empty(track.sampledTracks)) { - const samples = sections.samples = {}; + creditingSourceEntries: + track.creditingSources + .map(entry => relation('generateCommentaryEntry', entry)), - samples.heading = - relation('generateContentHeading'); + referencingSourceEntries: + track.referencingSources + .map(entry => relation('generateCommentaryEntry', entry)), + }), - samples.list = - relation('generateTrackList', track.sampledTracks); - } + data: (_query, track) => ({ + name: + track.name, - // Section: Tracks that reference + color: + track.color, + }), - if (!empty(track.referencedByTracks)) { - const referencedBy = sections.referencedBy = {}; - - referencedBy.heading = - relation('generateContentHeading'); - - referencedBy.list = - relation('generateTrackListDividedByGroups', - track.referencedByTracks, - sprawl.divideTrackListsByGroups); - } - - // Section: Tracks that sample - - if (!empty(track.sampledByTracks)) { - const sampledBy = sections.sampledBy = {}; - - sampledBy.heading = - relation('generateContentHeading'); - - sampledBy.list = - relation('generateTrackListDividedByGroups', - track.sampledByTracks, - sprawl.divideTrackListsByGroups); - } - - // Section: Flashes that feature + generate: (data, relations, {html, language}) => + language.encapsulate('trackPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + track: data.name, + }), - if (sprawl.enableFlashesAndGames) { - const sortedFeatures = - sortFlashesChronologically( - [track, ...track.otherReleases].flatMap(track => - track.featuredInFlashes.map(flash => ({ - // These aren't going to be exposed directly, they're processed - // into the appropriate relations after this sort. - flash, track, + headingMode: 'sticky', - // These properties are only used for the sort. - act: flash.act, - date: flash.date, - })))); + additionalNames: relations.additionalNamesBox, - if (!empty(sortedFeatures)) { - const flashesThatFeature = sections.flashesThatFeature = {}; + color: data.color, + styleTags: relations.albumStyleTags, - flashesThatFeature.heading = - relation('generateContentHeading'); + artworkColumnContent: + relations.artworkColumn, - flashesThatFeature.entries = - sortedFeatures.map(({flash, track: directlyFeaturedTrack}) => - (directlyFeaturedTrack === track - ? { - flashLink: relation('linkFlash', flash), - } - : { - flashLink: relation('linkFlash', flash), - trackLink: relation('linkTrack', directlyFeaturedTrack), - })); - } - } + mainContent: [ + relations.releaseInfo, - // Section: Lyrics + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, - if (track.lyrics) { - const lyrics = sections.lyrics = {}; + language.encapsulate('releaseInfo', capsule => [ + !html.isBlank(relations.sheetMusicFilesList) && + language.encapsulate(capsule, 'sheetMusicFiles.shortcut', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#sheet-music-files'}, + language.$(capsule, 'link')), + })), - lyrics.heading = - relation('generateContentHeading'); + !html.isBlank(relations.midiProjectFilesList) && + language.encapsulate(capsule, 'midiProjectFiles.shortcut', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#midi-project-files'}, + language.$(capsule, 'link')), + })), - lyrics.content = - relation('transformContent', track.lyrics); - } + !html.isBlank(relations.additionalFilesList) && + language.encapsulate(capsule, 'additionalFiles.shortcut', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#midi-project-files'}, + language.$(capsule, 'link')), + })), - // Sections: Sheet music files, MIDI/proejct files, additional files + !html.isBlank(relations.artistCommentarySection) && + language.encapsulate(capsule, 'readCommentary', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#artist-commentary'}, + language.$(capsule, 'link')), + })), - if (!empty(track.sheetMusicFiles)) { - sections.sheetMusicFiles = additionalFilesSection(track.sheetMusicFiles); - } + !html.isBlank(relations.creditingSourceEntries) && + language.encapsulate(capsule, 'readCreditingSources', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#crediting-sources'}, + language.$(capsule, 'link')), + })), - if (!empty(track.midiProjectFiles)) { - sections.midiProjectFiles = additionalFilesSection(track.midiProjectFiles); - } + !html.isBlank(relations.referencingSourceEntries) && + language.encapsulate(capsule, 'readReferencingSources', capsule => + language.$(capsule, { + link: + html.tag('a', + {href: '#referencing-sources'}, + language.$(capsule, 'link')), + })), + ])), - if (!empty(track.additionalFiles)) { - sections.additionalFiles = additionalFilesSection(track.additionalFiles); - } + relations.otherReleasesList, - // Section: Artist commentary + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'contributors'}, + title: language.$('releaseInfo.contributors'), + }), - if (track.commentary) { - sections.artistCommentary = - relation('generateCommentarySection', track.commentary); - } + relations.contributorContributionList.slots({ + chronologyKind: 'trackContribution', + }), + ]), + + html.tags([ + language.encapsulate('releaseInfo.tracksReferenced', capsule => + relations.contentHeading.clone() + .slots({ + attributes: {id: 'references'}, + + title: + language.$(capsule, { + track: + html.tag('i', data.name), + }), + + stickyTitle: + language.$(capsule, 'sticky'), + })), - return relations; - }, + relations.referencedTracksList, + ]), - data(sprawl, track) { - return { - name: track.name, - color: track.color, + html.tags([ + language.encapsulate('releaseInfo.tracksSampled', capsule => + relations.contentHeading.clone() + .slots({ + attributes: {id: 'samples'}, - hasTrackNumbers: track.album.hasTrackNumbers, - trackNumber: track.album.tracks.indexOf(track) + 1, + title: + language.$(capsule, { + track: + html.tag('i', data.name), + }), - numAdditionalFiles: track.additionalFiles.length, - }; - }, + stickyTitle: + language.$(capsule, 'sticky'), + })), - generate(data, relations, {html, language}) { - const {sections: sec} = relations; + relations.sampledTracksList, + ]), - return relations.layout - .slots({ - title: language.$('trackPage.title', {track: data.name}), - headingMode: 'sticky', + language.encapsulate('releaseInfo.tracksThatReference', capsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'referenced-by'}, - additionalNames: relations.additionalNamesBox, + title: + language.$(capsule, { + track: html.tag('i', data.name), + }), - color: data.color, - styleRules: [relations.albumStyleRules], + stickyTitle: + language.$(capsule, 'sticky'), + }), - cover: - (relations.cover - ? relations.cover.slots({ - alt: language.$('misc.alt.trackCover'), - }) - : null), + relations.referencedByTracksList + .slots({ + headingString: capsule, + }), + ])), - mainContent: [ - relations.releaseInfo, + language.encapsulate('releaseInfo.tracksThatSample', capsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + attributes: {id: 'sampled-by'}, - html.tag('p', - {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, + title: + language.$(capsule, { + track: html.tag('i', data.name), + }), - [ - sec.sheetMusicFiles && - language.$('releaseInfo.sheetMusicFiles.shortcut', { - link: html.tag('a', - {href: '#sheet-music-files'}, - language.$('releaseInfo.sheetMusicFiles.shortcut.link')), + stickyTitle: + language.$(capsule, 'sticky'), }), - sec.midiProjectFiles && - language.$('releaseInfo.midiProjectFiles.shortcut', { - link: html.tag('a', - {href: '#midi-project-files'}, - language.$('releaseInfo.midiProjectFiles.shortcut.link')), + relations.sampledByTracksList + .slots({ + headingString: capsule, }), + ])), - sec.additionalFiles && - sec.extra.additionalFilesShortcut, + html.tags([ + language.encapsulate('releaseInfo.flashesThatFeature', capsule => + relations.contentHeading.clone() + .slots({ + attributes: {id: 'featured-in'}, - sec.artistCommentary && - language.$('releaseInfo.readCommentary', { - link: html.tag('a', - {href: '#artist-commentary'}, - language.$('releaseInfo.readCommentary.link')), - }), - ]), + title: + language.$(capsule, { + track: html.tag('i', data.name), + }), - sec.otherReleases && [ - sec.otherReleases.heading - .slots({ - id: 'also-released-as', - title: language.$('releaseInfo.alsoReleasedAs'), - }), - - html.tag('ul', - stitchArrays({ - trackLink: sec.otherReleases.trackLinks, - albumLink: sec.otherReleases.albumLinks, - datetimestamp: sec.otherReleases.datetimestamps, - colorStyle: sec.otherReleases.colorStyles, - }).map(({ - trackLink, - albumLink, - datetimestamp, - colorStyle, - }) => { - const parts = ['releaseInfo.alsoReleasedAs.item']; - const options = {}; - - options.track = trackLink.slot('color', false); - options.album = albumLink; - - if (datetimestamp) { - parts.push('withYear'); - options.year = - datetimestamp.slots({ - style: 'year', - tooltip: true, - }); - } - - return ( - html.tag('li', - colorStyle, - language.$(...parts, options))); + stickyTitle: + language.$(capsule, 'sticky'), })), - ], - - sec.contributors && [ - sec.contributors.heading - .slots({ - id: 'contributors', - title: language.$('releaseInfo.contributors'), - }), - - sec.contributors.list, - ], - sec.references && [ - sec.references.heading - .slots({ - id: 'references', - title: - language.$('releaseInfo.tracksReferenced', { - track: html.tag('i', data.name), - }), - }), + relations.flashesThatFeatureList, + ]), - sec.references.list, - ], + relations.lyricsSection, - sec.samples && [ - sec.samples.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'samples', - title: - language.$('releaseInfo.tracksSampled', { - track: html.tag('i', data.name), - }), + attributes: {id: 'sheet-music-files'}, + title: language.$('releaseInfo.sheetMusicFiles.heading'), }), - sec.samples.list, - ], + relations.sheetMusicFilesList, + ]), - sec.referencedBy && [ - sec.referencedBy.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'referenced-by', - title: - language.$('releaseInfo.tracksThatReference', { - track: html.tag('i', data.name), - }), + attributes: {id: 'midi-project-files'}, + title: language.$('releaseInfo.midiProjectFiles.heading'), }), - sec.referencedBy.list, - ], + relations.midiProjectFilesList, + ]), - sec.sampledBy && [ - sec.sampledBy.heading + html.tags([ + relations.contentHeading.clone() .slots({ - id: 'referenced-by', - title: - language.$('releaseInfo.tracksThatSample', { - track: html.tag('i', data.name), - }), + attributes: {id: 'additional-files'}, + title: language.$('releaseInfo.additionalFiles.heading'), }), - sec.sampledBy.list, - ], + relations.additionalFilesList, + ]), - sec.flashesThatFeature && [ - sec.flashesThatFeature.heading - .slots({ - id: 'featured-in', - title: - language.$('releaseInfo.flashesThatFeature', { - track: html.tag('i', data.name), - }), - }), + relations.artistCommentarySection, - html.tag('ul', sec.flashesThatFeature.entries.map(({flashLink, trackLink}) => - (trackLink - ? html.tag('li', {class: 'rerelease'}, - language.$('releaseInfo.flashesThatFeature.item.asDifferentRelease', { - flash: flashLink, - track: trackLink, - })) - : html.tag('li', - language.$('releaseInfo.flashesThatFeature.item', { - flash: flashLink, - }))))), - ], - - sec.lyrics && [ - sec.lyrics.heading + html.tags([ + relations.contentContentHeading.clone() .slots({ - id: 'lyrics', - title: language.$('releaseInfo.lyrics'), + attributes: {id: 'crediting-sources'}, + string: 'misc.creditingSources', }), - html.tag('blockquote', - sec.lyrics.content - .slot('mode', 'lyrics')), - ], + relations.creditingSourceEntries, + ]), - sec.sheetMusicFiles && [ - sec.sheetMusicFiles.heading + html.tags([ + relations.contentContentHeading.clone() .slots({ - id: 'sheet-music-files', - title: language.$('releaseInfo.sheetMusicFiles.heading'), + attributes: {id: 'referencing-sources'}, + string: 'misc.referencingSources', }), - sec.sheetMusicFiles.list, - ], - - sec.midiProjectFiles && [ - sec.midiProjectFiles.heading - .slots({ - id: 'midi-project-files', - title: language.$('releaseInfo.midiProjectFiles.heading'), - }), - - sec.midiProjectFiles.list, - ], - - sec.additionalFiles && [ - sec.additionalFiles.heading - .slots({ - id: 'additional-files', - title: - language.$('releaseInfo.additionalFiles.heading', { - additionalFiles: - language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}), - }), - }), - - sec.additionalFiles.list, - ], - - sec.artistCommentary, + relations.referencingSourceEntries, + ]), ], navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - {html: relations.albumLink.slot('color', false)}, - { - html: - (data.hasTrackNumbers - ? language.$('trackPage.nav.track.withNumber', { - number: data.trackNumber, - track: relations.trackLink - .slot('attributes', {class: 'current'}), - }) - : language.$('trackPage.nav.track', { - track: relations.trackLink - .slot('attributes', {class: 'current'}), - })), - }, - ], + navLinks: html.resolve(relations.navLinks), navBottomRowContent: relations.albumNavAccent.slots({ @@ -581,25 +384,14 @@ export default { showExtraLinks: false, }), - navContent: - relations.chronologyLinks.slots({ - chronologyInfoSets: [ - { - headingString: 'misc.chronology.heading.track', - contributions: relations.artistChronologyContributions, - }, - { - headingString: 'misc.chronology.heading.coverArt', - contributions: relations.coverArtistChronologyContributions, - }, - ], - }), + secondaryNav: + relations.secondaryNav + .slot('mode', 'track'), - ...relations.sidebar, + leftSidebar: relations.sidebar, socialEmbed: relations.socialEmbed, - }); - }, + })), }; /* diff --git a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js new file mode 100644 index 00000000..61654512 --- /dev/null +++ b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js @@ -0,0 +1,63 @@ +import {sortFlashesChronologically} from '#sort'; +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['linkFlash', 'linkTrack'], + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({wikiInfo}) => ({ + enableFlashesAndGames: + wikiInfo.enableFlashesAndGames, + }), + + query: (sprawl, track) => ({ + sortedFeatures: + (sprawl.enableFlashesAndGames + ? sortFlashesChronologically( + track.allReleases.flatMap(track => + track.featuredInFlashes.map(flash => ({ + flash, + track, + + // These properties are only used for the sort. + act: flash.act, + date: flash.date, + })))) + : []), + }), + + relations: (relation, query, _sprawl, track) => ({ + flashLinks: + query.sortedFeatures + .map(({flash}) => relation('linkFlash', flash)), + + trackLinks: + query.sortedFeatures + .map(({track: directlyFeaturedTrack}) => + (directlyFeaturedTrack === track + ? null + : directlyFeaturedTrack.name === track.name + ? null + : relation('linkTrack', directlyFeaturedTrack))), + }), + + generate: (relations, {html, language}) => + html.tag('ul', + {[html.onlyIfContent]: true}, + + stitchArrays({ + flashLink: relations.flashLinks, + trackLink: relations.trackLinks, + }).map(({flashLink, trackLink}) => { + const attributes = html.attributes(); + const parts = ['releaseInfo.flashesThatFeature.item']; + const options = {flash: flashLink}; + + if (trackLink) { + parts.push('asDifferentRelease'); + options.track = trackLink; + } + + return html.tag('li', attributes, language.$(...parts, options)); + })), +}; diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js new file mode 100644 index 00000000..ebd76577 --- /dev/null +++ b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js @@ -0,0 +1,42 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['linkTrack'], + extraDependencies: ['html', 'language'], + + relations: (relation, track) => ({ + trackLinks: + track.otherReleases + .map(track => relation('linkTrack', track)), + }), + + data: (track) => ({ + albumNames: + track.otherReleases + .map(track => track.album.name), + + albumColors: + track.otherReleases + .map(track => track.album.color), + }), + + generate: (data, relations, {html, language}) => + html.tag('p', + {[html.onlyIfContent]: true}, + + 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 65f5552b..53a32536 100644 --- a/src/content/dependencies/generateTrackList.js +++ b/src/content/dependencies/generateTrackList.js @@ -1,58 +1,28 @@ -import {empty, stitchArrays} from '#sugar'; - export default { - contentDependencies: ['linkTrack', 'linkContribution'], - - extraDependencies: ['html', 'language'], + contentDependencies: ['generateTrackListItem'], + extraDependencies: ['html'], - relations(relation, tracks) { - if (empty(tracks)) { - return {}; - } - - return { - trackLinks: - tracks - .map(track => relation('linkTrack', track)), - - contributionLinks: - tracks - .map(track => - (empty(track.artistContribs) - ? null - : track.artistContribs - .map(contrib => relation('linkContribution', contrib)))), - }; - }, + relations: (relation, tracks) => ({ + items: + tracks + .map(track => relation('generateTrackListItem', track, [])), + }), slots: { - showContribution: {type: 'boolean', default: false}, - showIcons: {type: 'boolean', default: false}, + colorMode: { + validate: v => v.is('none', 'track', 'line'), + default: 'track', + }, }, - generate(relations, slots, {html, language}) { - return ( - html.tag('ul', - stitchArrays({ - trackLink: relations.trackLinks, - contributionLinks: relations.contributionLinks, - }).map(({trackLink, contributionLinks}) => - html.tag('li', - (empty(contributionLinks) - ? trackLink - : language.$('trackList.item.withArtists', { - track: trackLink, - by: - html.tag('span', {class: 'by'}, - language.$('trackList.item.withArtists.by', { - artists: - language.formatConjunctionList( - contributionLinks.map(link => - link.slots({ - showContribution: slots.showContribution, - showIcons: slots.showIcons, - }))), - })), - })))))); - }, + generate: (relations, slots, {html}) => + html.tag('ul', + {[html.onlyIfContent]: true}, + + 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 e070ac35..230868d6 100644 --- a/src/content/dependencies/generateTrackListDividedByGroups.js +++ b/src/content/dependencies/generateTrackListDividedByGroups.js @@ -1,53 +1,145 @@ -import {empty} from '#sugar'; - -import groupTracksByGroup from '../util/groupTracksByGroup.js'; +import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateTrackList', 'linkGroup'], - extraDependencies: ['html', 'language'], + contentDependencies: [ + 'generateContentHeading', + 'generateTrackList', + 'linkGroup', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({wikiInfo}) => ({ + divideTrackListsByGroups: + wikiInfo.divideTrackListsByGroups, + }), - relations(relation, tracks, groups) { - if (empty(tracks)) { - return {}; + query(sprawl, tracks) { + const dividingGroups = sprawl.divideTrackListsByGroups; + + const groupings = new Map(); + const ungroupedTracks = []; + + // Entry order matters! Add blank lists for each group + // in the order that those groups are provided. + for (const group of dividingGroups) { + groupings.set(group, []); } - if (empty(groups)) { - return { - flatList: - relation('generateTrackList', tracks), - }; + for (const track of tracks) { + const firstMatchingGroup = + dividingGroups.find(group => group.albums.includes(track.album)); + + if (firstMatchingGroup) { + groupings.get(firstMatchingGroup).push(track); + } else { + ungroupedTracks.push(track); + } } - const lists = groupTracksByGroup(tracks, groups); + const groups = Array.from(groupings.keys()); + const groupedTracks = Array.from(groupings.values()); - return { - groupedLists: - Array.from(lists.entries()).map(([groupOrOther, tracks]) => ({ - ...(groupOrOther === 'other' - ? {other: true} - : {groupLink: relation('linkGroup', groupOrOther)}), + // Drop the empty lists, so just the groups which + // at least a single track matched are left. + filterMultipleArrays( + groups, + groupedTracks, + (_group, tracks) => !empty(tracks)); - list: - relation('generateTrackList', tracks), - })), - }; + return {groups, groupedTracks, ungroupedTracks}; }, - generate(relations, {html, language}) { - if (relations.flatList) { - return relations.flatList; - } + relations: (relation, query, sprawl, tracks) => ({ + flatList: + (empty(sprawl.divideTrackListsByGroups) + ? relation('generateTrackList', tracks) + : null), + + contentHeading: + relation('generateContentHeading'), + + groupLinks: + query.groups + .map(group => relation('linkGroup', group)), + + groupedTrackLists: + query.groupedTracks + .map(tracks => relation('generateTrackList', tracks)), + + ungroupedTrackList: + (empty(query.ungroupedTracks) + ? null + : relation('generateTrackList', query.ungroupedTracks)), + }), - return html.tag('dl', - relations.groupedLists.map(({other, groupLink, list}) => [ - html.tag('dt', - (other - ? language.$('trackList.group.fromOther') - : language.$('trackList.group', { - group: groupLink - }))), - - html.tag('dd', list), - ])); + data: (query, _sprawl, _tracks) => ({ + groupNames: + query.groups + .map(group => group.name), + }), + + slots: { + headingString: { + type: 'string', + }, }, + + generate: (data, relations, slots, {html, language}) => + relations.flatList ?? + + html.tag('dl', + {[html.onlyIfContent]: true}, + + language.encapsulate('trackList', listCapsule => [ + stitchArrays({ + groupName: data.groupNames, + groupLink: relations.groupLinks, + trackList: relations.groupedTrackLists, + }).map(({ + groupName, + groupLink, + trackList, + }) => [ + language.encapsulate(listCapsule, 'fromGroup', capsule => + (slots.headingString + ? relations.contentHeading.clone().slots({ + tag: 'dt', + + title: + language.$(capsule, { + group: groupLink + }), + + stickyTitle: + language.$(slots.headingString, 'sticky', 'fromGroup', { + group: groupName, + }), + }) + : html.tag('dt', + language.$(capsule, { + group: groupLink + })))), + + html.tag('dd', trackList), + ]), + + relations.ungroupedTrackList && [ + language.encapsulate(listCapsule, 'fromOther', capsule => + (slots.headingString + ? relations.contentHeading.clone().slots({ + tag: 'dt', + + title: + language.$(capsule), + + stickyTitle: + language.$(slots.headingString, 'sticky', 'fromOther'), + }) + : html.tag('dt', + language.$(capsule)))), + + html.tag('dd', relations.ungroupedTrackList), + ], + ])), }; diff --git a/src/content/dependencies/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/generateTrackListMissingDuration.js b/src/content/dependencies/generateTrackListMissingDuration.js new file mode 100644 index 00000000..b5917982 --- /dev/null +++ b/src/content/dependencies/generateTrackListMissingDuration.js @@ -0,0 +1,35 @@ +export default { + contentDependencies: ['generateTextWithTooltip', 'generateTooltip'], + extraDependencies: ['html', 'language'], + + relations: (relation) => ({ + textWithTooltip: + relation('generateTextWithTooltip'), + + tooltip: + relation('generateTooltip'), + }), + + generate: (relations, {html, language}) => + language.encapsulate('trackList.item.withDuration', itemCapsule => + language.encapsulate(itemCapsule, 'duration', durationCapsule => + relations.textWithTooltip.slots({ + attributes: {class: 'missing-duration'}, + customInteractionCue: true, + + text: + language.$(durationCapsule, { + duration: + html.tag('span', {class: 'text-with-tooltip-interaction-cue'}, + language.$(durationCapsule, 'missing')), + }), + + tooltip: + relations.tooltip.slots({ + attributes: {class: 'missing-duration-tooltip'}, + + content: + language.$(durationCapsule, 'missing.info'), + }), + }))), +}; 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 3bdeaa4f..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; @@ -47,44 +36,34 @@ export default { }, generate: (data, relations, {html, language}) => - html.tags([ - html.tag('p', - {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, - - [ - relations.artistContributionLinks - .slots({stringKey: 'releaseInfo.by'}), - - relations.coverArtistContributionsLine - ?.slots({stringKey: 'releaseInfo.coverArtBy'}), - - data.date && - language.$('releaseInfo.released', { - date: language.formatDate(data.date), + language.encapsulate('releaseInfo', capsule => + html.tags([ + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + relations.artistContributionsLine.slots({ + stringKey: capsule + '.by', + featuringStringKey: capsule + '.by.featuring', + chronologyKind: 'track', }), - data.coverArtDate && - language.$('releaseInfo.artReleased', { - date: language.formatDate(data.coverArtDate), + language.$(capsule, 'released', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.date), }), - data.duration && - language.$('releaseInfo.duration', { + language.$(capsule, 'duration', { + [language.onlyIfOptions]: ['duration'], duration: language.formatDuration(data.duration), }), - ]), - - html.tag('p', - (relations.externalLinks - ? language.$('releaseInfo.listenOn', { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => link.slot('context', 'track'))), - }) - : language.$('releaseInfo.listenOn.noLinks', { - name: html.tag('i', data.name), - }))), - ]), + ]), + + html.tag('p', + relations.listenLine.slots({ + visibleWithoutLinks: true, + context: ['track'], + })), + ])), }; diff --git a/src/content/dependencies/generateTrackSocialEmbed.js b/src/content/dependencies/generateTrackSocialEmbed.js index 0337fc46..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,61 +26,39 @@ 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}) { - return relations.socialEmbed.slots({ - title: - language.$('trackPage.socialEmbed.title', { - track: data.trackName, - }), + generate: (data, relations, {absoluteTo, language}) => + language.encapsulate('trackPage.socialEmbed', embedCapsule => + relations.socialEmbed.slots({ + title: + language.$(embedCapsule, 'title', { + track: data.trackName, + }), - headingContent: - language.$('trackPage.socialEmbed.heading', { - album: data.albumName, - }), + description: + relations.description, - headingLink: - absoluteTo('localized.album', data.albumDirectory), + headingContent: + language.$(embedCapsule, 'heading', { + album: data.albumName, + }), - imagePath: - (data.imageSource === 'album' - ? '/' + - urls - .from('shared.root') - .to('media.albumCover', data.albumDirectory, data.coverArtFileExtension) - : data.imageSource === 'track' - ? '/' + - urls - .from('shared.root') - .to('media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension) - : null), - }); - }, -}; + headingLink: + absoluteTo('localized.album', data.albumDirectory), -/* - 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, - }, -*/ + imagePath: + (data.hasImage + ? data.imagePath + : null), + })), +}; 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 a19f104c..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: - name && - language.$('misc.albumGrid.noCoverArt', { - 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/generateWikiHomeNewsBox.js b/src/content/dependencies/generateWikiHomeNewsBox.js deleted file mode 100644 index f592ab99..00000000 --- a/src/content/dependencies/generateWikiHomeNewsBox.js +++ /dev/null @@ -1,82 +0,0 @@ -import {empty, stitchArrays} from '#sugar'; - -export default { - contentDependencies: ['linkNewsEntry', 'transformContent'], - extraDependencies: ['html', 'language', 'wikiData'], - - sprawl({newsData}) { - return { - entries: newsData.slice(0, 3), - }; - }, - - relations(relation, sprawl) { - return { - entryContents: - sprawl.entries - .map(entry => relation('transformContent', entry.contentShort)), - - entryMainLinks: - sprawl.entries - .map(entry => relation('linkNewsEntry', entry)), - - entryReadMoreLinks: - sprawl.entries - .map(entry => - entry.contentShort !== entry.content && - relation('linkNewsEntry', entry)), - }; - }, - - data(sprawl) { - return { - entryDates: - sprawl.entries - .map(entry => entry.date), - } - }, - - generate(data, relations, {html, language}) { - if (empty(relations.entryContents)) { - return html.blank(); - } - - return { - class: 'latest-news-sidebar-box', - content: [ - html.tag('h1', language.$('homepage.news.title')), - - stitchArrays({ - date: data.entryDates, - content: relations.entryContents, - mainLink: relations.entryMainLinks, - readMoreLink: relations.entryReadMoreLinks, - }).map(({ - date, - content, - mainLink, - readMoreLink, - }, index) => - html.tag('article', {class: 'news-entry'}, - index === 0 && - {class: 'first-news-entry'}, - - [ - html.tag('h2', [ - html.tag('time', language.formatDate(date)), - mainLink, - ]), - - content.slot('thumb', 'medium'), - - html.tag('p', - {[html.onlyIfContent]: true}, - readMoreLink - ?.slots({ - content: language.$('homepage.news.entry.viewRest'), - })), - ])), - ], - }; - }, -}; diff --git a/src/content/dependencies/generateWikiHomePage.js b/src/content/dependencies/generateWikiHomePage.js deleted file mode 100644 index 36fcc6f2..00000000 --- a/src/content/dependencies/generateWikiHomePage.js +++ /dev/null @@ -1,105 +0,0 @@ -export default { - contentDependencies: [ - 'generatePageLayout', - 'generateWikiHomeAlbumsRow', - 'generateWikiHomeNewsBox', - 'transformContent', - ], - - extraDependencies: ['wikiData'], - - sprawl({wikiInfo}) { - return { - wikiName: wikiInfo.name, - - enableNews: wikiInfo.enableNews, - }; - }, - - relations(relation, sprawl, homepageLayout) { - const relations = {}; - - relations.layout = - relation('generatePageLayout'); - - if (homepageLayout.sidebarContent) { - 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, - ], - - leftSidebarCollapse: false, - leftSidebarWide: true, - - leftSidebarMultiple: [ - (relations.customSidebarContent - ? { - class: 'custom-content-sidebar-box', - content: - relations.customSidebarContent - .slot('mode', 'multiline'), - } - : null), - - relations.newsSidebarBox ?? null, - ], - - 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/generateWikiHomepageNewsBox.js b/src/content/dependencies/generateWikiHomepageNewsBox.js new file mode 100644 index 00000000..83a27695 --- /dev/null +++ b/src/content/dependencies/generateWikiHomepageNewsBox.js @@ -0,0 +1,86 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generatePageSidebarBox', + 'linkNewsEntry', + 'transformContent', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({newsData}) => ({ + entries: + newsData.slice(0, 3), + }), + + relations: (relation, sprawl) => ({ + box: + relation('generatePageSidebarBox'), + + entryContents: + sprawl.entries + .map(entry => relation('transformContent', entry.contentShort)), + + entryMainLinks: + sprawl.entries + .map(entry => relation('linkNewsEntry', entry)), + + entryReadMoreLinks: + sprawl.entries + .map(entry => + entry.contentShort !== entry.content && + relation('linkNewsEntry', entry)), + }), + + data: (sprawl) => ({ + entryDates: + sprawl.entries + .map(entry => entry.date), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('homepage.news', boxCapsule => + relations.box.slots({ + attributes: {class: 'latest-news-sidebar-box'}, + collapsible: false, + + content: [ + html.tag('h1', + {[html.onlyIfSiblings]: true}, + language.$(boxCapsule, 'title')), + + stitchArrays({ + date: data.entryDates, + content: relations.entryContents, + mainLink: relations.entryMainLinks, + readMoreLink: relations.entryReadMoreLinks, + }).map(({ + date, + content, + mainLink, + readMoreLink, + }, index) => + language.encapsulate(boxCapsule, 'entry', entryCapsule => + html.tag('article', {class: 'news-entry'}, + index === 0 && + {class: 'first-news-entry'}, + + [ + html.tag('h2', [ + html.tag('time', language.formatDate(date)), + mainLink, + ]), + + content.slot('thumb', 'medium'), + + html.tag('p', + {[html.onlyIfContent]: true}, + readMoreLink + ?.slots({ + content: language.$(entryCapsule, 'viewRest'), + })), + ]))), + ], + })), +}; 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 db307a6b..bf47b14f 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -1,12 +1,11 @@ -import {logInfo, logWarn} from '#cli'; +import {logWarn} from '#cli'; import {empty} from '#sugar'; export default { extraDependencies: [ - 'cachebust', 'checkIfImagePathHasCachedThumbnails', 'getDimensionsOfImagePath', - 'getSizeOfImagePath', + 'getSizeOfMediaFile', 'getThumbnailEqualOrSmaller', 'getThumbnailsAvailableForDimensions', 'html', @@ -17,72 +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'}, - width: {type: 'number'}, - height: {type: 'number'}, - attributes: { - type: 'attributes', - mutable: false, + // 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. + + src: {type: 'string'}, + + 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, { - cachebust, checkIfImagePathHasCachedThumbnails, getDimensionsOfImagePath, - getSizeOfImagePath, + getSizeOfMediaFile, getThumbnailEqualOrSmaller, getThumbnailsAvailableForDimensions, html, @@ -90,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 @@ -116,32 +125,31 @@ export default { const isMissingImageFile = missingImagePaths.includes(mediaSrc); - if (isMissingImageFile) { - logInfo`No image file for ${mediaSrc} - build again for list of missing images.`; - } - const willLink = !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.width && {width: slots.width}, - slots.height && {height: slots.height}, + + dimensions && + dimensions[0] && + {width: dimensions[0]}, + + dimensions && + dimensions[1] && + {height: dimensions[1]}, ]); const isPlaceholder = @@ -161,13 +169,13 @@ export default { if (willReveal) { reveal = [ html.tag('img', {class: 'reveal-symbol'}, - {src: to('shared.staticFile', 'warning.svg', cachebust)}), + {src: to('staticMisc.path', 'warning.svg')}), html.tag('br'), html.tag('span', {class: 'reveal-warnings'}, language.$('misc.contentWarnings.warnings', { - warnings: language.formatUnitList(contentWarnings), + warnings: language.formatUnitList(warnings), })), html.tag('br'), @@ -220,23 +228,19 @@ export default { to('thumb.path', mediaSrcJpeg); } - const dimensions = getDimensionsOfImagePath(mediaSrc); - const availableThumbs = getThumbnailsAvailableForDimensions(dimensions); - - const [width, height] = dimensions; - const originalLength = Math.max(width, height) + const originalDimensions = getDimensionsOfImagePath(mediaSrc); + const availableThumbs = getThumbnailsAvailableForDimensions(originalDimensions); 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 new file mode 100644 index 00000000..e408c1b2 --- /dev/null +++ b/src/content/dependencies/linkAnythingMan.js @@ -0,0 +1,28 @@ +export default { + contentDependencies: [ + 'linkAlbum', + 'linkArtwork', + 'linkFlash', + 'linkTrack', + ], + + query: (thing) => ({ + referenceType: thing.constructor[Symbol.for('Thing.referenceType')], + }), + + relations: (relation, query, thing) => ({ + link: + (query.referenceType === 'album' + ? relation('linkAlbum', thing) + : query.referenceType === 'artwork' + ? relation('linkArtwork', thing) + : query.referenceType === 'flash' + ? relation('linkFlash', thing) + : query.referenceType === 'track' + ? relation('linkTrack', thing) + : null), + }), + + generate: (relations) => + relations.link, +}; 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 cb57aa47..c658d461 100644 --- a/src/content/dependencies/linkContribution.js +++ b/src/content/dependencies/linkContribution.js @@ -1,120 +1,85 @@ -import {empty} from '#sugar'; - export default { contentDependencies: [ + 'generateContributionTooltip', 'generateTextWithTooltip', - 'generateTooltip', 'linkArtist', - 'linkExternalAsIcon', ], extraDependencies: ['html', 'language'], - relations(relation, contribution) { - const relations = {}; + relations: (relation, contribution) => ({ + artistLink: + relation('linkArtist', contribution.artist), - relations.artistLink = - relation('linkArtist', contribution.who); + textWithTooltip: + relation('generateTextWithTooltip'), - relations.textWithTooltip = - relation('generateTextWithTooltip'); + tooltip: + relation('generateContributionTooltip', contribution), + }), - relations.tooltip = - relation('generateTooltip'); + data: (contribution) => ({ + annotation: contribution.annotation, + urls: contribution.artist.urls, + }), - if (!empty(contribution.who.urls)) { - relations.artistIcons = - contribution.who.urls - .map(url => relation('linkExternalAsIcon', url)); - } + slots: { + showAnnotation: {type: 'boolean', default: false}, + showExternalLinks: {type: 'boolean', default: false}, + showChronology: {type: 'boolean', default: false}, - return relations; - }, + trimAnnotation: {type: 'boolean', default: false}, - data(contribution) { - return { - what: contribution.what, - }; - }, - - slots: { - showContribution: {type: 'boolean', default: false}, - showIcons: {type: 'boolean', default: false}, preventWrapping: {type: 'boolean', default: true}, + preventTooltip: {type: 'boolean', default: false}, - iconMode: { - validate: v => v.is('inline', 'tooltip'), - default: 'inline' - }, + chronologyKind: {type: 'string'}, }, - generate(data, relations, slots, {html, language}) { - const hasContribution = !!(slots.showContribution && data.what); - const hasExternalIcons = !!(slots.showIcons && relations.artistIcons); - - const parts = ['misc.artistLink']; - const options = {}; - - options.artist = - (hasExternalIcons && slots.iconMode === 'tooltip' - ? relations.textWithTooltip.slots({ - customInteractionCue: true, - - text: - relations.artistLink.slots({ - attributes: {class: 'text-with-tooltip-interaction-cue'}, - }), - - tooltip: - relations.tooltip.slots({ - attributes: - {class: ['icons', 'icons-tooltip']}, - - contentAttributes: - {[html.joinChildren]: ''}, - - content: - relations.artistIcons - .map(icon => - icon.slots({ - context: 'artist', - withText: true, - })), - }), - }) - : relations.artistLink); - - if (hasContribution) { - parts.push('withContribution'); - options.contrib = data.what; - } - - if (hasExternalIcons && slots.iconMode === 'inline') { - parts.push('withExternalLinks'); - options.links = - html.tag('span', {class: ['icons', 'icons-inline']}, - {[html.noEdgeWhitespace]: true}, - language.formatUnitList( - relations.artistIcons - .slice(0, 4) - .map(icon => icon.slot('context', 'artist')))); - } - - const contributionPart = - language.formatString(...parts, options); - - if (!hasContribution && !hasExternalIcons) { - return contributionPart; - } - - return ( - html.tag('span', {class: 'contribution'}, - {[html.noEdgeWhitespace]: true}, - - parts.length > 1 && - slots.preventWrapping && - {class: 'nowrap'}, - - contributionPart)); - }, + generate: (data, relations, slots, {html, language}) => + html.tag('span', {class: 'contribution'}, + {[html.noEdgeWhitespace]: true}, + + slots.preventWrapping && + {class: 'nowrap'}, + + language.encapsulate('misc.artistLink', workingCapsule => { + const workingOptions = {}; + + // 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, + chronologyKind: slots.chronologyKind, + }); + + workingOptions.artist = + (html.isBlank(relations.tooltip) || slots.preventTooltip + ? relations.artistLink + : relations.textWithTooltip.slots({ + customInteractionCue: true, + + text: + relations.artistLink.slots({ + attributes: {class: 'text-with-tooltip-interaction-cue'}, + }), + + tooltip: + relations.tooltip, + })); + + const annotation = + (slots.trimAnnotation + ? data.annotation?.replace(/^edits for wiki(: )?/, '') + : data.annotation); + + if (slots.showAnnotation && annotation) { + workingCapsule += '.withContribution'; + workingOptions.contrib = annotation; + } + + return language.formatString(workingCapsule, workingOptions); + })), }; diff --git a/src/content/dependencies/linkExternal.js b/src/content/dependencies/linkExternal.js index ba2dbf21..45c08a08 100644 --- a/src/content/dependencies/linkExternal.js +++ b/src/content/dependencies/linkExternal.js @@ -6,12 +6,22 @@ export default { data: (url) => ({url}), slots: { + content: { + type: 'html', + 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 // syntax) and a function that is itself a validator. validate: () => isExternalLinkStyle, - default: 'normal', + default: 'platform', }, context: { @@ -19,22 +29,130 @@ export default { default: 'generic', }, + fromContent: { + type: 'boolean', + default: false, + }, + + indicateExternal: { + type: 'boolean', + default: false, + }, + + disableBrowserTooltip: { + type: 'boolean', + default: false, + }, + tab: { validate: v => v.is('default', 'separate'), default: 'default', }, }, - generate: (data, slots, {html, language}) => - html.tag('a', - {href: data.url}, - {class: 'nowrap'}, + generate(data, slots, {html, language}) { + let urlIsValid; + try { + new URL(data.url); + urlIsValid = true; + } catch { + urlIsValid = false; + } + + let formattedLink; + if (urlIsValid) { + formattedLink = + language.formatExternalLink(data.url, { + style: slots.style, + context: slots.context, + }); - slots.tab === 'separate' && - {target: '_blank'}, + // Fall back to platform if nothing matched the desired style. + if (html.isBlank(formattedLink) && slots.style !== 'platform') { + formattedLink = + language.formatExternalLink(data.url, { + style: 'platform', + context: slots.context, + }); + } + } else { + formattedLink = null; + } - language.formatExternalLink(data.url, { - style: slots.style, - context: slots.context, - })), + const linkAttributes = html.attributes({ + class: 'external-link', + }); + + let linkContent; + if (urlIsValid) { + linkAttributes.set('href', data.url); + + if (html.isBlank(slots.content)) { + linkContent = formattedLink; + } else { + linkContent = slots.content; + } + } else { + if (html.isBlank(slots.content)) { + linkContent = + html.tag('i', + language.$('misc.external.invalidURL.annotation')); + } else { + linkContent = + language.$('misc.external.invalidURL', { + link: slots.content, + annotation: + html.tag('i', + language.$('misc.external.invalidURL.annotation')), + }); + } + } + + if (slots.fromContent) { + linkAttributes.add('class', 'from-content'); + } + + if (urlIsValid && slots.indicateExternal) { + linkAttributes.add('class', 'indicate-external'); + + let titleText; + if (slots.disableBrowserTooltip) { + titleText = null; + } else if (slots.tab === 'separate') { + if (html.isBlank(slots.content)) { + titleText = + language.$('misc.external.opensInNewTab.annotation'); + } else { + titleText = + language.$('misc.external.opensInNewTab', { + link: formattedLink, + annotation: + language.$('misc.external.opensInNewTab.annotation'), + }); + } + } else if (!html.isBlank(slots.content)) { + titleText = formattedLink; + } + + if (titleText) { + linkAttributes.set('title', titleText.toString()); + } + } + + if (urlIsValid && slots.tab === 'separate') { + 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/linkExternalAsIcon.js b/src/content/dependencies/linkExternalAsIcon.js deleted file mode 100644 index 3eb355a9..00000000 --- a/src/content/dependencies/linkExternalAsIcon.js +++ /dev/null @@ -1,49 +0,0 @@ -import {isExternalLinkContext} from '#external-links'; - -export default { - extraDependencies: ['html', 'language', 'to'], - - data: (url) => ({url}), - - slots: { - context: { - // This awkward syntax is because the slot descriptor validator can't - // differentiate between a function that returns a validator (the usual - // syntax) and a function that is itself a validator. - validate: () => isExternalLinkContext, - default: 'generic', - }, - - withText: {type: 'boolean'}, - }, - - generate(data, slots, {html, language, to}) { - const format = style => - language.formatExternalLink(data.url, {style, context: slots.context}); - - const normalText = format('normal'); - const compactText = format('compact'); - const iconId = format('icon-id'); - - return html.tag('a', {class: 'icon'}, - {href: data.url}, - - slots.withText && - {class: 'has-text'}, - - [ - html.tag('svg', [ - !slots.withText && - html.tag('title', normalText), - - html.tag('use', { - href: to('shared.staticIcon', iconId), - }), - ]), - - slots.withText && - html.tag('span', {class: 'icon-text'}, - compactText ?? normalText), - ]); - }, -}; 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 bf48c966..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 0af586cd..99f19764 100644 --- a/src/content/dependencies/listArtistsByContributions.js +++ b/src/content/dependencies/listArtistsByContributions.js @@ -1,5 +1,5 @@ import {sortAlphabetically, sortByCount} from '#sort'; -import {empty, filterByCount, filterMultipleArrays, stitchArrays, unique} +import {empty, filterByCount, filterMultipleArrays, stitchArrays} from '#sugar'; export default { @@ -34,30 +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.tracksAsContributor, - ...artist.tracksAsArtist, - ]).length); + countContributions(artist, [ + 'trackArtistContributions', + 'trackContributorContributions', + ])); queryContributionInfo( 'artistsByArtworkContributions', 'countsByArtworkContributions', artist => - artist.tracksAsCoverArtist.length + - artist.albumsAsCoverArtist.length + - artist.albumsAsWallpaperArtist.length + - artist.albumsAsBannerArtist.length); + countContributions(artist, [ + 'albumCoverArtistContributions', + 'albumWallpaperArtistContributions', + 'albumBannerArtistContributions', + 'trackCoverArtistContributions', + ])); if (sprawl.enableFlashesAndGames) { queryContributionInfo( 'artistsByFlashContributions', 'countsByFlashContributions', artist => - artist.flashesAsContributor.length); + countContributions(artist, [ + 'flashContributorContributions', + ])); } return query; diff --git a/src/content/dependencies/listArtistsByDuration.js b/src/content/dependencies/listArtistsByDuration.js index f677d82c..6b2a18a0 100644 --- a/src/content/dependencies/listArtistsByDuration.js +++ b/src/content/dependencies/listArtistsByDuration.js @@ -1,6 +1,5 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; -import {getTotalDuration} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], @@ -16,11 +15,7 @@ export default { artistData.filter(artist => !artist.isAlias)); const durations = - artists.map(artist => - getTotalDuration([ - ...(artist.tracksAsArtist ?? []), - ...(artist.tracksAsContributor ?? []), - ], {originalReleasesOnly: true})); + artists.map(artist => artist.totalDuration); filterByCount(artists, durations); sortByCount(artists, durations, {greatestFirst: true}); diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js index 30884d24..17096cfc 100644 --- a/src/content/dependencies/listArtistsByGroup.js +++ b/src/content/dependencies/listArtistsByGroup.js @@ -1,6 +1,13 @@ import {sortAlphabetically} from '#sort'; -import {empty, filterMultipleArrays, stitchArrays, unique} from '#sugar'; -import {getArtistNumContributions} from '#wiki-data'; + +import { + empty, + filterByCount, + filterMultipleArrays, + stitchArrays, + transposeArrays, + unique, +} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], @@ -15,29 +22,74 @@ export default { sortAlphabetically( sprawl.artistData.filter(artist => !artist.isAlias)); - const groups = + const interestingGroups = sprawl.wikiInfo.divideTrackListsByGroups; - if (empty(groups)) { - return {spec, artists}; + if (empty(interestingGroups)) { + return {spec}; } - const artistGroups = + // We don't actually care about *which* things belong to each group, only + // how many belong to each group. So we'll just compute a list of all the + // (interesting) groups that each of each artists' things belongs to. + const artistThingGroups = artists.map(artist => - unique( - unique([ - ...artist.albumsAsAny, - ...artist.tracksAsAny.map(track => track.album), - ]).flatMap(album => album.groups))) - - const artistsByGroup = - groups.map(group => - artists.filter((artist, index) => artistGroups[index].includes(group))); - - filterMultipleArrays(groups, artistsByGroup, - (group, artists) => !empty(artists)); - - return {spec, groups, artistsByGroup}; + ([ + (unique( + ([ + artist.albumArtistContributions + .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(album => album.groups), + (unique( + ([ + artist.trackArtistContributions + .map(contrib => contrib.thing), + artist.trackContributorContributions + .map(contrib => contrib.thing), + artist.trackCoverArtistContributions + .map(contrib => contrib.thing.thing), + ]).flat() + )).map(track => track.album.groups), + ]).flat() + .map(groups => groups + .filter(group => interestingGroups.includes(group)))); + + const [artistsByGroup, countsByGroup] = + transposeArrays(interestingGroups.map(group => { + const counts = + artistThingGroups + .map(thingGroups => thingGroups + .filter(thingGroups => thingGroups.includes(group)) + .length); + + const filteredArtists = artists.slice(); + + filterByCount(filteredArtists, counts); + + return [filteredArtists, counts]; + })); + + const groups = interestingGroups; + + filterMultipleArrays( + groups, + artistsByGroup, + countsByGroup, + (_group, artists, _counts) => !empty(artists)); + + return { + spec, + groups, + artistsByGroup, + countsByGroup, + }; }, relations(relation, query) { @@ -46,12 +98,6 @@ export default { relations.page = relation('generateListingPage', query.spec); - if (query.artists) { - relations.artistLinks = - query.artists - .map(artist => relation('linkArtist', artist)); - } - if (query.artistsByGroup) { relations.groupLinks = query.groups @@ -69,65 +115,43 @@ export default { data(query) { const data = {}; - if (query.artists) { - data.counts = - query.artists - .map(artist => getArtistNumContributions(artist)); - } - if (query.artistsByGroup) { data.groupDirectories = query.groups .map(group => group.directory); data.countsByGroup = - query.artistsByGroup - .map(artists => artists - .map(artist => getArtistNumContributions(artist))); + query.countsByGroup; } return data; }, - generate(data, relations, {language}) { - return ( - (relations.artistLinksByGroup - ? relations.page.slots({ - type: 'chunks', - - showSkipToSection: true, - chunkIDs: - data.groupDirectories - .map(directory => `contributed-to-${directory}`), - - chunkTitles: - relations.groupLinks.map(groupLink => ({ - group: groupLink, - })), - - chunkRows: - stitchArrays({ - artistLinks: relations.artistLinksByGroup, - counts: data.countsByGroup, - }).map(({artistLinks, counts}) => - stitchArrays({ - link: artistLinks, - count: counts, - }).map(({link, count}) => ({ - artist: link, - contributions: language.countContributions(count, {unit: true}), - }))), - }) - : relations.page.slots({ - type: 'rows', - rows: - stitchArrays({ - link: relations.artistLinks, - count: data.counts, - }).map(({link, count}) => ({ - artist: link, - contributions: language.countContributions(count, {unit: true}), - })), - }))); - }, + generate: (data, relations, {language}) => + relations.page.slots({ + type: 'chunks', + + showSkipToSection: true, + chunkIDs: + data.groupDirectories + .map(directory => `contributed-to-${directory}`), + + chunkTitles: + relations.groupLinks.map(groupLink => ({ + group: groupLink, + })), + + chunkRows: + stitchArrays({ + artistLinks: relations.artistLinksByGroup, + counts: data.countsByGroup, + }).map(({artistLinks, counts}) => + stitchArrays({ + link: artistLinks, + count: counts, + }).map(({link, count}) => ({ + artist: link, + contributions: language.countContributions(count, {unit: true}), + }))), + }), }; diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js index 0f709577..2a8d1b4c 100644 --- a/src/content/dependencies/listArtistsByLatestContribution.js +++ b/src/content/dependencies/listArtistsByLatestContribution.js @@ -83,7 +83,8 @@ export default { }); }; - const getArtists = (thing, key) => thing[key].map(({who}) => who); + const getArtists = (thing, key) => + thing[key].map(({artist}) => artist); const albumsLatestFirst = sortAlbumsTracksChronologically(sprawl.albumData.slice()); const tracksLatestFirst = sortAlbumsTracksChronologically(sprawl.trackData.slice()); @@ -97,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/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js index ab2eca93..79bba441 100644 --- a/src/content/dependencies/listRandomPageLinks.js +++ b/src/content/dependencies/listRandomPageLinks.js @@ -74,20 +74,22 @@ export default { }, generate(data, relations, {html, language}) { + const capsule = language.encapsulate('listingPage.other.randomPages'); + const miscellaneousChunkRows = [ - { + language.encapsulate(capsule, 'chunk.item.randomArtist', capsule => ({ stringsKey: 'randomArtist', mainLink: html.tag('a', {href: '#', 'data-random': 'artist'}, - language.$('listingPage.other.randomPages.chunk.item.randomArtist.mainLink')), + language.$(capsule, 'mainLink')), atLeastTwoContributions: html.tag('a', {href: '#', 'data-random': 'artist-more-than-one-contrib'}, - language.$('listingPage.other.randomPages.chunk.item.randomArtist.atLeastTwoContributions')), - }, + language.$(capsule, 'atLeastTwoContributions')), + })), {stringsKey: 'randomAlbumWholeSite'}, {stringsKey: 'randomTrackWholeSite'}, @@ -104,24 +106,25 @@ export default { content: [ html.tag('p', - language.$('listingPage.other.randomPages.chooseLinkLine', { - fromPart: - (relations.groupLinks - ? language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.dividedByGroups') - : language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.notDividedByGroups')), + language.encapsulate(capsule, 'chooseLinkLine', capsule => + language.$(capsule, { + fromPart: + (relations.groupLinks + ? language.$(capsule, 'fromPart.dividedByGroups') + : language.$(capsule, 'fromPart.notDividedByGroups')), - browserSupportPart: - language.$('listingPage.other.randomPages.chooseLinkLine.browserSupportPart'), - })), + browserSupportPart: + language.$(capsule, 'browserSupportPart'), + }))), html.tag('p', {id: 'data-loading-line'}, - language.$('listingPage.other.randomPages.dataLoadingLine')), + language.$(capsule, 'dataLoadingLine')), html.tag('p', {id: 'data-loaded-line'}, - language.$('listingPage.other.randomPages.dataLoadedLine')), + language.$(capsule, 'dataLoadedLine')), html.tag('p', {id: 'data-error-line'}, - language.$('listingPage.other.randomPages.dataErrorLine')), + language.$(capsule, 'dataErrorLine')), ], showSkipToSection: true, @@ -148,17 +151,18 @@ export default { ... (relations.groupLinks - ? relations.groupLinks.map(() => ({ - randomAlbum: - html.tag('a', - {href: '#', 'data-random': 'album-in-group-dl'}, - language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomAlbum')), - - randomTrack: - html.tag('a', - {href: '#', 'data-random': 'track-in-group-dl'}, - language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomTrack')), - })) + ? relations.groupLinks.map(() => + language.encapsulate(capsule, 'chunk.title.fromGroup.accent', capsule => ({ + randomAlbum: + html.tag('a', + {href: '#', 'data-random': 'album-in-group-dl'}, + language.$(capsule, 'randomAlbum')), + + randomTrack: + html.tag('a', + {href: '#', 'data-random': 'track-in-group-dl'}, + language.$(capsule, 'randomTrack')), + }))) : [null]), ], diff --git a/src/content/dependencies/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 01ce4e2d..dcfaeaf0 100644 --- a/src/content/dependencies/listTracksByDate.js +++ b/src/content/dependencies/listTracksByDate.js @@ -5,48 +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.slice()), - ['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({ @@ -78,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 faae35aa..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,23 +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') { @@ -114,7 +154,31 @@ export default { data.hash = enteredHash ?? null; - return {i: node.i, iEnd: node.iEnd, type: 'link', data}; + 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 @@ -136,14 +200,22 @@ export default { return { content, + error: + sprawl.error, + nodes: sprawl.nodes .map(node => { switch (node.type) { - // Replace link nodes with a stub. It'll be replaced (by position) - // with an item from relations. - case 'link': - return {type: 'link'}; + // Replace internal link nodes with a stub. It'll be replaced + // (by position) with an item from relations. + // + // TODO: This should be where label and hash get passed through, + // rather than in relations... (in which case there's no need to + // handle it specially here, and we can really just return + // data.nodes = sprawl.nodes) + case 'internal-link': + return {type: 'internal-link'}; // Other nodes will get processed in generate. default: @@ -163,13 +235,21 @@ 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 { - links: + textWithTooltip: + relation('generateTextWithTooltip'), + + tooltip: + relation('generateTooltip'), + + internalLinks: nodes - .filter(({type}) => type === 'link') + .filter(({type}) => type === 'internal-link') .map(node => { const {link, thing, value} = node.data; @@ -182,6 +262,19 @@ export default { } }), + externalLinks: + nodes + .filter(({type}) => type === 'external-link') + .map(({data: {href}}) => + relation('linkExternal', href)), + + externalLinksForTooltipNodes: + nodes + .filter(({type}) => type === 'tooltip') + .filter(({data}) => data.link) + .map(({data: {link: href}}) => + relation('linkExternal', href)), + images: nodes .filter(({type}) => type === 'image') @@ -201,24 +294,66 @@ export default { default: false, }, + indicateExternalLinks: { + type: 'boolean', + 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}) { - let linkIndex = 0; + 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; - // This array contains only straight text and link nodes, which are directly - // representable in html (so no further processing is needed on the level of - // individual nodes). 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 = @@ -237,57 +372,185 @@ export default { } = node; if (node.inline) { + let content = + html.tag('img', + src && {src}, + width && {width}, + height && {height}, + style && {style}, + + align && !link && + {class: 'align-' + align}, + + pixelate && + {class: 'pixelate'}); + + if (link) { + content = + html.tag('a', + {href: link}, + {target: '_blank'}, + + align && + {class: 'align-' + align}, + + {title: + language.encapsulate('misc.external.opensInNewTab', capsule => + language.$(capsule, { + link: + language.formatExternalLink(link, { + style: 'platform', + }), + + annotation: + language.$(capsule, 'annotation'), + }).toString())}, + + content); + } + return { - type: 'image', + type: 'processed-image', inline: true, - data: - html.tag('img', - src && {src}, - width && {width}, - height && {height}, - style && {style}, - - pixelate && - {class: 'pixelate'}), + data: content, }; } const image = relations.images[imageIndex++]; + image.setSlots({ + src, + + link: link ?? true, + warnings: warnings ?? null, + thumb: slots.thumb, + }); + + if (width || height) { + image.setSlot('dimensions', [width ?? null, height ?? null]); + } + + image.setSlot('attributes', [ + {class: 'content-image'}, + + pixelate && + {class: 'pixelate'}, + ]); + return { - type: 'image', + type: 'processed-image', inline: false, data: html.tag('div', {class: 'content-image-container'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, - image.slots({ - src, + image), + }; + } - link: link ?? true, - width: width ?? null, - height: height ?? null, - warnings: warnings ?? null, - thumb: slots.thumb, + 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)); - attributes: [ - {class: 'content-image'}, - pixelate && - {class: 'pixelate'}, - ], - })), + return { + type: 'processed-video', + data: content, }; } - case 'link': { - const linkNode = relations.links[linkIndex++]; - if (linkNode.type === 'text') { - return {type: 'text', data: linkNode.data}; + 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} = linkNode; + // 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 @@ -301,7 +564,7 @@ export default { try { link.getSlotDescription('preferShortName'); hasPreferShortNameSlot = true; - } catch (error) { + } catch { hasPreferShortNameSlot = false; } @@ -314,7 +577,7 @@ export default { try { link.getSlotDescription('tooltipStyle'); hasTooltipStyleSlot = true; - } catch (error) { + } catch { hasTooltipStyleSlot = false; } @@ -322,7 +585,93 @@ export default { link.setSlot('tooltipStyle', 'none'); } - return {type: 'link', data: link}; + 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}; + } + + case 'external-link': { + 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, + tab: 'separate', + style: 'platform', + }); + } + + 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': { @@ -341,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: @@ -358,7 +714,10 @@ export default { // access to its slots. if (slots.mode === 'single-link') { - const link = contentFromNodes.find(node => node.type === 'link'); + const link = + contentFromNodes.find(node => + node.type === 'processed-internal-link' || + node.type === 'processed-external-link'); if (!link) { return html.blank(); @@ -385,13 +744,10 @@ export default { return getTextNodeContents(node, index); } - const attributes = html.attributes({ - class: 'INSERT-NON-TEXT', - 'data-type': node.type, - }); + let attributes = `class="INSERT-NON-TEXT" data-type="${node.type}"`; - if (node.type === 'image') { - attributes.set('data-inline', node.inline); + if (node.type === 'processed-image' && node.inline) { + attributes += ` data-inline`; } return `<span ${attributes}>${index}</span>`; @@ -422,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') === '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]; @@ -441,7 +801,11 @@ export default { addText(markedOutput.slice(parseFrom)); } - return html.tags(tags, {[html.joinChildren]: ''}); + return ( + html.tags(tags, { + [html.joinChildren]: '', + [html.onlyIfContent]: true, + })); }; if (slots.mode === 'inline') { @@ -466,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(/(?<!^ *-.*|^>.*|^ .*\n*| $|<br>$)\n+(?! |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */ + .replace(/(?<!^ *(?:-|\d+\.).*|^>.*|^ .*\n*| $|<br>$)\n+(?! |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */ // Expand line breaks which are at the end of a list. - .replace(/(?<=^ *-.*)\n+(?!^ *-)/gm, '\n\n') + .replace(/(?<=^ *(?:-|\d+\.).*)\n+(?!^ *(?:-|\d+\.))/gm, '\n\n') // Expand line breaks which are at the end of a quote. .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n'); @@ -500,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'); }, }); |