diff options
Diffstat (limited to 'src/content/dependencies')
293 files changed, 5866 insertions, 4172 deletions
diff --git a/src/content/dependencies/generateAbsoluteDatetimestamp.js b/src/content/dependencies/generateAbsoluteDatetimestamp.js index 930b6f13..d006374a 100644 --- a/src/content/dependencies/generateAbsoluteDatetimestamp.js +++ b/src/content/dependencies/generateAbsoluteDatetimestamp.js @@ -1,15 +1,12 @@ export default { - contentDependencies: [ - 'generateDatetimestampTemplate', - 'generateTooltip', - ], + data: (date, contextDate) => ({ + date, - extraDependencies: ['html', 'language'], - - data: (date) => - ({date}), + contextDate: + contextDate ?? null, + }), - relations: (relation) => ({ + relations: (relation, _date, _contextDate) => ({ template: relation('generateDatetimestampTemplate'), @@ -19,35 +16,74 @@ export default { slots: { style: { - validate: v => v.is('full', 'year'), + validate: v => v.is(...[ + 'full', + 'year', + 'minimal-difference', + 'year-difference', + ]), default: 'full', }, - - // Only has an effect for 'year' style. - tooltip: { - type: 'boolean', - default: false, - }, }, - generate: (data, relations, slots, {language}) => - relations.template.slots({ - mainContent: - (slots.style === 'full' - ? language.formatDate(data.date) - : slots.style === 'year' - ? data.date.getFullYear().toString() - : null), - - tooltip: - slots.tooltip && - slots.style === 'year' && - relations.tooltip.slots({ - content: - language.formatDate(data.date), - }), - - datetime: - data.date.toISOString(), - }), + generate(data, relations, slots, {html, language}) { + if (!data.date) { + return html.blank(); + } + + relations.template.setSlots({ + tooltip: relations.tooltip, + datetime: data.date.toISOString(), + }); + + let label = null; + let tooltip = null; + + switch (slots.style) { + case 'full': { + label = language.formatDate(data.date); + break; + } + + case 'year': { + label = language.formatYear(data.date); + tooltip = language.formatDate(data.date); + break; + } + + case 'minimal-difference': { + if (data.date.toDateString() === data.contextDate?.toDateString()) { + return html.blank(); + } + + if (data.date.getFullYear() === data.contextDate?.getFullYear()) { + label = language.formatMonthDay(data.date); + tooltip = language.formatDate(data.date); + } else { + label = language.formatYear(data.date); + tooltip = language.formatDate(data.date); + } + + break; + } + + case 'year-difference': { + if (data.date.toDateString() === data.contextDate?.toDateString()) { + return html.blank(); + } + + if (data.date.getFullYear() === data.contextDate?.getFullYear()) { + label = language.formatDate(data.date); + } else { + label = language.formatYear(data.date); + tooltip = language.formatDate(data.date); + } + } + } + + relations.template.setSlot('mainContent', label); + relations.tooltip.setSlot('content', tooltip); + + return relations.template; + }, }; diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js index 68120b23..699c5f86 100644 --- a/src/content/dependencies/generateAdditionalFilesList.js +++ b/src/content/dependencies/generateAdditionalFilesList.js @@ -1,26 +1,19 @@ -import {stitchArrays} from '#sugar'; - export default { - extraDependencies: ['html'], + relations: (relation, additionalFiles) => ({ + chunks: + additionalFiles + .map(file => relation('generateAdditionalFilesListChunk', file)), + }), slots: { - chunks: { - validate: v => v.strictArrayOf(v.isHTML), - }, - - chunkItems: { - validate: v => v.strictArrayOf(v.isHTML), - }, + showFileSizes: {type: 'boolean', default: true}, }, - generate: (slots, {html}) => + generate: (relations, slots, {html}) => html.tag('ul', {class: 'additional-files-list'}, {[html.onlyIfContent]: true}, - stitchArrays({ - chunk: slots.chunks, - items: slots.chunkItems, - }).map(({chunk, items}) => - chunk.clone() - .slot('items', items))), + relations.chunks.map(chunk => chunk.slots({ + showFileSizes: slots.showFileSizes, + }))), }; diff --git a/src/content/dependencies/generateAdditionalFilesListChunk.js b/src/content/dependencies/generateAdditionalFilesListChunk.js index 507b2329..466a5d8d 100644 --- a/src/content/dependencies/generateAdditionalFilesListChunk.js +++ b/src/content/dependencies/generateAdditionalFilesListChunk.js @@ -1,46 +1,78 @@ +import {stitchArrays} from '#sugar'; + export default { - extraDependencies: ['html', 'language'], + relations: (relation, file) => ({ + description: + relation('transformContent', file.description), - slots: { - title: { - type: 'html', - mutable: false, - }, + links: + file.filenames + .map(filename => relation('linkAdditionalFile', file, filename)), + }), - description: { - type: 'html', - mutable: false, - }, + data: (file) => ({ + title: + file.title, + + paths: + file.paths, + }), - items: { - validate: v => v.looseArrayOf(v.isHTML), + slots: { + showFileSizes: { + type: 'boolean', }, }, - generate: (slots, {html, language}) => - language.encapsulate('releaseInfo.additionalFiles.entry', capsule => + generate: (data, relations, slots, {getSizeOfMediaFile, html, language, urls}) => + language.encapsulate('releaseInfo.additionalFiles', capsule => html.tag('li', html.tag('details', - html.isBlank(slots.items) && + html.isBlank(relations.links) && {open: true}, [ html.tag('summary', html.tag('span', - language.$(capsule, { + language.$(capsule, 'entry', { title: - html.tag('b', slots.title), + html.tag('b', data.title), }))), html.tag('ul', [ html.tag('li', {class: 'entry-description'}, {[html.onlyIfContent]: true}, - slots.description), - (html.isBlank(slots.items) + relations.description.slot('mode', 'inline')), + + (html.isBlank(relations.links) ? html.tag('li', - language.$(capsule, 'noFilesAvailable')) - : slots.items), + language.$(capsule, 'entry.noFilesAvailable')) + + : stitchArrays({ + link: relations.links, + path: data.paths, + }).map(({link, path}) => + html.tag('li', + language.encapsulate(capsule, 'file', workingCapsule => { + const workingOptions = {file: link}; + + if (slots.showFileSizes) { + const fileSize = + getSizeOfMediaFile( + urls + .from('media.root') + .to(...path)); + + if (fileSize) { + workingCapsule += '.withSize'; + workingOptions.size = + language.formatFileSize(fileSize); + } + } + + return language.$(workingCapsule, workingOptions); + })))), ]), ]))), }; diff --git a/src/content/dependencies/generateAdditionalFilesListChunkItem.js b/src/content/dependencies/generateAdditionalFilesListChunkItem.js deleted file mode 100644 index c37d6bb2..00000000 --- a/src/content/dependencies/generateAdditionalFilesListChunkItem.js +++ /dev/null @@ -1,30 +0,0 @@ -export default { - extraDependencies: ['html', 'language'], - - slots: { - fileLink: { - type: 'html', - mutable: false, - }, - - fileSize: { - validate: v => v.isWholeNumber, - }, - }, - - generate(slots, {html, language}) { - const itemParts = ['releaseInfo.additionalFiles.file']; - const itemOptions = {file: slots.fileLink}; - - if (slots.fileSize) { - itemParts.push('withSize'); - itemOptions.size = language.formatFileSize(slots.fileSize); - } - - const li = - html.tag('li', - language.$(...itemParts, itemOptions)); - - return li; - }, -}; diff --git a/src/content/dependencies/generateAdditionalNamesBox.js b/src/content/dependencies/generateAdditionalNamesBox.js index b7392dfd..6bd1ab42 100644 --- a/src/content/dependencies/generateAdditionalNamesBox.js +++ b/src/content/dependencies/generateAdditionalNamesBox.js @@ -1,18 +1,25 @@ export default { - contentDependencies: ['generateAdditionalNamesBoxItem'], - extraDependencies: ['html', 'language'], - relations: (relation, additionalNames) => ({ items: additionalNames .map(entry => relation('generateAdditionalNamesBoxItem', entry)), }), - generate: (relations, {html, language}) => + slots: { + alwaysVisible: { + type: 'boolean', + default: false, + }, + }, + + generate: (relations, slots, {html, language}) => html.tag('div', {id: 'additional-names-box'}, {class: 'drop'}, {[html.onlyIfContent]: true}, + slots.alwaysVisible && + {class: 'always-visible'}, + [ html.tag('p', {[html.onlyIfSiblings]: true}, diff --git a/src/content/dependencies/generateAdditionalNamesBoxItem.js b/src/content/dependencies/generateAdditionalNamesBoxItem.js index e3e59a34..a39711c1 100644 --- a/src/content/dependencies/generateAdditionalNamesBoxItem.js +++ b/src/content/dependencies/generateAdditionalNamesBoxItem.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['transformContent'], - extraDependencies: ['html', 'language'], - relations: (relation, entry) => ({ nameContent: relation('transformContent', entry.name), diff --git a/src/content/dependencies/generateAlbumAdditionalFilesList.js b/src/content/dependencies/generateAlbumAdditionalFilesList.js deleted file mode 100644 index ad17206f..00000000 --- a/src/content/dependencies/generateAlbumAdditionalFilesList.js +++ /dev/null @@ -1,96 +0,0 @@ -import {stitchArrays} from '#sugar'; - -export default { - contentDependencies: [ - 'generateAdditionalFilesList', - 'generateAdditionalFilesListChunk', - 'generateAdditionalFilesListChunkItem', - 'linkAlbumAdditionalFile', - 'transformContent', - ], - - extraDependencies: ['getSizeOfMediaFile', 'html', 'urls'], - - relations: (relation, album, additionalFiles) => ({ - list: - relation('generateAdditionalFilesList', additionalFiles), - - chunks: - additionalFiles - .map(() => relation('generateAdditionalFilesListChunk')), - - chunkDescriptions: - additionalFiles - .map(({description}) => - (description - ? relation('transformContent', description) - : null)), - - chunkItems: - additionalFiles - .map(({files}) => - (files ?? []) - .map(() => relation('generateAdditionalFilesListChunkItem'))), - - chunkItemFileLinks: - additionalFiles - .map(({files}) => - (files ?? []) - .map(file => relation('linkAlbumAdditionalFile', album, file))), - }), - - data: (album, additionalFiles) => ({ - albumDirectory: album.directory, - - chunkTitles: - additionalFiles - .map(({title}) => title), - - chunkItemLocations: - additionalFiles - .map(({files}) => files ?? []), - }), - - slots: { - showFileSizes: {type: 'boolean', default: true}, - }, - - generate: (data, relations, slots, {getSizeOfMediaFile, urls}) => - relations.list.slots({ - chunks: - stitchArrays({ - chunk: relations.chunks, - description: relations.chunkDescriptions, - title: data.chunkTitles, - }).map(({chunk, title, description}) => - chunk.slots({ - title, - description: - (description - ? description.slot('mode', 'inline') - : null), - })), - - chunkItems: - stitchArrays({ - items: relations.chunkItems, - fileLinks: relations.chunkItemFileLinks, - locations: data.chunkItemLocations, - }).map(({items, fileLinks, locations}) => - stitchArrays({ - item: items, - fileLink: fileLinks, - location: locations, - }).map(({item, fileLink, location}) => - item.slots({ - fileLink: fileLink, - fileSize: - (slots.showFileSizes - ? getSizeOfMediaFile( - urls - .from('media.root') - .to('media.albumAdditionalFile', data.albumDirectory, location)) - : 0), - }))), - }), -}; diff --git a/src/content/dependencies/generateAlbumArtInfoBox.js b/src/content/dependencies/generateAlbumArtInfoBox.js new file mode 100644 index 00000000..5491192a --- /dev/null +++ b/src/content/dependencies/generateAlbumArtInfoBox.js @@ -0,0 +1,36 @@ +export default { + 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..5346e56b --- /dev/null +++ b/src/content/dependencies/generateAlbumArtworkColumn.js @@ -0,0 +1,51 @@ +export default { + query: (album) => ({ + nonAttachingArtworkIndex: + (album.hasCoverArt + ? album.coverArtworks.findIndex((artwork, index) => + index > 1 && + !artwork.attachAbove) + : null), + }), + + relations: (relation, query, album) => ({ + firstCovers: + (album.hasCoverArt && query.nonAttachingArtworkIndex >= 1 + ? album.coverArtworks + .slice(0, query.nonAttachingArtworkIndex) + .map(artwork => relation('generateCoverArtwork', artwork)) + + : album.hasCoverArt + ? album.coverArtworks + .map(artwork => relation('generateCoverArtwork', artwork)) + + : []), + + albumArtInfoBox: + relation('generateAlbumArtInfoBox', album), + + restCovers: + (album.hasCoverArt && query.nonAttachingArtworkIndex >= 1 + ? album.coverArtworks + .slice(query.nonAttachingArtworkIndex) + .map(artwork => relation('generateCoverArtwork', artwork)) + + : []), + }), + + generate(relations, {html}) { + for (const cover of [...relations.firstCovers, ...relations.restCovers]) { + cover.setSlots({ + showOriginDetails: true, + showArtTagDetails: true, + showReferenceDetails: true, + }); + } + + return html.tags([ + relations.firstCovers, + relations.albumArtInfoBox, + relations.restCovers, + ]); + }, +}; diff --git a/src/content/dependencies/generateAlbumBanner.js b/src/content/dependencies/generateAlbumBanner.js index 3cc141bc..dce258de 100644 --- a/src/content/dependencies/generateAlbumBanner.js +++ b/src/content/dependencies/generateAlbumBanner.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateBanner'], - extraDependencies: ['html', 'language'], - relations(relation, album) { if (!album.hasBannerArt) { return {}; diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index f5df7c3d..4c203877 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -1,23 +1,6 @@ import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateAlbumCommentarySidebar', - 'generateAlbumCoverArtwork', - 'generateAlbumNavAccent', - 'generateAlbumSecondaryNav', - 'generateAlbumStyleRules', - 'generateCommentaryEntry', - 'generateContentHeading', - 'generateTrackCoverArtwork', - 'generatePageLayout', - 'linkAlbum', - 'linkExternal', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - query(album) { const query = {}; @@ -45,8 +28,8 @@ export default { relations.sidebar = relation('generateAlbumCommentarySidebar', album); - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); + relations.albumStyleTags = + relation('generateAlbumStyleTags', album, null); relations.albumLink = relation('linkAlbum', album); @@ -66,7 +49,7 @@ export default { if (album.hasCoverArt) { relations.albumCommentaryCover = - relation('generateAlbumCoverArtwork', album); + relation('generateCoverArtwork', album.coverArtworks[0]); } relations.albumCommentaryEntries = @@ -91,7 +74,7 @@ export default { query.tracksWithCommentary .map(track => (track.hasUniqueCoverArt - ? relation('generateTrackCoverArtwork', track) + ? relation('generateCoverArtwork', track.trackArtworks[0]) : null)); relations.trackCommentaryEntries = @@ -152,7 +135,7 @@ export default { headingMode: 'sticky', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['long-content'], mainContent: [ @@ -267,7 +250,10 @@ export default { }), })), - cover?.slots({mode: 'commentary'}), + cover?.slots({ + mode: 'commentary', + color: true, + }), trackDate && trackDate !== data.date && diff --git a/src/content/dependencies/generateAlbumCommentarySidebar.js b/src/content/dependencies/generateAlbumCommentarySidebar.js index 9ecec66d..4863f059 100644 --- a/src/content/dependencies/generateAlbumCommentarySidebar.js +++ b/src/content/dependencies/generateAlbumCommentarySidebar.js @@ -1,15 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateAlbumSidebarTrackSection', - 'generatePageSidebar', - 'generatePageSidebarBox', - 'linkAlbum', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, album) => ({ sidebar: relation('generatePageSidebar'), diff --git a/src/content/dependencies/generateAlbumCoverArtwork.js b/src/content/dependencies/generateAlbumCoverArtwork.js deleted file mode 100644 index ff7d2b85..00000000 --- a/src/content/dependencies/generateAlbumCoverArtwork.js +++ /dev/null @@ -1,100 +0,0 @@ -export default { - contentDependencies: [ - 'generateCoverArtwork', - 'generateCoverArtworkArtTagDetails', - 'generateCoverArtworkArtistDetails', - 'generateCoverArtworkReferenceDetails', - 'image', - 'linkAlbumReferencedArtworks', - 'linkAlbumReferencingArtworks', - ], - - extraDependencies: ['html', 'language'], - - relations: (relation, album) => ({ - coverArtwork: - relation('generateCoverArtwork'), - - image: - relation('image'), - - artTagDetails: - relation('generateCoverArtworkArtTagDetails', album.artTags), - - artistDetails: - relation('generateCoverArtworkArtistDetails', album.coverArtistContribs), - - referenceDetails: - relation('generateCoverArtworkReferenceDetails', - album.referencedArtworks, - album.referencedByArtworks), - - referencedArtworksLink: - relation('linkAlbumReferencedArtworks', album), - - referencingArtworksLink: - relation('linkAlbumReferencingArtworks', album), - }), - - data: (album) => ({ - path: - ['media.albumCover', album.directory, album.coverArtFileExtension], - - color: - album.color, - - dimensions: - album.coverArtDimensions, - - warnings: - album.artTags - .filter(tag => tag.isContentWarning) - .map(tag => tag.name), - }), - - slots: { - mode: {type: 'string'}, - - details: { - validate: v => v.is('tags', 'artists'), - default: 'tags', - }, - - showReferenceLinks: { - type: 'boolean', - default: false, - }, - }, - - generate: (data, relations, slots, {language}) => - relations.coverArtwork.slots({ - mode: slots.mode, - - image: - relations.image.slots({ - path: data.path, - color: data.color, - alt: language.$('misc.alt.albumCover'), - }), - - dimensions: data.dimensions, - warnings: data.warnings, - - details: [ - slots.details === 'tags' && - relations.artTagDetails, - - slots.details === 'artists' && - relations.artistDetails, - - slots.showReferenceLinks && - relations.referenceDetails.slots({ - referencedLink: - relations.referencedArtworksLink, - - referencingLink: - relations.referencingArtworksLink, - }), - ], - }), -}; diff --git a/src/content/dependencies/generateAlbumGalleryAlbumGrid.js b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js new file mode 100644 index 00000000..f9cd027e --- /dev/null +++ b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js @@ -0,0 +1,82 @@ +import {stitchArrays} from '#sugar'; + +export default { + 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/generateAlbumGalleryCoverArtistsLine.js b/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js index 7dcdf6de..0322e227 100644 --- a/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js +++ b/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkArtistGallery'], - extraDependencies: ['html', 'language'], - relations(relation, coverArtists) { return { coverArtistLinks: diff --git a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js index ad99cb87..5932514e 100644 --- a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js +++ b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - generate: ({html, language}) => html.tag('p', {class: 'quick-info'}, language.$('albumGalleryPage.noTrackArtworksLine')), diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index b48d92af..85b0fb74 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -1,166 +1,86 @@ -import {compareArrays, stitchArrays} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; +import {getKebabCase} from '#wiki-data'; export default { - contentDependencies: [ - 'generateAlbumGalleryCoverArtistsLine', - 'generateAlbumGalleryNoTrackArtworksLine', - 'generateAlbumGalleryStatsLine', - 'generateAlbumNavAccent', - 'generateAlbumSecondaryNav', - 'generateAlbumStyleRules', - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - query(album) { const query = {}; - const tracksWithUniqueCoverArt = + const trackArtworkLabels = album.tracks - .filter(track => track.hasUniqueCoverArt); - - // Don't display "all artwork by..." for albums where there's - // only one unique artwork in the first place. - if (tracksWithUniqueCoverArt.length > 1) { - const allCoverArtistArrays = - tracksWithUniqueCoverArt - .map(track => track.coverArtistContribs) - .map(contribs => contribs.map(contrib => contrib.artist)); - - const allSameCoverArtists = - allCoverArtistArrays - .slice(1) - .every(artists => compareArrays(artists, allCoverArtistArrays[0])); - - if (allSameCoverArtists) { - query.coverArtistsForAllTracks = - allCoverArtistArrays[0]; - } - } + .map(track => track.trackArtworks + .map(artwork => artwork.label)); + + const recurranceThreshold = 2; + + // This list may include null, if some artworks are not labelled! + // That's expected. + query.recurringTrackArtworkLabels = + unique(trackArtworkLabels.flat()) + .filter(label => + trackArtworkLabels + .filter(labels => labels.includes(label)) + .length >= + (label === null + ? 1 + : recurranceThreshold)); return query; }, - relations(relation, query, album) { - const relations = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); - - relations.albumLink = - relation('linkAlbum', album); - - relations.albumNavAccent = - relation('generateAlbumNavAccent', album, null); - - relations.secondaryNav = - relation('generateAlbumSecondaryNav', album); + relations: (relation, query, album) => ({ + layout: + relation('generatePageLayout'), - relations.statsLine = - relation('generateAlbumGalleryStatsLine', album); + albumStyleTags: + relation('generateAlbumStyleTags', album, null), - 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')), - - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? relation('image', track.artTags) - : relation('image'))), - ]; + albumNavAccent: + relation('generateAlbumNavAccent', album, null), - return relations; - }, - - data(query, album) { - const data = {}; - - data.name = album.name; - data.color = album.color; + secondaryNav: + relation('generateAlbumSecondaryNav', album), - data.names = [ - album.name, - ...album.tracks.map(track => track.name), - ]; + statsLine: + relation('generateAlbumGalleryStatsLine', album), - data.coverArtists = [ - (album.hasCoverArt - ? album.coverArtistContribs.map(({artist}) => artist.name) + noTrackArtworksLine: + (album.tracks.every(track => !track.hasUniqueCoverArt) + ? relation('generateAlbumGalleryNoTrackArtworksLine') : null), - ... - album.tracks.map(track => { - if (query.coverArtistsForAllTracks) { - return null; - } + setSwitcher: + relation('generateIntrapageDotSwitcher'), - if (track.hasUniqueCoverArt) { - return track.coverArtistContribs.map(({artist}) => artist.name); - } + albumGrid: + relation('generateAlbumGalleryAlbumGrid', album), - return null; - }), - ]; + 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')), - data.dimensions = [ - (album.hasCoverArt - ? album.coverArtDimensions - : null), - - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? track.coverArtDimensions - : null)), - ]; + name: + album.name, - return data; - }, + color: + album.color, + }), - generate: (data, relations, {language}) => + generate: (data, relations, {html, language}) => language.encapsulate('albumGalleryPage', pageCapsule => relations.layout.slots({ title: @@ -171,39 +91,44 @@ export default { headingMode: 'static', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['top-index'], mainContent: [ relations.statsLine, - relations.coverArtistsLine, + + relations.albumGrid, + relations.noTrackArtworksLine, - relations.coverGrid - .slots({ - links: relations.links, - names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - name: data.names, - }).map(({image, path, dimensions, name}) => - image.slots({ - path, - dimensions, - missingSourceContent: - language.$('misc.albumGalleryGrid.noCoverArt', {name}), - })), - info: - data.coverArtists.map(names => - (names === null - ? null - : language.$('misc.coverGrid.details.coverArtists', { - artists: language.formatUnitList(names), - }))), - }), + data.trackGridLabels.some(value => value !== null) && + html.tag('p', {class: 'gallery-set-switcher'}, + language.encapsulate(pageCapsule, 'setSwitcher', switcherCapsule => + language.$(switcherCapsule, { + sets: + relations.setSwitcher.slots({ + initialOptionIndex: 0, + + titles: + data.trackGridLabels.map(label => + label ?? + language.$(switcherCapsule, 'unlabeledSet')), + + targetIDs: + data.trackGridIDs, + }), + }))), + + stitchArrays({ + grid: relations.trackGrids, + id: data.trackGridIDs, + }).map(({grid, id}, index) => + grid.slots({ + attributes: [ + {id}, + index >= 1 && {style: 'display: none'}, + ], + })), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateAlbumGalleryStatsLine.js b/src/content/dependencies/generateAlbumGalleryStatsLine.js index 75bffb36..75341937 100644 --- a/src/content/dependencies/generateAlbumGalleryStatsLine.js +++ b/src/content/dependencies/generateAlbumGalleryStatsLine.js @@ -1,38 +1,56 @@ import {getTotalDuration} from '#wiki-data'; export default { - extraDependencies: ['html', 'language'], - - data(album) { - return { - name: album.name, - date: album.date, - duration: getTotalDuration(album.tracks), - numTracks: album.tracks.length, - }; - }, - - generate(data, {html, language}) { - const parts = ['albumGalleryPage.statsLine']; - const options = {}; - - options.tracks = - html.tag('b', - language.countTracks(data.numTracks, {unit: true})); - - options.duration = - html.tag('b', - language.formatDuration(data.duration, {unit: true})); - - if (data.date) { - parts.push('withDate'); - options.date = - html.tag('b', - language.formatDate(data.date)); - } - - return ( - html.tag('p', {class: 'quick-info'}, - language.formatString(...parts, options))); - }, + data: (album) => ({ + date: + album.date, + + hideDuration: + album.hideDuration, + + duration: + (album.hideDuration + ? null + : getTotalDuration(album.tracks)), + + tracks: + (album.hideDuration + ? null + : album.tracks.length), + }), + + generate: (data, {html, language}) => + html.tag('p', {class: 'quick-info'}, + {[html.onlyIfContent]: true}, + + language.encapsulate('albumGalleryPage.statsLine', workingCapsule => { + const workingOptions = {}; + + if (data.hideDuration && !data.date) { + return html.blank(); + } + + if (!data.hideDuration) { + workingOptions.tracks = + html.tag('b', + language.countTracks(data.tracks, {unit: true})); + + workingOptions.duration = + html.tag('b', + language.formatDuration(data.duration, {unit: true})); + } + + if (data.date) { + workingCapsule += '.withDate'; + workingOptions.date = + html.tag('b', + language.formatDate(data.date)); + } + + if (data.hideDuration) { + workingCapsule += '.noDuration'; + } + + return language.$(workingCapsule, workingOptions); + })), }; diff --git a/src/content/dependencies/generateAlbumGalleryTrackGrid.js b/src/content/dependencies/generateAlbumGalleryTrackGrid.js new file mode 100644 index 00000000..a50448c6 --- /dev/null +++ b/src/content/dependencies/generateAlbumGalleryTrackGrid.js @@ -0,0 +1,118 @@ +import {compareArrays, stitchArrays} from '#sugar'; + +export default { + 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)), + + allWarnings: + query.artworks.flatMap(artwork => artwork?.contentWarnings), + }), + + 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), + })), + + revealAllWarnings: + data.allWarnings, + }), + ]), +}; diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index aae56637..a27074ff 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -1,33 +1,12 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateAlbumAdditionalFilesList', - 'generateAlbumBanner', - 'generateAlbumCoverArtwork', - 'generateAlbumNavAccent', - 'generateAlbumReleaseInfo', - 'generateAlbumSecondaryNav', - 'generateAlbumSidebar', - 'generateAlbumSocialEmbed', - 'generateAlbumStyleRules', - 'generateAlbumTrackList', - 'generateCommentaryEntry', - 'generateContentHeading', - 'generatePageLayout', - 'linkAlbumCommentary', - 'linkAlbumGallery', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, album) => ({ layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), socialEmbed: relation('generateAlbumSocialEmbed', album), @@ -44,10 +23,8 @@ export default { additionalNamesBox: relation('generateAdditionalNamesBox', album.additionalNames), - cover: - (album.hasCoverArt - ? relation('generateAlbumCoverArtwork', album) - : null), + artworkColumn: + relation('generateAlbumArtworkColumn', album), banner: (album.hasBannerArt @@ -66,25 +43,30 @@ export default { : null), commentaryLink: - ([album, ...album.tracks].some(({commentary}) => !empty(commentary)) + (album.tracks.some(track => !empty(track.commentary)) ? relation('linkAlbumCommentary', album) : null), + readCommentaryLine: + relation('generateReadCommentaryLine', album), + trackList: relation('generateAlbumTrackList', album), additionalFilesList: - relation('generateAlbumAdditionalFilesList', - album, - album.additionalFiles), + relation('generateAdditionalFilesList', album.additionalFiles), + + commentaryContentHeading: + relation('generateCommentaryContentHeading', album), artistCommentaryEntries: album.commentary .map(entry => relation('generateCommentaryEntry', entry)), - creditSourceEntries: - album.creditSources - .map(entry => relation('generateCommentaryEntry', entry)), + creditingSourcesSection: + relation('generateCollapsedContentEntrySection', + album.creditingSources, + album), }), data: (album) => ({ @@ -108,16 +90,12 @@ export default { color: data.color, headingMode: 'sticky', - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, additionalNames: relations.additionalNamesBox, - cover: - (relations.cover - ? relations.cover.slots({ - showReferenceLinks: true, - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ relations.releaseInfo, @@ -164,12 +142,16 @@ export default { : html.blank()), - !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + !relations.commentaryLink && + !html.isBlank(relations.artistCommentaryEntries) && + relations.readCommentaryLine, + + !html.isBlank(relations.creditingSourcesSection) && + language.encapsulate(capsule, 'readCreditingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#crediting-sources'}, language.$(capsule, 'link')), })), ])), @@ -178,14 +160,14 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, - language.encapsulate('releaseInfo', capsule => [ - language.$(capsule, 'addedToWiki', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.dateAddedToWiki), - }), - ])), + language.$('releaseInfo.addedToWiki', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.dateAddedToWiki), + })), + + !html.isBlank(relations.artistCommentaryEntries) && + html.tag('hr', {class: 'main-separator'}), language.encapsulate('releaseInfo.additionalFiles', capsule => html.tags([ @@ -199,24 +181,14 @@ export default { ])), html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), - }), - + relations.commentaryContentHeading, relations.artistCommentaryEntries, ]), - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), - }), - - relations.creditSourceEntries, - ]), + relations.creditingSourcesSection.slots({ + id: 'crediting-sources', + string: 'misc.creditingSources', + }), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index 432c5f3d..237120f3 100644 --- a/src/content/dependencies/generateAlbumNavAccent.js +++ b/src/content/dependencies/generateAlbumNavAccent.js @@ -1,17 +1,6 @@ import {atOffset, empty} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkTrack', - 'linkAlbumCommentary', - 'linkAlbumGallery', - ], - - extraDependencies: ['html', 'language'], - query(album, track) { const query = {}; @@ -64,9 +53,8 @@ export default { hasMultipleTracks: album.tracks.length > 1, - commentaryPageIsStub: - [album, ...album.tracks] - .every(({commentary}) => empty(commentary)), + hasSubstantialCommentaryPage: + album.tracks.some(track => !empty(track.commentary)), galleryIsStub: album.tracks.every(t => !t.hasUniqueCoverArt), @@ -97,14 +85,16 @@ export default { relations.nextLink.slot('link', relations.nextTrackLink); const galleryLink = - (!data.galleryIsStub || slots.currentExtra === 'gallery') && + (!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') && + (data.hasSubstantialCommentaryPage || + slots.currentExtra === 'commentary') && relations.albumCommentaryLink.slots({ attributes: {class: slots.currentExtra === 'commentary' && 'current'}, content: language.$(albumNavCapsule, 'commentary'), diff --git a/src/content/dependencies/generateAlbumReferencedArtworksPage.js b/src/content/dependencies/generateAlbumReferencedArtworksPage.js index 3f3d77b3..e4022f0d 100644 --- a/src/content/dependencies/generateAlbumReferencedArtworksPage.js +++ b/src/content/dependencies/generateAlbumReferencedArtworksPage.js @@ -1,37 +1,21 @@ export default { - contentDependencies: [ - 'generateAlbumCoverArtwork', - 'generateAlbumStyleRules', - 'generateBackToAlbumLink', - 'generateReferencedArtworksPage', - 'linkAlbum', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, album) => ({ page: - relation('generateReferencedArtworksPage', album.referencedArtworks), + relation('generateReferencedArtworksPage', album.coverArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), albumLink: relation('linkAlbum', album), backToAlbumLink: relation('generateBackToAlbumLink', album), - - cover: - relation('generateAlbumCoverArtwork', album), }), data: (album) => ({ name: album.name, - - color: - album.color, }), generate: (data, relations, {html, language}) => @@ -42,10 +26,7 @@ export default { data.name, }), - color: data.color, - styleRules: [relations.albumStyleRules], - - cover: relations.cover, + styleTags: relations.albumStyleTags, navLinks: [ {auto: 'home'}, diff --git a/src/content/dependencies/generateAlbumReferencingArtworksPage.js b/src/content/dependencies/generateAlbumReferencingArtworksPage.js index 8f2349f9..0dc1bf15 100644 --- a/src/content/dependencies/generateAlbumReferencingArtworksPage.js +++ b/src/content/dependencies/generateAlbumReferencingArtworksPage.js @@ -1,37 +1,21 @@ export default { - contentDependencies: [ - 'generateAlbumCoverArtwork', - 'generateAlbumStyleRules', - 'generateBackToAlbumLink', - 'generateReferencingArtworksPage', - 'linkAlbum', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, album) => ({ page: - relation('generateReferencingArtworksPage', album.referencedByArtworks), + relation('generateReferencingArtworksPage', album.coverArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), albumLink: relation('linkAlbum', album), backToAlbumLink: relation('generateBackToAlbumLink', album), - - cover: - relation('generateAlbumCoverArtwork', album), }), data: (album) => ({ name: album.name, - - color: - album.color, }), generate: (data, relations, {html, language}) => @@ -42,10 +26,7 @@ export default { data.name, }), - color: data.color, - styleRules: [relations.albumStyleRules], - - cover: relations.cover, + styleTags: relations.albumStyleTags, navLinks: [ {auto: 'home'}, diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js index 217282c0..4cec4120 100644 --- a/src/content/dependencies/generateAlbumReleaseInfo.js +++ b/src/content/dependencies/generateAlbumReleaseInfo.js @@ -1,31 +1,14 @@ import {accumulateSum, empty} from '#sugar'; export default { - contentDependencies: [ - 'generateReleaseInfoContributionsLine', - 'linkExternal', - ], - - extraDependencies: ['html', 'language'], - relations(relation, album) { const relations = {}; relations.artistContributionsLine = relation('generateReleaseInfoContributionsLine', album.artistContribs); - relations.coverArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.coverArtistContribs); - - relations.wallpaperArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.wallpaperArtistContribs); - - relations.bannerArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs); - - relations.externalLinks = - album.urls.map(url => - relation('linkExternal', url)); + relations.listenLine = + relation('generateReleaseInfoListenLine', album); return relations; }, @@ -46,7 +29,7 @@ export default { .map(track => track.duration) .filter(value => value > 0); - if (empty(durationTerms)) { + if (empty(durationTerms) || album.hideDuration) { data.duration = null; data.durationApproximate = null; } else { @@ -73,31 +56,11 @@ export default { chronologyKind: 'album', }), - relations.coverArtistContributionsLine.slots({ - stringKey: capsule + '.coverArtBy', - chronologyKind: 'coverArt', - }), - - relations.wallpaperArtistContributionsLine.slots({ - stringKey: capsule + '.wallpaperArtBy', - chronologyKind: 'wallpaperArt', - }), - - relations.bannerArtistContributionsLine.slots({ - stringKey: capsule + '.bannerArtBy', - chronologyKind: 'bannerArt', - }), - language.$(capsule, 'released', { [language.onlyIfOptions]: ['date'], date: language.formatDate(data.date), }), - language.$(capsule, 'artReleased', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.coverArtDate), - }), - language.$(capsule, 'duration', { [language.onlyIfOptions]: ['duration'], duration: @@ -110,21 +73,16 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, - language.$(capsule, 'listenOn', { - [language.onlyIfOptions]: ['links'], - - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => - link.slot('context', [ - 'album', - (data.numTracks === 0 - ? 'albumNoTracks' - : data.numTracks === 1 - ? 'albumOneTrack' - : 'albumMultipleTracks'), - ]))), + relations.listenLine.slots({ + context: [ + 'album', + + (data.numTracks === 0 + ? 'albumNoTracks' + : data.numTracks === 1 + ? 'albumOneTrack' + : 'albumMultipleTracks'), + ], })), ])), }; diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js index bfa48f03..2140bfdb 100644 --- a/src/content/dependencies/generateAlbumSecondaryNav.js +++ b/src/content/dependencies/generateAlbumSecondaryNav.js @@ -1,15 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateAlbumSecondaryNavGroupPart', - 'generateAlbumSecondaryNavSeriesPart', - 'generateDotSwitcherTemplate', - 'generateSecondaryNav', - ], - - extraDependencies: ['html', 'wikiData'], - sprawl: ({groupData}) => ({ // TODO: Series aren't their own things, so we access them weirdly. seriesData: diff --git a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js index 9f9aaf23..2f08804b 100644 --- a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js +++ b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js @@ -2,15 +2,6 @@ import {sortChronologically} from '#sort'; import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateSecondaryNavParentSiblingsPart', - 'linkAlbumDynamically', - 'linkGroup', - ], - - extraDependencies: ['html'], - query(group, album) { const query = {}; @@ -67,6 +58,8 @@ export default { generate: (relations, slots) => relations.parentSiblingsPart.slots({ + attributes: {class: 'group-nav-links'}, + showPreviousNext: slots.mode === 'album', colorStyle: relations.colorStyle, diff --git a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js index f579cdc9..ee180f16 100644 --- a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js +++ b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateSecondaryNavParentSiblingsPart', - 'linkAlbumDynamically', - 'linkGroup', - ], - - extraDependencies: ['html', 'language'], - query(series, album) { const query = {}; @@ -62,7 +53,7 @@ export default { generate: (data, relations, slots, {language}) => relations.parentSiblingsPart.slots({ - attributes: {class: 'series-nav-link'}, + attributes: {class: 'series-nav-links'}, showPreviousNext: slots.mode === 'album', diff --git a/src/content/dependencies/generateAlbumSidebar.js b/src/content/dependencies/generateAlbumSidebar.js index 7cf689cc..83a637b0 100644 --- a/src/content/dependencies/generateAlbumSidebar.js +++ b/src/content/dependencies/generateAlbumSidebar.js @@ -2,17 +2,6 @@ import {sortAlbumsTracksChronologically} from '#sort'; import {stitchArrays, transposeArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateAlbumSidebarGroupBox', - 'generateAlbumSidebarSeriesBox', - 'generateAlbumSidebarTrackListBox', - 'generatePageSidebar', - 'generatePageSidebarConjoinedBox', - 'generateTrackReleaseBox', - ], - - extraDependencies: ['html', 'wikiData'], - sprawl: ({groupData}) => ({ // TODO: Series aren't their own things, so we access them weirdly. seriesData: @@ -46,7 +35,8 @@ export default { const allReleaseAlbums = sortAlbumsTracksChronologically( - Array.from(albumTrackMap.keys())); + Array.from(albumTrackMap.keys()), + {getDate: album => albumTrackMap.get(album).date}); const currentReleaseIndex = allReleaseAlbums.indexOf(track.album); @@ -108,39 +98,65 @@ export default { : null), }), - data: (_query, _sprawl, _album, track) => ({ + data: (_query, _sprawl, album, track) => ({ isAlbumPage: !track, isTrackPage: !!track, + + albumStyle: album.style, }), generate(data, relations, {html}) { + const presentGroupsLikeAlbum = + data.isAlbumPage || + data.albumStyle === 'single'; + for (const box of [ ...relations.groupBoxes, ...relations.seriesBoxes.flat(), ...relations.disconnectedSeriesBoxes, ]) { - box.setSlot('mode', - data.isAlbumPage ? 'album' : 'track'); + box.setSlot('mode', presentGroupsLikeAlbum ? 'album' : 'track'); } + const groupBoxes = + (presentGroupsLikeAlbum + ? [ + 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, + ]), + ]), + ] + : [ + 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. */ + }) + ]); + 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.isAlbumPage && + groupBoxes, data.isTrackPage && relations.earlierTrackReleaseBoxes, @@ -151,20 +167,7 @@ export default { 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. */ - }), + groupBoxes, ], }); }, diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js index f3be74f7..0a9c0db9 100644 --- a/src/content/dependencies/generateAlbumSidebarGroupBox.js +++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js @@ -2,16 +2,6 @@ import {sortChronologically} from '#sort'; import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generatePageSidebarBox', - 'linkAlbum', - 'linkExternal', - 'linkGroup', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query(album, group) { const query = {}; diff --git a/src/content/dependencies/generateAlbumSidebarSeriesBox.js b/src/content/dependencies/generateAlbumSidebarSeriesBox.js index 37616cb2..22f1fe72 100644 --- a/src/content/dependencies/generateAlbumSidebarSeriesBox.js +++ b/src/content/dependencies/generateAlbumSidebarSeriesBox.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generatePageSidebarBox', - 'linkAlbum', - 'linkGroup', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query(album, series) { const query = {}; diff --git a/src/content/dependencies/generateAlbumSidebarTrackListBox.js b/src/content/dependencies/generateAlbumSidebarTrackListBox.js index 3a244e3a..4e9437c9 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackListBox.js +++ b/src/content/dependencies/generateAlbumSidebarTrackListBox.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateAlbumSidebarTrackSection', - 'generatePageSidebarBox', - 'linkAlbum', - ], - - extraDependencies: ['html'], - relations: (relation, album, track) => ({ box: relation('generatePageSidebarBox'), @@ -24,7 +16,9 @@ export default { attributes: {class: 'track-list-sidebar-box'}, content: [ - html.tag('h1', relations.albumLink), + html.tag('h1', {[html.onlyIfSiblings]: true}, + relations.albumLink), + relations.trackSections, ], }) diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index 88aea409..68281bfe 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -1,9 +1,6 @@ -import {empty} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: ['linkTrack'], - extraDependencies: ['getColors', 'html', 'language'], - relations(relation, album, track, trackSection) { const relations = {}; @@ -17,23 +14,27 @@ export default { data(album, track, trackSection) { const data = {}; - data.hasTrackNumbers = album.hasTrackNumbers; + data.hasTrackNumbers = + album.hasTrackNumbers && + !empty(trackSection.tracks); + data.isTrackPage = !!track; + data.albumStyle = album.style; data.name = trackSection.name; data.color = trackSection.color; data.isDefaultTrackSection = trackSection.isDefaultTrackSection; + data.hasSiblingSections = album.trackSections.length > 1; - 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 @@ -43,6 +44,13 @@ export default { trackSection.tracks .map(track => empty(track.commentary)); + data.tracksAreCurrentTrack = + trackSection.tracks + .map(traaaaaaaack => traaaaaaaack === track); + + data.includesCurrentTrack = + data.tracksAreCurrentTrack.includes(true); + return data; }, @@ -72,29 +80,54 @@ export default { } const trackListItems = - relations.trackLinks.map((trackLink, index) => - html.tag('li', - data.includesCurrentTrack && - index === data.currentTrackIndex && - {class: 'current'}, - - slots.mode === 'commentary' && - data.tracksAreMissingCommentary[index] && - {class: 'no-commentary'}, - - language.$(capsule, 'item', { - track: - (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index] - ? trackLink.slots({ - linkless: true, - }) - : slots.anchor - ? trackLink.slots({ - anchor: true, - hash: data.trackDirectories[index], - }) - : trackLink), - }))); + stitchArrays({ + trackLink: relations.trackLinks, + directory: data.trackDirectories, + isCurrentTrack: data.tracksAreCurrentTrack, + missingCommentary: data.tracksAreMissingCommentary, + }).map(({ + trackLink, + directory, + isCurrentTrack, + missingCommentary, + }) => + html.tag('li', + data.includesCurrentTrack && + isCurrentTrack && + {class: 'current'}, + + slots.mode === 'commentary' && + missingCommentary && + {class: 'no-commentary'}, + + language.$(capsule, 'item', { + track: + (slots.mode === 'commentary' && missingCommentary + ? trackLink.slots({ + linkless: true, + }) + : slots.anchor + ? trackLink.slots({ + anchor: true, + hash: directory, + }) + : trackLink), + }))); + + const list = + (data.hasTrackNumbers + ? html.tag('ol', + {start: data.firstTrackNumber}, + trackListItems) + : html.tag('ul', trackListItems)); + + if (data.albumStyle === 'single' && !data.hasSiblingSections) { + if (trackListItems.length <= 1) { + return html.blank(); + } else { + return list; + } + } return html.tag('details', data.includesCurrentTrack && @@ -121,23 +154,24 @@ export default { colorStyle, html.tag('span', - language.encapsulate(capsule, 'group', workingCapsule => { - const workingOptions = {group: sectionName}; - - if (data.hasTrackNumbers) { - workingCapsule += '.withRange'; - workingOptions.range = - `${data.firstTrackNumber}–${data.lastTrackNumber}`; - } - - return language.$(workingCapsule, workingOptions); - }))), - - (data.hasTrackNumbers - ? html.tag('ol', - {start: data.firstTrackNumber}, - trackListItems) - : html.tag('ul', trackListItems)), + 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); + })))), + + list, ]); }, }; diff --git a/src/content/dependencies/generateAlbumSocialEmbed.js b/src/content/dependencies/generateAlbumSocialEmbed.js index ad02e180..1200ec8b 100644 --- a/src/content/dependencies/generateAlbumSocialEmbed.js +++ b/src/content/dependencies/generateAlbumSocialEmbed.js @@ -1,13 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateSocialEmbed', - 'generateAlbumSocialEmbedDescription', - ], - - extraDependencies: ['absoluteTo', 'language'], - relations(relation, album) { return { socialEmbed: @@ -32,8 +25,7 @@ export default { 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; @@ -65,7 +57,7 @@ export default { imagePath: (data.hasImage - ? ['media.albumCover', data.coverArtDirectory, data.coverArtFileExtension] + ? data.imagePath : null), })), }; diff --git a/src/content/dependencies/generateAlbumSocialEmbedDescription.js b/src/content/dependencies/generateAlbumSocialEmbedDescription.js index 69c39c3a..db6da5b7 100644 --- a/src/content/dependencies/generateAlbumSocialEmbedDescription.js +++ b/src/content/dependencies/generateAlbumSocialEmbedDescription.js @@ -1,8 +1,6 @@ import {accumulateSum} from '#sugar'; export default { - extraDependencies: ['language'], - data: (album) => ({ duration: accumulateSum(album.tracks, track => track.duration), diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js deleted file mode 100644 index 6bfcc62e..00000000 --- a/src/content/dependencies/generateAlbumStyleRules.js +++ /dev/null @@ -1,107 +0,0 @@ -import {empty, stitchArrays} from '#sugar'; - -export default { - extraDependencies: ['to'], - - data(album, track) { - const data = {}; - - data.hasWallpaper = !empty(album.wallpaperArtistContribs); - data.hasBanner = !empty(album.bannerArtistContribs); - - if (data.hasWallpaper) { - if (!empty(album.wallpaperParts)) { - data.wallpaperMode = 'parts'; - - data.wallpaperPaths = - album.wallpaperParts.map(part => - (part.asset - ? ['media.albumWallpaperPart', album.directory, part.asset] - : null)); - - data.wallpaperStyles = - album.wallpaperParts.map(part => part.style); - } else { - data.wallpaperMode = 'one'; - data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension]; - data.wallpaperStyle = album.wallpaperStyle; - } - } - - if (data.hasBanner) { - data.hasBannerStyle = !!album.bannerStyle; - data.bannerStyle = album.bannerStyle; - } - - data.albumDirectory = album.directory; - - if (track) { - data.trackDirectory = track.directory; - } - - return data; - }, - - generate(data, {to}) { - const indent = parts => - (parts ?? []) - .filter(Boolean) - .join('\n') - .split('\n') - .map(line => ' '.repeat(4) + line) - .join('\n'); - - const rule = (selector, parts) => - (!empty(parts.filter(Boolean)) - ? [`${selector} {`, indent(parts), `}`] - : []); - - const oneWallpaperRule = - data.wallpaperMode === 'one' && - rule(`body::before`, [ - `background-image: url("${to(...data.wallpaperPath)}");`, - data.wallpaperStyle, - ]); - - const wallpaperPartRules = - data.wallpaperMode === 'parts' && - stitchArrays({ - path: data.wallpaperPaths, - style: data.wallpaperStyles, - }).map(({path, style}, index) => - rule(`.wallpaper-part:nth-child(${index + 1})`, [ - path && `background-image: url("${to(...path)}");`, - style, - ])); - - const nukeBasicWallpaperRule = - data.wallpaperMode === 'parts' && - rule(`body::before`, ['display: none']); - - const wallpaperRules = [ - oneWallpaperRule, - ...wallpaperPartRules || [], - nukeBasicWallpaperRule, - ]; - - const bannerRule = - data.hasBanner && - rule(`#banner img`, [ - data.bannerStyle, - ]); - - const dataRule = - rule(`:root`, [ - data.albumDirectory && - `--album-directory: ${data.albumDirectory};`, - data.trackDirectory && - `--track-directory: ${data.trackDirectory};`, - ]); - - return ( - [...wallpaperRules, bannerRule, dataRule] - .filter(Boolean) - .flat() - .join('\n')); - }, -}; diff --git a/src/content/dependencies/generateAlbumStyleTags.js b/src/content/dependencies/generateAlbumStyleTags.js new file mode 100644 index 00000000..caf21dc4 --- /dev/null +++ b/src/content/dependencies/generateAlbumStyleTags.js @@ -0,0 +1,62 @@ +import {empty} from '#sugar'; + +export default { + 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 9743c750..93cb420b 100644 --- a/src/content/dependencies/generateAlbumTrackList.js +++ b/src/content/dependencies/generateAlbumTrackList.js @@ -35,14 +35,6 @@ function getDisplayMode(album) { } export default { - contentDependencies: [ - 'generateAlbumTrackListItem', - 'generateContentHeading', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query(album) { return { displayMode: getDisplayMode(album), @@ -102,11 +94,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); } @@ -147,7 +139,7 @@ export default { name: data.trackSectionNames, duration: data.trackSectionDurations, durationApproximate: data.trackSectionDurationsApproximate, - startIndex: data.trackSectionStartIndices, + startCountingFrom: data.trackSectionsStartCountingFrom, }).map(({ heading, description, @@ -156,7 +148,7 @@ export default { name, duration, durationApproximate, - startIndex, + startCountingFrom, }) => [ language.encapsulate('trackList.section', capsule => heading.slots({ @@ -190,7 +182,7 @@ export default { html.tag(listTag, data.hasTrackNumbers && - {start: startIndex + 1}, + {start: startCountingFrom}, slotItems(items)), ]), diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js index 44297c15..68722a83 100644 --- a/src/content/dependencies/generateAlbumTrackListItem.js +++ b/src/content/dependencies/generateAlbumTrackListItem.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateTrackListItem'], - extraDependencies: ['html'], - query: (track, album) => ({ trackHasDuration: !!track.duration, @@ -20,7 +17,7 @@ export default { item: relation('generateTrackListItem', track, - track.album.artistContribs), + track.album.trackArtistContribs), }), data: (query, track, album) => ({ @@ -43,7 +40,7 @@ export default { generate: (data, relations, slots) => relations.item.slots({ - showArtists: true, + showArtists: 'auto', showDuration: (slots.collapseDurationScope === 'track' diff --git a/src/content/dependencies/generateAlbumWallpaperStyleTag.js b/src/content/dependencies/generateAlbumWallpaperStyleTag.js new file mode 100644 index 00000000..b3f74716 --- /dev/null +++ b/src/content/dependencies/generateAlbumWallpaperStyleTag.js @@ -0,0 +1,35 @@ +export default { + 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 index 89150615..37a32a94 100644 --- a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js +++ b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js @@ -6,9 +6,6 @@ import { } from '#sugar'; export default { - contentDependencies: ['linkArtTagDynamically'], - extraDependencies: ['html', 'language'], - // Recursion ain't too pretty! query(ancestorArtTag, targetArtTag) { @@ -33,8 +30,8 @@ export default { const artTagsTimesFeaturedTotal = artTags.map(artTag => unique([ - ...artTag.directlyTaggedInThings, - ...artTag.indirectlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, ]).length); const sublists = diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js index b633e58f..f20babba 100644 --- a/src/content/dependencies/generateArtTagGalleryPage.js +++ b/src/content/dependencies/generateArtTagGalleryPage.js @@ -1,24 +1,7 @@ -import {sortAlbumsTracksChronologically} from '#sort'; +import {sortArtworksChronologically} from '#sort'; import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateArtTagGalleryPageFeaturedLine', - 'generateArtTagGalleryPageShowingLine', - 'generateArtTagNavLinks', - 'generateCoverGrid', - 'generatePageLayout', - 'generateQuickDescription', - 'image', - 'linkAlbum', - 'linkArtTagGallery', - 'linkExternal', - 'linkTrack', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({wikiInfo}) { return { enableListings: wikiInfo.enableListings, @@ -26,16 +9,13 @@ export default { }, query(sprawl, artTag) { - const directThings = artTag.directlyTaggedInThings; - const indirectThings = artTag.indirectlyTaggedInThings; - const allThings = unique([...directThings, ...indirectThings]); + const directArtworks = artTag.directlyFeaturedInArtworks; + const indirectArtworks = artTag.indirectlyFeaturedInArtworks; + const allArtworks = unique([...directArtworks, ...indirectArtworks]); - sortAlbumsTracksChronologically(allThings, { - getDate: thing => thing.coverArtDate ?? thing.date, - latestFirst: true, - }); + sortArtworksChronologically(allArtworks, {latestFirst: true}); - return {directThings, indirectThings, allThings}; + return {directArtworks, indirectArtworks, allArtworks}; }, relations(relation, query, sprawl, artTag) { @@ -81,15 +61,12 @@ export default { relation('generateCoverGrid'); relations.links = - query.allThings - .map(thing => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))); + query.allArtworks + .map(artwork => relation('linkAnythingMan', artwork.thing)); relations.images = - query.allThings - .map(thing => relation('image', thing.artTags)); + query.allArtworks + .map(artwork => relation('image', artwork)); return relations; }, @@ -102,30 +79,26 @@ export default { data.name = artTag.name; data.color = artTag.color; - data.numArtworksIndirectly = query.indirectThings.length; - data.numArtworksDirectly = query.directThings.length; - data.numArtworksTotal = query.allThings.length; + data.numArtworksIndirectly = query.indirectArtworks.length; + data.numArtworksDirectly = query.directArtworks.length; + data.numArtworksTotal = query.allArtworks.length; data.names = - query.allThings.map(thing => thing.name); - - data.paths = - query.allThings.map(thing => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])); + query.allArtworks + .map(artwork => artwork.thing.name); - data.dimensions = - query.allThings.map(thing => thing.coverArtDimensions); + data.artworkArtists = + query.allArtworks + .map(artwork => artwork.artistContribs + .map(contrib => contrib.artist.name)); - data.coverArtists = - query.allThings.map(thing => - thing.coverArtistContribs - .map(({artist}) => artist.name)); + data.artworkLabels = + query.allArtworks + .map(artwork => artwork.label) data.onlyFeaturedIndirectly = - query.allThings.map(thing => - !query.directThings.includes(thing)); + query.allArtworks.map(artwork => + !query.directArtworks.includes(artwork)); data.hasMixedDirectIndirect = data.onlyFeaturedIndirectly.includes(true) && @@ -210,6 +183,7 @@ export default { relations.coverGrid .slots({ links: relations.links, + images: relations.images, names: data.names, lazy: 12, @@ -217,24 +191,25 @@ export default { data.onlyFeaturedIndirectly.map(onlyFeaturedIndirectly => (onlyFeaturedIndirectly ? 'featured-indirectly' : '')), - images: + info: stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, + 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); })), - - info: - data.coverArtists.map(names => - (names === null - ? null - : language.$('misc.coverGrid.details.coverArtists', { - artists: language.formatUnitList(names), - }))), }), ], diff --git a/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js index b4620fa4..8593cc21 100644 --- a/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js +++ b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - slots: { showing: { validate: v => v.is('all', 'direct', 'indirect'), diff --git a/src/content/dependencies/generateArtTagGalleryPageShowingLine.js b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js index 6df4d0e5..2a34ae57 100644 --- a/src/content/dependencies/generateArtTagGalleryPageShowingLine.js +++ b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - slots: { showing: { validate: v => v.is('all', 'direct', 'indirect'), diff --git a/src/content/dependencies/generateArtTagInfoPage.js b/src/content/dependencies/generateArtTagInfoPage.js index 7765f159..683eeab6 100644 --- a/src/content/dependencies/generateArtTagInfoPage.js +++ b/src/content/dependencies/generateArtTagInfoPage.js @@ -1,20 +1,6 @@ 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, }), @@ -23,10 +9,10 @@ export default { const query = {}; query.directThings = - artTag.directlyTaggedInThings; + artTag.directlyFeaturedInArtworks; query.indirectThings = - artTag.indirectlyTaggedInThings; + artTag.indirectlyFeaturedInArtworks; query.allThings = unique([...query.directThings, ...query.indirectThings]); @@ -111,8 +97,8 @@ export default { directDescendantTimesFeaturedTotal: artTag.directDescendantArtTags.map(artTag => unique([ - ...artTag.directlyTaggedInThings, - ...artTag.indirectlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, ]).length), }), @@ -182,12 +168,12 @@ export default { artTagLink: relations.relatedArtTagLinks, annotation: data.relatedArtTagAnnotations, }).map(({artTagLink, annotation}) => - (html.isBlank(annotation) - ? artTagLink - : language.$(capsule, 'tagWithAnnotation', { + (annotation + ? language.$(capsule, 'tagWithAnnotation', { tag: artTagLink, annotation, - })))), + }) + : artTagLink))), }))), html.tag('blockquote', diff --git a/src/content/dependencies/generateArtTagNavLinks.js b/src/content/dependencies/generateArtTagNavLinks.js index 9061a09f..1298ce99 100644 --- a/src/content/dependencies/generateArtTagNavLinks.js +++ b/src/content/dependencies/generateArtTagNavLinks.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'linkArtTagInfo', - 'linkArtTagGallery', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({enableListings: wikiInfo.enableListings}), diff --git a/src/content/dependencies/generateArtTagSidebar.js b/src/content/dependencies/generateArtTagSidebar.js index c281b93d..60ea504f 100644 --- a/src/content/dependencies/generateArtTagSidebar.js +++ b/src/content/dependencies/generateArtTagSidebar.js @@ -1,15 +1,6 @@ import {collectTreeLeaves, empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: [ - 'generatePageSidebar', - 'generatePageSidebarBox', - 'generateArtTagAncestorDescendantMapList', - 'linkArtTagDynamically', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({artTagData}) => ({artTagData}), @@ -54,8 +45,8 @@ export default { directDescendantTimesFeaturedTotal: artTag.directDescendantArtTags.map(artTag => unique([ - ...artTag.directlyTaggedInThings, - ...artTag.indirectlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, ]).length), furthestAncestorArtTagNames: diff --git a/src/content/dependencies/generateArtistArtworkColumn.js b/src/content/dependencies/generateArtistArtworkColumn.js new file mode 100644 index 00000000..19c66b8a --- /dev/null +++ b/src/content/dependencies/generateArtistArtworkColumn.js @@ -0,0 +1,11 @@ +export default { + 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 index 72d55854..389de740 100644 --- a/src/content/dependencies/generateArtistCredit.js +++ b/src/content/dependencies/generateArtistCredit.js @@ -1,14 +1,7 @@ -import {compareArrays, empty} from '#sugar'; +import {compareArrays, empty, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistCreditWikiEditsPart', - 'linkContribution', - ], - - extraDependencies: ['html', 'language'], - - query: (creditContributions, contextContributions) => { + query: (creditContributions, contextContributions, _formatText) => { const query = {}; const featuringFilter = contribution => @@ -36,16 +29,26 @@ export default { // Note that the normal contributions will implicitly *always* // "differ from context" if no context contributions are given, // as in release info lines. - query.normalContributionsDifferFromContext = + + query.normalContributionArtistsDifferFromContext = !compareArrays( query.normalContributions.map(({artist}) => artist), contextNormalContributions.map(({artist}) => artist), - {checkOrder: false}); + {checkOrder: true}); + + query.normalContributionAnnotationsDifferFromContext = + !compareArrays( + query.normalContributions.map(({annotation}) => annotation), + contextNormalContributions.map(({annotation}) => annotation), + {checkOrder: true}); return query; }, - relations: (relation, query, _creditContributions, _contextContributions) => ({ + relations: (relation, query, + _creditContributions, + _contextContributions, + formatText) => ({ normalContributionLinks: query.normalContributions .map(contrib => relation('linkContribution', contrib)), @@ -57,11 +60,25 @@ export default { wikiEditsPart: relation('generateArtistCreditWikiEditsPart', query.wikiEditContributions), + + formatText: + relation('transformContent', formatText), }), - data: (query, _creditContributions, _contextContributions) => ({ - normalContributionsDifferFromContext: - query.normalContributionsDifferFromContext, + data: (query, _creditContributions, _contextContributions, _formatText) => ({ + normalContributionArtistsDifferFromContext: + query.normalContributionArtistsDifferFromContext, + + normalContributionAnnotationsDifferFromContext: + query.normalContributionAnnotationsDifferFromContext, + + normalContributionArtistDirectories: + query.normalContributions + .map(contrib => contrib.artist.directory), + + featuringContributionArtistDirectories: + query.featuringContributions + .map(contrib => contrib.artist.directory), hasWikiEdits: !empty(query.wikiEditContributions), @@ -80,6 +97,8 @@ export default { // 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}, @@ -93,6 +112,10 @@ export default { generate(data, relations, slots, {html, language}) { if (!slots.normalStringKey) return html.blank(); + const effectivelyDiffers = + (slots.showAnnotation && data.normalContributionAnnotationsDifferFromContext) || + (data.normalContributionArtistsDifferFromContext); + for (const link of [ ...relations.normalContributionLinks, ...relations.featuringContributionLinks, @@ -120,49 +143,112 @@ export default { }); } - if (empty(relations.normalContributionLinks)) { - return html.blank(); + let formattedArtistList = null; + + if (!html.isBlank(relations.formatText)) { + formattedArtistList = relations.formatText; + + const substituteContrib = ({link, directory}) => ({ + match: {replacerKey: 'artist', replacerValue: directory}, + substitute: link, + + apply(link, node) { + if (node.data.label) { + link.setSlot('content', language.sanitize(node.data.label)); + } + }, + }); + + relations.formatText.setSlots({ + mode: 'inline', + + substitute: [ + stitchArrays({ + link: relations.normalContributionLinks, + directory: data.normalContributionArtistDirectories, + }).map(substituteContrib), + + stitchArrays({ + link: relations.featuringContributionLinks, + directory: data.featuringContributionArtistDirectories, + }).map(substituteContrib), + ].flat(), + }); } - 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, - ]); - - if (empty(relations.featuringContributionLinks)) { - if (data.normalContributionsDifferFromContext) { - return language.$(slots.normalStringKey, {artists: artistsList}); - } else { + let content; + + if (formattedArtistList) { + if (effectivelyDiffers) { + content = + language.$(slots.normalStringKey, { + ...slots.additionalStringOptions, + artists: formattedArtistList, + }); + } + } else { + if (empty(relations.normalContributionLinks)) { return html.blank(); } - } - if (data.normalContributionsDifferFromContext && slots.normalFeaturingStringKey) { - return language.$(slots.normalFeaturingStringKey, { - artists: artistsList, - featuring: featuringList, - }); - } else if (slots.featuringStringKey) { - return language.$(slots.featuringStringKey, {artists: featuringList}); - } else { - return language.$(slots.normalStringKey, {artists: everyoneList}); + 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, + ]); + + if (empty(relations.featuringContributionLinks)) { + if (effectivelyDiffers) { + content = + language.$(slots.normalStringKey, { + ...slots.additionalStringOptions, + artists: artistsList, + }); + } else { + return html.blank(); + } + } else if (effectivelyDiffers && slots.normalFeaturingStringKey) { + content = + language.$(slots.normalFeaturingStringKey, { + ...slots.additionalStringOptions, + artists: artistsList, + featuring: featuringList, + }); + } else if (slots.featuringStringKey) { + content = + language.$(slots.featuringStringKey, { + ...slots.additionalStringOptions, + artists: featuringList, + }); + } else { + content = + language.$(slots.normalStringKey, { + ...slots.additionalStringOptions, + artists: everyoneList, + }); + } } + + // TODO: This is obviously evil. + return ( + html.metatag('chunkwrap', {split: /,| (?=and)/}, + html.resolve(content))); }, }; diff --git a/src/content/dependencies/generateArtistCreditWikiEditsPart.js b/src/content/dependencies/generateArtistCreditWikiEditsPart.js index 70296e39..4178928d 100644 --- a/src/content/dependencies/generateArtistCreditWikiEditsPart.js +++ b/src/content/dependencies/generateArtistCreditWikiEditsPart.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateTextWithTooltip', - 'generateTooltip', - 'linkContribution', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, contributions) => ({ textWithTooltip: relation('generateTextWithTooltip'), @@ -48,6 +40,7 @@ export default { showAnnotation: slots.showAnnotation, trimAnnotation: true, preventTooltip: true, + preventWrapping: true, }))), }), }), diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js index 7a76188a..d8f1c4b1 100644 --- a/src/content/dependencies/generateArtistGalleryPage.js +++ b/src/content/dependencies/generateArtistGalleryPage.js @@ -1,89 +1,58 @@ -import {sortAlbumsTracksChronologically} from '#sort'; -import {stitchArrays} from '#sugar'; +import {sortArtworksChronologically} from '#sort'; export default { - contentDependencies: [ - 'generateArtistNavLinks', - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - - query(artist) { - const things = - ([ - artist.albumCoverArtistContributions, - artist.trackCoverArtistContributions, - ]).flat() - .filter(({annotation}) => !annotation?.startsWith(`edits for wiki`)) - .map(({thing}) => thing); - - sortAlbumsTracksChronologically(things, { - latestFirst: true, - getDate: thing => thing.coverArtDate ?? thing.date, - }); - - return {things}; - }, - - relations(relation, query, artist) { - const relations = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.artistNavLinks = - relation('generateArtistNavLinks', artist); - - relations.coverGrid = - relation('generateCoverGrid'); - - relations.links = - query.things.map(thing => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))); - - relations.images = - query.things.map(thing => - relation('image', thing.artTags)); - - return relations; - }, - - data(query, artist) { - const data = {}; - - data.name = artist.name; - - data.numArtworks = query.things.length; - - data.names = - query.things.map(thing => thing.name); - - data.paths = - query.things.map(thing => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])); - - data.dimensions = - query.things.map(thing => thing.coverArtDimensions); - - data.otherCoverArtists = - query.things.map(thing => - (thing.coverArtistContribs.length > 1 - ? thing.coverArtistContribs - .filter(({artist: otherArtist}) => otherArtist !== artist) - .map(({artist: otherArtist}) => otherArtist.name) - : null)); - - return data; - }, + query: (artist) => ({ + artworks: + sortArtworksChronologically( + ([ + artist.albumCoverArtistContributions, + artist.trackCoverArtistContributions, + ]).flat() + .filter(contrib => !contrib.annotation?.startsWith(`edits for wiki`)) + .map(contrib => contrib.thing), + {latestFirst: true}), + }), + + relations: (relation, query, artist) => ({ + layout: + relation('generatePageLayout'), + + artistNavLinks: + relation('generateArtistNavLinks', artist), + + coverGrid: + relation('generateCoverGrid'), + + links: + query.artworks + .map(artwork => relation('linkAnythingMan', artwork.thing)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + }), + + data: (query, artist) => ({ + name: + artist.name, + + numArtworks: + query.artworks.length, + + names: + query.artworks + .map(artwork => artwork.thing.name), + + otherCoverArtists: + query.artworks + .map(artwork => artwork.artistContribs + .filter(contrib => contrib.artist !== artist) + .map(contrib => contrib.artist.name)), + + allWarnings: + query.artworks + .flatMap(artwork => artwork.contentWarnings), + }), generate: (data, relations, {html, language}) => language.encapsulate('artistGalleryPage', pageCapsule => @@ -100,7 +69,7 @@ export default { html.tag('p', {class: 'quick-info'}, language.$(pageCapsule, 'infoLine', { coverArts: - language.countCoverArts(data.numArtworks, { + language.countArtworks(data.numArtworks, { unit: true, }), })), @@ -108,27 +77,18 @@ export default { relations.coverGrid .slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, - })), - - // TODO: Can this be [language.onlyIfOptions]? info: data.otherCoverArtists.map(names => - (names === null - ? null - : language.$('misc.coverGrid.details.otherCoverArtists', { - artists: language.formatUnitList(names), - }))), + language.$('misc.coverGrid.details.otherCoverArtists', { + [language.onlyIfOptions]: ['artists'], + + artists: language.formatUnitList(names), + })), + + revealAllWarnings: data.allWarnings, }), ], diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js index 3e0cd1d2..6940053f 100644 --- a/src/content/dependencies/generateArtistGroupContributionsInfo.js +++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js @@ -1,83 +1,87 @@ -import {empty, filterProperties, stitchArrays, unique} from '#sugar'; +import {accumulateSum, empty, stitchArrays, withEntries} from '#sugar'; export default { - contentDependencies: ['linkGroup'], - extraDependencies: ['html', 'language', 'wikiData'], + sprawl: ({groupCategoryData}) => ({ + groupOrder: + groupCategoryData.flatMap(category => category.groups), + }), - sprawl({groupCategoryData}) { - return { - groupOrder: groupCategoryData.flatMap(category => category.groups), - } - }, + query(sprawl, contributions) { + const allGroupsUnordered = + new Set(contributions.flatMap(contrib => contrib.groups)); - query(sprawl, tracksAndAlbums) { - const filteredAlbums = tracksAndAlbums.filter(thing => !thing.album); - const filteredTracks = tracksAndAlbums.filter(thing => thing.album); + const allGroupsOrdered = + sprawl.groupOrder.filter(group => allGroupsUnordered.has(group)); - const allAlbums = unique([ - ...filteredAlbums, - ...filteredTracks.map(track => track.album), - ]); + const groupToThingsCountedForContributions = + new Map(allGroupsOrdered.map(group => [group, new Set])); - const allGroupsUnordered = new Set(Array.from(allAlbums).flatMap(album => album.groups)); - const allGroupsOrdered = sprawl.groupOrder.filter(group => allGroupsUnordered.has(group)); + const groupToThingsCountedForDuration = + 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); - - for (const album of filteredAlbums) { - for (const group of album.groups) { - groupToCountMap.set(group, groupToCountMap.get(group) + 1); - } - } + for (const contrib of contributions) { + for (const group of contrib.groups) { + if (contrib.countInContributionTotals) { + groupToThingsCountedForContributions.get(group).add(contrib.thing); + } - for (const track of filteredTracks) { - for (const group of track.album.groups) { - groupToCountMap.set(group, groupToCountMap.get(group) + 1); - if (track.duration && track.mainReleaseTrack === null) { - groupToDurationMap.set(group, groupToDurationMap.get(group) + track.duration); - groupToDurationCountMap.set(group, groupToDurationCountMap.get(group) + 1); + if (contrib.countInDurationTotals) { + groupToThingsCountedForDuration.get(group).add(contrib.thing); } } } + const groupToTotalContributions = + withEntries( + groupToThingsCountedForContributions, + entries => entries.map( + ([group, things]) => + ([group, things.size]))); + + const groupToTotalDuration = + withEntries( + groupToThingsCountedForDuration, + entries => entries.map( + ([group, things]) => + ([group, accumulateSum(things, thing => thing.duration)]))) + const groupsSortedByCount = allGroupsOrdered - .slice() - .sort((a, b) => groupToCountMap.get(b) - groupToCountMap.get(a)); + .filter(group => groupToTotalContributions.get(group) > 0) + .sort((a, b) => + (groupToTotalContributions.get(b) + - groupToTotalContributions.get(a))); - // The filter here ensures all displayed groups have at least some duration - // when sorting by duration. const groupsSortedByDuration = allGroupsOrdered - .filter(group => groupToDurationMap.get(group) > 0) - .sort((a, b) => groupToDurationMap.get(b) - groupToDurationMap.get(a)); + .filter(group => groupToTotalDuration.get(group) > 0) + .sort((a, b) => + (groupToTotalDuration.get(b) + - groupToTotalDuration.get(a))); const groupCountsSortedByCount = groupsSortedByCount - .map(group => groupToCountMap.get(group)); + .map(group => groupToTotalContributions.get(group)); const groupDurationsSortedByCount = groupsSortedByCount - .map(group => groupToDurationMap.get(group)); + .map(group => groupToTotalDuration.get(group)); const groupDurationsApproximateSortedByCount = groupsSortedByCount - .map(group => groupToDurationCountMap.get(group) > 1); + .map(group => groupToThingsCountedForDuration.get(group).size > 1); const groupCountsSortedByDuration = groupsSortedByDuration - .map(group => groupToCountMap.get(group)); + .map(group => groupToTotalContributions.get(group)); const groupDurationsSortedByDuration = groupsSortedByDuration - .map(group => groupToDurationMap.get(group)); + .map(group => groupToTotalDuration.get(group)); const groupDurationsApproximateSortedByDuration = groupsSortedByDuration - .map(group => groupToDurationCountMap.get(group) > 1); + .map(group => groupToThingsCountedForDuration.get(group).size > 1); return { groupsSortedByCount, @@ -93,29 +97,35 @@ export default { }; }, - relations(relation, query) { - return { - groupLinksSortedByCount: - query.groupsSortedByCount - .map(group => relation('linkGroup', group)), + relations: (relation, query) => ({ + groupLinksSortedByCount: + query.groupsSortedByCount + .map(group => relation('linkGroup', group)), - groupLinksSortedByDuration: - query.groupsSortedByDuration - .map(group => relation('linkGroup', group)), - }; - }, + groupLinksSortedByDuration: + query.groupsSortedByDuration + .map(group => relation('linkGroup', group)), + }), - data(query) { - return filterProperties(query, [ - 'groupCountsSortedByCount', - 'groupDurationsSortedByCount', - 'groupDurationsApproximateSortedByCount', + data: (query) => ({ + groupCountsSortedByCount: + query.groupCountsSortedByCount, - 'groupCountsSortedByDuration', - 'groupDurationsSortedByDuration', - 'groupDurationsApproximateSortedByDuration', - ]); - }, + groupDurationsSortedByCount: + query.groupDurationsSortedByCount, + + groupDurationsApproximateSortedByCount: + query.groupDurationsApproximateSortedByCount, + + groupCountsSortedByDuration: + query.groupCountsSortedByDuration, + + groupDurationsSortedByDuration: + query.groupDurationsSortedByDuration, + + groupDurationsApproximateSortedByDuration: + query.groupDurationsApproximateSortedByDuration, + }), slots: { title: { diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index 0c4e4189..cf8ce994 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -1,49 +1,18 @@ import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistGroupContributionsInfo', - 'generateArtistInfoPageArtworksChunkedList', - 'generateArtistInfoPageCommentaryChunkedList', - 'generateArtistInfoPageFlashesChunkedList', - 'generateArtistInfoPageTracksChunkedList', - 'generateArtistNavLinks', - 'generateContentHeading', - 'generateCoverArtwork', - 'generatePageLayout', - 'image', - 'linkArtistGallery', - 'linkExternal', - 'linkGroup', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query: (artist) => ({ - // Even if an artist has served as both "artist" (compositional) and - // "contributor" (instruments, production, etc) on the same track, that - // track only counts as one unique contribution in the list. - allTracks: - unique( - ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - ]).flat() - .map(({thing}) => thing)), - - // Artworks are different, though. We intentionally duplicate album data - // objects when the artist has contributed some combination of cover art, - // wallpaper, and banner - these each count as a unique contribution. - allArtworks: - ([ - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, - artist.trackCoverArtistContributions, - ]).flat() - .filter(({annotation}) => !annotation?.startsWith('edits for wiki')) - .map(({thing}) => thing), + trackContributions: [ + ...artist.trackArtistContributions, + ...artist.trackContributorContributions, + ], + + artworkContributions: [ + ...artist.albumCoverArtistContributions, + ...artist.albumWallpaperArtistContributions, + ...artist.albumBannerArtistContributions, + ...artist.trackCoverArtistContributions, + ], // Banners and wallpapers don't show up in the artist gallery page, only // cover art. @@ -69,15 +38,8 @@ export default { artistNavLinks: relation('generateArtistNavLinks', artist), - cover: - (artist.hasAvatar - ? relation('generateCoverArtwork', [], []) - : null), - - image: - (artist.hasAvatar - ? relation('image') - : null), + artworkColumn: + relation('generateArtistArtworkColumn', artist), contentHeading: relation('generateContentHeading'), @@ -101,7 +63,7 @@ export default { relation('generateArtistInfoPageTracksChunkedList', artist), tracksGroupInfo: - relation('generateArtistGroupContributionsInfo', query.allTracks), + relation('generateArtistGroupContributionsInfo', query.trackContributions), artworksChunkedList: relation('generateArtistInfoPageArtworksChunkedList', artist, false), @@ -110,7 +72,7 @@ export default { relation('generateArtistInfoPageArtworksChunkedList', artist, true), artworksGroupInfo: - relation('generateArtistGroupContributionsInfo', query.allArtworks), + relation('generateArtistGroupContributionsInfo', query.artworkContributions), artistGalleryLink: (query.hasGallery @@ -131,20 +93,16 @@ export default { name: artist.name, - directory: - artist.directory, - - avatarFileExtension: - (artist.hasAvatar - ? artist.avatarFileExtension - : null), - closeGroupAnnotations: query.generalLinkedGroups .map(({annotation}) => annotation), totalTrackCount: - query.allTracks.length, + unique( + query.trackContributions + .filter(contrib => contrib.countInContributionTotals) + .map(contrib => contrib.thing)) + .length, totalDuration: artist.totalDuration, @@ -156,19 +114,8 @@ export default { title: data.name, headingMode: 'sticky', - cover: - (relations.cover - ? relations.cover.slots({ - image: - relations.image.slots({ - path: [ - 'media.artistAvatar', - data.directory, - data.avatarFileExtension, - ], - }), - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ html.tags([ diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js index 66e4204a..f4c9439a 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageArtworksChunkItem', - 'linkAlbum', - ], - - extraDependencies: ['html'], - relations: (relation, album, contribs) => ({ template: relation('generateArtistInfoPageChunk'), diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js index 089cfb8d..e3ba5342 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js @@ -1,19 +1,13 @@ -export default { - contentDependencies: [ - 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageOtherArtistLinks', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], +import {empty} from '#sugar'; +export default { query: (contrib) => ({ kind: - (contrib.isBannerArtistContribution + (contrib.thing.thingProperty === 'bannerArtwork' ? 'banner' - : contrib.isWallpaperArtistContribution + : contrib.thing.thingProperty === 'wallpaperArtwork' ? 'wallpaper' - : contrib.isForAlbum + : contrib.thing.thingProperty === 'coverArtworks' ? 'album-cover' : 'track-cover'), }), @@ -24,11 +18,14 @@ export default { trackLink: (query.kind === 'track-cover' - ? relation('linkTrack', contrib.thing) + ? relation('linkTrack', contrib.thing.thing) : null), otherArtistLinks: relation('generateArtistInfoPageOtherArtistLinks', [contrib]), + + originDetails: + relation('transformContent', contrib.thing.originDetails), }), data: (query, contrib) => ({ @@ -37,6 +34,9 @@ export default { annotation: contrib.annotation, + + label: + contrib.thing.label, }), slots: { @@ -51,9 +51,33 @@ export default { otherArtistLinks: relations.otherArtistLinks, annotation: - (slots.filterEditsForWiki - ? data.annotation?.replace(/^edits for wiki(: )?/, '') - : data.annotation), + language.encapsulate('artistPage.creditList.entry.artwork.accent', workingCapsule => { + const workingOptions = {}; + + const artworkLabel = data.label; + + if (artworkLabel) { + workingCapsule += '.withLabel'; + workingOptions.label = + language.typicallyLowerCase(artworkLabel); + } + + const contribAnnotation = + (slots.filterEditsForWiki + ? data.annotation?.replace(/^edits for wiki(: )?/, '') + : data.annotation); + + if (contribAnnotation) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = contribAnnotation; + } + + if (empty(Object.keys(workingOptions))) { + return html.blank(); + } + + return language.$(workingCapsule, workingOptions); + }), content: language.encapsulate('artistPage.creditList.entry', capsule => @@ -68,5 +92,11 @@ export default { : data.kind === 'banner' ? language.$(capsule, 'bannerArt') : language.$(capsule, 'coverArt')))))), + + originDetails: + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }), }), }; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index 8b024147..40ffc5dd 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -3,11 +3,6 @@ import {sortAlbumsTracksChronologically, sortContributionsChronologically} import {chunkByConditions, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageArtworksChunk', - ], - query(artist, filterEditsForWiki) { const query = {}; @@ -27,20 +22,21 @@ export default { sortContributionsChronologically( filteredContributions, - sortAlbumsTracksChronologically); + sortAlbumsTracksChronologically, + {getThing: contrib => contrib.thing.thing}); query.contribs = chunkByConditions(filteredContributions, [ ({date: date1}, {date: date2}) => +date1 !== +date2, - ({thing: thing1}, {thing: thing2}) => + ({thing: {thing: thing1}}, {thing: {thing: thing2}}) => (thing1.album ?? thing1) !== (thing2.album ?? thing2), ]); query.albums = query.contribs - .map(contribs => contribs[0].thing) + .map(contribs => contribs[0].thing.thing) .map(thing => thing.album ?? thing); return query; diff --git a/src/content/dependencies/generateArtistInfoPageChunk.js b/src/content/dependencies/generateArtistInfoPageChunk.js index fce68a7d..80429912 100644 --- a/src/content/dependencies/generateArtistInfoPageChunk.js +++ b/src/content/dependencies/generateArtistInfoPageChunk.js @@ -1,8 +1,6 @@ import {empty} from '#sugar'; export default { - extraDependencies: ['html', 'language'], - slots: { mode: { validate: v => v.is('flash', 'album'), diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js index 7987b642..8117ca9a 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js @@ -1,9 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: ['generateTextWithTooltip'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ textWithTooltip: relation('generateTextWithTooltip'), @@ -33,6 +30,11 @@ export default { type: 'html', mutable: false, }, + + originDetails: { + type: 'html', + mutable: false, + }, }, generate: (relations, slots, {html, language}) => @@ -40,52 +42,59 @@ export default { html.tag('li', slots.rerelease && {class: 'rerelease'}, - language.encapsulate(entryCapsule, workingCapsule => { - const workingOptions = {entry: slots.content}; - - if (!html.isBlank(slots.rereleaseTooltip)) { - workingCapsule += '.rerelease'; - workingOptions.rerelease = - relations.textWithTooltip.slots({ - attributes: {class: 'rerelease'}, - text: language.$(entryCapsule, 'rerelease.term'), - tooltip: slots.rereleaseTooltip, - }); - - return language.$(workingCapsule, workingOptions); - } - - if (!html.isBlank(slots.firstReleaseTooltip)) { - workingCapsule += '.firstRelease'; - workingOptions.firstRelease = - relations.textWithTooltip.slots({ - attributes: {class: 'first-release'}, - text: language.$(entryCapsule, 'firstRelease.term'), - tooltip: slots.firstReleaseTooltip, - }); - - return language.$(workingCapsule, workingOptions); - } - - let anyAccent = false; - - if (!empty(slots.otherArtistLinks)) { - anyAccent = true; - workingCapsule += '.withArtists'; - workingOptions.artists = - language.formatConjunctionList(slots.otherArtistLinks); - } - - if (!html.isBlank(slots.annotation)) { - anyAccent = true; - workingCapsule += '.withAnnotation'; - workingOptions.annotation = slots.annotation; - } - - if (anyAccent) { - return language.$(workingCapsule, workingOptions); - } else { - return slots.content; - } - }))), + html.tags([ + language.encapsulate(entryCapsule, workingCapsule => { + const workingOptions = {entry: slots.content}; + + if (!html.isBlank(slots.rereleaseTooltip)) { + workingCapsule += '.rerelease'; + workingOptions.rerelease = + relations.textWithTooltip.slots({ + attributes: {class: 'rerelease'}, + text: language.$(entryCapsule, 'rerelease.term'), + tooltip: slots.rereleaseTooltip, + }); + + return language.$(workingCapsule, workingOptions); + } + + if (!html.isBlank(slots.firstReleaseTooltip)) { + workingCapsule += '.firstRelease'; + workingOptions.firstRelease = + relations.textWithTooltip.slots({ + attributes: {class: 'first-release'}, + text: language.$(entryCapsule, 'firstRelease.term'), + tooltip: slots.firstReleaseTooltip, + }); + + return language.$(workingCapsule, workingOptions); + } + + let anyAccent = false; + + if (!empty(slots.otherArtistLinks)) { + anyAccent = true; + workingCapsule += '.withArtists'; + workingOptions.artists = + language.formatConjunctionList(slots.otherArtistLinks); + } + + if (!html.isBlank(slots.annotation)) { + anyAccent = true; + workingCapsule += '.withAnnotation'; + workingOptions.annotation = slots.annotation; + } + + if (anyAccent) { + return language.$(workingCapsule, workingOptions); + } else { + return slots.content; + } + }), + + html.tag('span', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + slots.originDetails), + ]))), }; diff --git a/src/content/dependencies/generateArtistInfoPageChunkedList.js b/src/content/dependencies/generateArtistInfoPageChunkedList.js index e7915ab7..54577885 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageChunkedList.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { groupInfo: { type: 'html', diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js index d0c5e14e..caec58d6 100644 --- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js @@ -7,18 +7,6 @@ import { } from '#sort'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageChunkItem', - 'linkAlbum', - 'linkFlash', - 'linkFlashAct', - 'linkTrack', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query(artist, filterWikiEditorCommentary) { const processEntry = ({ thing, @@ -43,6 +31,7 @@ export default { flash, annotation: entry.annotation, + annotationParts: entry.annotationParts, }, }); @@ -88,10 +77,10 @@ export default { thing.commentary .filter(entry => entry.artists.includes(artist)) - .filter(({annotation}) => + .filter(entry => (filterWikiEditorCommentary - ? annotation?.match(/^wiki editor/i) - : !annotation?.match(/^wiki editor/i))) + ? entry.isWikiEditorCommentary + : !entry.isWikiEditorCommentary)) .map(entry => processEntry({thing, entry}))); @@ -184,11 +173,13 @@ export default { itemAnnotations: query.chunks .map(({chunk}) => chunk - .map(({annotation}) => + .map(entry => relation('transformContent', (filterWikiEditorCommentary - ? annotation?.replace(/^wiki editor(, )?/i, '') - : annotation)))), + ? entry.annotationParts + .filter(part => part !== 'wiki editor') + .join(', ') + : entry.annotation)))), }), data: (query, _artist, _filterWikiEditorCommentary) => ({ diff --git a/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js b/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js index f86dead7..1d498b9f 100644 --- a/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js +++ b/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js @@ -1,19 +1,19 @@ -import {sortChronologically} from '#sort'; +import {sortAlbumsTracksChronologically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateTooltip', - 'linkOtherReleaseOnArtistInfoPage', - ], +query: (track, artist) => ({ + rereleases: + sortAlbumsTracksChronologically( + track.otherReleases.filter(track => { + const contribs = [ + ...track.artistContribs, + ...track.contributorContribs, + ]; - extraDependencies: ['html', 'language'], - - query: (track) => ({ - rereleases: - sortChronologically(track.allReleases).slice(1), - }), + return contribs.some(contrib => contrib.artist === artist); + })), +}), relations: (relation, query, track, artist) => ({ tooltip: diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunk.js b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js index 8aa7223a..acdb9897 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunk.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js @@ -1,10 +1,4 @@ export default { - contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageFlashesChunkItem', - 'linkFlashAct', - ], - relations: (relation, flashAct, contribs) => ({ template: relation('generateArtistInfoPageChunk'), diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js index e4908bf9..36d7945d 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js @@ -1,8 +1,4 @@ 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 diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js index b347faf5..762386a2 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js @@ -3,13 +3,6 @@ import {sortContributionsChronologically, sortFlashesChronologically} import {chunkByConditions, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageFlashesChunk', - ], - - extraDependencies: ['wikiData'], - sprawl: ({wikiInfo}) => ({ enableFlashesAndGames: wikiInfo.enableFlashesAndGames, diff --git a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js index dcee9c00..afb61c33 100644 --- a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js +++ b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js @@ -1,8 +1,6 @@ import {unique} from '#sugar'; export default { - contentDependencies: ['linkArtist'], - query(contribs) { const associatedContributionsByOtherArtists = contribs diff --git a/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js b/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js index 1d849919..bf5fe616 100644 --- a/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js +++ b/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js @@ -1,18 +1,22 @@ -import {sortChronologically} from '#sort'; +import {sortAlbumsTracksChronologically} from '#sort'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateTooltip', - 'linkOtherReleaseOnArtistInfoPage' - ], + query(track, artist) { + const query = {}; - extraDependencies: ['html', 'language'], + query.firstRelease = + sortAlbumsTracksChronologically(track.allReleases)[0]; - query: (track) => ({ - firstRelease: - sortChronologically(track.allReleases)[0], - }), + const contribs = [ + ...query.firstRelease.artistContribs, + ...query.firstRelease.contributorContribs, + ]; + + query.creditedOnFirstRelease = + contribs.some(contrib => contrib.artist === artist); + + return query; + }, relations: (relation, query, track, artist) => ({ tooltip: @@ -22,10 +26,15 @@ export default { relation('generateColorStyleAttribute', track.color), firstReleaseLink: - relation('linkOtherReleaseOnArtistInfoPage', query.firstRelease, artist), + (query.creditedOnFirstRelease + ? relation('linkOtherReleaseOnArtistInfoPage', query.firstRelease, artist) + : relation('linkTrackAsRelease', query.firstRelease)), }), - data: (query, track) => ({ + data: (query, track, artist) => ({ + artistName: + artist.name, + rereleaseDate: track.dateFirstReleased ?? track.album.date, @@ -33,6 +42,9 @@ export default { firstReleaseDate: query.firstRelease.dateFirstReleased ?? query.firstRelease.album.date, + + creditedOnFirstRelease: + query.creditedOnFirstRelease, }), generate: (data, relations, {html, language}) => @@ -56,6 +68,15 @@ export default { approximate: true, absolute: true, }), + + !data.creditedOnFirstRelease && [ + html.tag('hr', {class: 'cute'}), + + html.tag('span', {class: 'not-credited-on-first-release'}, + language.$(capsule, 'notCreditedOnFirstRelease', { + artist: data.artistName, + })), + ], ], })), }; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunk.js b/src/content/dependencies/generateArtistInfoPageTracksChunk.js index f6d70901..3e4cc4e9 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunk.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js @@ -2,12 +2,6 @@ import {unique} from '#sugar'; import {getTotalDuration} from '#wiki-data'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageTracksChunkItem', - 'linkAlbum', - ], - relations: (relation, artist, album, trackContribLists) => ({ template: relation('generateArtistInfoPageChunk'), diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js index a42d6fee..e976c57f 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js @@ -1,18 +1,8 @@ -import {sortChronologically} from '#sort'; +import {sortAlbumsTracksChronologically} from '#sort'; import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageFirstReleaseTooltip', - 'generateArtistInfoPageOtherArtistLinks', - 'generateArtistInfoPageRereleaseTooltip', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - - query (_artist, contribs) { + query(artist, contribs) { const query = {}; // TODO: Very mysterious what to do if the set of contributions is, @@ -22,11 +12,11 @@ export default { const creditedAsArtist = contribs - .some(contrib => contrib.isArtistContribution); + .some(contrib => contrib.thingProperty === 'artistContribs'); const creditedAsContributor = contribs - .some(contrib => contrib.isContributorContribution); + .some(contrib => contrib.thingProperty === 'contributorContribs'); const annotatedContribs = contribs @@ -34,11 +24,11 @@ export default { const annotatedArtistContribs = annotatedContribs - .filter(contrib => contrib.isArtistContribution); + .filter(contrib => contrib.thingProperty === 'artistContribs'); const annotatedContributorContribs = annotatedContribs - .filter(contrib => contrib.isContributorContribution); + .filter(contrib => contrib.thingProperty === 'contributorContribs'); // Don't display annotations associated with crediting in the // Contributors field if the artist is also credited as an Artist @@ -73,16 +63,23 @@ export default { // different - and it's the latter that determines whether the // track is a rerelease! const allReleasesChronologically = - sortChronologically(query.track.allReleases); + sortAlbumsTracksChronologically(query.track.allReleases); query.isFirstRelease = allReleasesChronologically[0] === query.track; - query.isRerelease = + query.isLaterRelease = allReleasesChronologically[0] !== query.track; - query.hasOtherReleases = - !empty(query.track.otherReleases); + query.hasOtherCreditedReleases = + query.track.otherReleases.some(track => { + const contribs = [ + ...track.artistContribs, + ...track.contributorContribs, + ]; + + return contribs.some(contrib => contrib.artist === artist); + }); return query; }, @@ -98,12 +95,12 @@ export default { relation('generateArtistInfoPageOtherArtistLinks', contribs), rereleaseTooltip: - (query.isRerelease + (query.isLaterRelease ? relation('generateArtistInfoPageRereleaseTooltip', query.track, artist) : null), firstReleaseTooltip: - (query.isFirstRelease && query.hasOtherReleases + (query.isFirstRelease && query.hasOtherCreditedReleases ? relation('generateArtistInfoPageFirstReleaseTooltip', query.track, artist) : null), }), diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index 84eb29ac..15588ed3 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -4,11 +4,6 @@ import {stitchArrays} from '#sugar'; import {chunkArtistTrackContributions} from '#wiki-data'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageTracksChunk', - ], - query(artist) { const query = {}; diff --git a/src/content/dependencies/generateArtistNavLinks.js b/src/content/dependencies/generateArtistNavLinks.js index 1b4b6eca..69ae3e19 100644 --- a/src/content/dependencies/generateArtistNavLinks.js +++ b/src/content/dependencies/generateArtistNavLinks.js @@ -1,14 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'linkArtist', - 'linkArtistGallery', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ enableListings: wikiInfo.enableListings, @@ -34,6 +26,9 @@ export default { (query.hasGallery ? relation('linkArtistGallery', artist) : null), + + artistRollingWindowLink: + relation('linkArtistRollingWindow', artist), }), data: (_query, sprawl) => ({ @@ -45,7 +40,7 @@ export default { showExtraLinks: {type: 'boolean', default: false}, currentExtra: { - validate: v => v.is('gallery'), + validate: v => v.is('gallery', 'rolling-window'), }, }, @@ -79,6 +74,7 @@ export default { }), slots.showExtraLinks && + slots.currentExtra !== 'rolling-window' && relations.artistGalleryLink?.slots({ attributes: [ slots.currentExtra === 'gallery' && @@ -87,6 +83,12 @@ export default { content: language.$('misc.nav.gallery'), }), + + slots.currentExtra === 'rolling-window' && + relations.artistRollingWindowLink.slots({ + attributes: {class: 'current'}, + content: language.$('misc.nav.rollingWindow'), + }), ], }), }, diff --git a/src/content/dependencies/generateArtistRollingWindowPage.js b/src/content/dependencies/generateArtistRollingWindowPage.js new file mode 100644 index 00000000..aafd1b55 --- /dev/null +++ b/src/content/dependencies/generateArtistRollingWindowPage.js @@ -0,0 +1,418 @@ +import {sortAlbumsTracksChronologically} from '#sort'; +import Thing from '#thing'; + +import { + chunkByConditions, + filterMultipleArrays, + empty, + sortMultipleArrays, + stitchArrays, + unique, +} from '#sugar'; + +export default { + sprawl: ({groupCategoryData}) => ({ + groupCategoryData, + }), + + query(sprawl, artist) { + const query = {}; + + const musicContributions = + artist.musicContributions + .filter(contrib => contrib.date); + + const artworkContributions = + artist.artworkContributions + .filter(contrib => + contrib.date && + contrib.thingProperty !== 'wallpaperArtistContribs' && + contrib.thingProperty !== 'bannerArtistContribs'); + + const musicThings = + musicContributions + .map(contrib => contrib.thing); + + const artworkThings = + artworkContributions + .map(contrib => contrib.thing.thing); + + const musicContributionDates = + musicContributions + .map(contrib => contrib.date); + + const artworkContributionDates = + artworkContributions + .map(contrib => contrib.date); + + const musicContributionKinds = + musicContributions + .map(() => 'music'); + + const artworkContributionKinds = + artworkContributions + .map(() => 'artwork'); + + const allThings = [ + ...artworkThings, + ...musicThings, + ]; + + const allContributionDates = [ + ...artworkContributionDates, + ...musicContributionDates, + ]; + + const allContributionKinds = [ + ...artworkContributionKinds, + ...musicContributionKinds, + ]; + + const sortedThings = + sortAlbumsTracksChronologically(allThings.slice(), {latestFirst: true}); + + sortMultipleArrays( + allThings, + allContributionDates, + allContributionKinds, + (thing1, thing2) => + sortedThings.indexOf(thing1) - + sortedThings.indexOf(thing2)); + + const sourceIndices = + Array.from({length: allThings.length}, (_, i) => i); + + const sourceChunks = + chunkByConditions(sourceIndices, [ + (index1, index2) => + allThings[index1] !== + allThings[index2], + ]); + + const indicesTo = array => index => array[index]; + + query.things = + sourceChunks + .map(chunks => allThings[chunks[0]]); + + query.thingGroups = + query.things.map(thing => + (thing.constructor[Thing.referenceType] === 'album' + ? thing.groups + : thing.constructor[Thing.referenceType] === 'track' + ? thing.album.groups + : null)); + + query.thingContributionDates = + sourceChunks + .map(indices => indices + .map(indicesTo(allContributionDates))); + + query.thingContributionKinds = + sourceChunks + .map(indices => indices + .map(indicesTo(allContributionKinds))); + + // Matches the "kind" dropdown. + const kinds = ['artwork', 'music', 'flash']; + + const allKinds = + unique(query.thingContributionKinds.flat(2)); + + query.kinds = + kinds + .filter(kind => allKinds.includes(kind)); + + query.firstKind = + query.kinds.at(0); + + query.thingArtworks = + stitchArrays({ + thing: query.things, + kinds: query.thingContributionKinds, + }).map(({thing, kinds}) => + (kinds.includes('artwork') + ? (thing.coverArtworks ?? thing.trackArtworks ?? []) + .find(artwork => artwork.artistContribs + .some(contrib => contrib.artist === artist)) + : (thing.coverArtworks ?? thing.trackArtworks)?.[0] ?? + thing.album?.coverArtworks[0] ?? + null)); + + const allGroups = + unique(query.thingGroups.flat()); + + query.groupCategories = + sprawl.groupCategoryData.slice(); + + query.groupCategoryGroups = + sprawl.groupCategoryData + .map(category => category.groups + .filter(group => allGroups.includes(group))); + + filterMultipleArrays( + query.groupCategories, + query.groupCategoryGroups, + (_category, groups) => !empty(groups)); + + const groupsMatchingFirstKind = + unique( + stitchArrays({ + thing: query.things, + groups: query.thingGroups, + kinds: query.thingContributionKinds, + }).filter(({kinds}) => kinds.includes(query.firstKind)) + .flatMap(({groups}) => groups)); + + query.firstGroup = + sprawl.groupCategoryData + .flatMap(category => category.groups) + .find(group => groupsMatchingFirstKind.includes(group)); + + query.firstGroupCategory = + query.firstGroup.category; + + return query; + }, + + relations: (relation, query, sprawl, artist) => ({ + layout: + relation('generatePageLayout'), + + artistNavLinks: + relation('generateArtistNavLinks', artist), + + sourceGrid: + relation('generateCoverGrid'), + + sourceGridImages: + query.thingArtworks + .map(artwork => relation('image', artwork)), + + sourceGridLinks: + query.things + .map(thing => relation('linkAnythingMan', thing)), + }), + + data: (query, sprawl, artist) => ({ + name: + artist.name, + + categoryGroupDirectories: + query.groupCategoryGroups + .map(groups => groups + .map(group => group.directory)), + + categoryGroupNames: + query.groupCategoryGroups + .map(groups => groups + .map(group => group.name)), + + firstGroupCategoryIndex: + query.groupCategories + .indexOf(query.firstGroupCategory), + + firstGroupIndex: + stitchArrays({ + category: query.groupCategories, + groups: query.groupCategoryGroups, + }).find(({category}) => category === query.firstGroupCategory) + .groups + .indexOf(query.firstGroup), + + kinds: + query.kinds, + + sourceGridNames: + query.things + .map(thing => thing.name), + + sourceGridGroupDirectories: + query.thingGroups + .map(groups => groups + .map(group => group.directory)), + + sourceGridGroupNames: + query.thingGroups + .map(groups => groups + .map(group => group.name)), + + sourceGridContributionKinds: + query.thingContributionKinds, + + sourceGridContributionDates: + query.thingContributionDates, + }), + + generate: (data, relations, {html, language}) => + relations.layout.slots({ + title: + language.$('artistRollingWindowPage.title', { + artist: data.name, + }), + + mainClasses: ['top-index'], + mainContent: [ + html.tag('p', {id: 'timeframe-configuration'}, + language.$('artistRollingWindowPage.windowConfigurationLine', { + timeBefore: + language.$('artistRollingWindowPage.timeframe.months', { + input: + html.tag('input', {id: 'timeframe-months-before'}, + {type: 'number'}, + {value: 3, min: 0}), + }), + + timeAfter: + language.$('artistRollingWindowPage.timeframe.months', { + input: + html.tag('input', {id: 'timeframe-months-after'}, + {type: 'number'}, + {value: 3, min: 1}), + }), + + peek: + language.$('artistRollingWindowPage.timeframe.months', { + input: + html.tag('input', {id: 'timeframe-months-peek'}, + {type: 'number'}, + {value: 1, min: 0}), + }), + })), + + html.tag('p', {id: 'contribution-configuration'}, + language.$('artistRollingWindowPage.contributionConfigurationLine', { + kind: + html.tag('select', {id: 'contribution-kind'}, + data.kinds.map(kind => + html.tag('option', {value: kind}, + language.$('artistRollingWindowPage.contributionKind', kind)))), + + group: + html.tag('select', {id: 'contribution-group'}, [ + html.tag('option', {value: '-'}, + language.$('artistRollingWindowPage.contributionGroup.all')), + + stitchArrays({ + names: data.categoryGroupNames, + directories: data.categoryGroupDirectories, + }).map(({names, directories}, categoryIndex) => [ + html.tag('hr'), + + stitchArrays({name: names, directory: directories}) + .map(({name, directory}, groupIndex) => + html.tag('option', {value: directory}, + categoryIndex === data.firstGroupCategoryIndex && + groupIndex === data.firstGroupIndex && + {selected: true}, + + language.$('artistRollingWindowPage.contributionGroup.group', { + group: name, + }))), + ]), + ]), + })), + + html.tag('p', {id: 'timeframe-selection-info'}, [ + html.tag('span', {id: 'timeframe-selection-some'}, + {style: 'display: none'}, + + language.$('artistRollingWindowPage.timeframeSelectionLine', { + contributions: + html.tag('b', {id: 'timeframe-selection-contribution-count'}), + + timeframes: + html.tag('b', {id: 'timeframe-selection-timeframe-count'}), + + firstDate: + html.tag('b', {id: 'timeframe-selection-first-date'}), + + lastDate: + html.tag('b', {id: 'timeframe-selection-last-date'}), + })), + + html.tag('span', {id: 'timeframe-selection-none'}, + {style: 'display: none'}, + language.$('artistRollingWindowPage.timeframeSelectionLine.none')), + ]), + + html.tag('p', {id: 'timeframe-selection-control'}, + {style: 'display: none'}, + + language.$('artistRollingWindowPage.timeframeSelectionControl', { + timeframes: + html.tag('select', {id: 'timeframe-selection-menu'}), + + previous: + html.tag('a', {id: 'timeframe-selection-previous'}, + {href: '#'}, + language.$('artistRollingWindowPage.timeframeSelectionControl.previous')), + + next: + html.tag('a', {id: 'timeframe-selection-next'}, + {href: '#'}, + language.$('artistRollingWindowPage.timeframeSelectionControl.next')), + })), + + html.tag('div', {id: 'timeframe-source-area'}, [ + html.tag('p', {id: 'timeframe-empty'}, + {style: 'display: none'}, + language.$('artistRollingWindowPage.emptyTimeframeLine')), + + relations.sourceGrid.slots({ + attributes: {style: 'display: none'}, + + lazy: true, + + links: + relations.sourceGridLinks.map(link => + link.slot('attributes', {target: '_blank'})), + + names: + data.sourceGridNames, + + images: + relations.sourceGridImages, + + info: + stitchArrays({ + contributionKinds: data.sourceGridContributionKinds, + contributionDates: data.sourceGridContributionDates, + groupDirectories: data.sourceGridGroupDirectories, + groupNames: data.sourceGridGroupNames, + }).map(({ + contributionKinds, + contributionDates, + groupDirectories, + groupNames, + }) => [ + stitchArrays({ + directory: groupDirectories, + name: groupNames, + }).map(({directory, name}) => + html.tag('data', {class: 'contribution-group'}, + {value: directory}, + name)), + + stitchArrays({ + kind: contributionKinds, + date: contributionDates, + }).map(({kind, date}) => + html.tag('time', {class: `${kind}-contribution-date`}, + {datetime: date.toUTCString()}, + language.formatDate(date))), + ]), + }), + ]), + ], + + navLinkStyle: 'hierarchical', + navLinks: + relations.artistNavLinks + .slots({ + showExtraLinks: true, + currentExtra: 'rolling-window', + }) + .content, + }), +} diff --git a/src/content/dependencies/generateBackToAlbumLink.js b/src/content/dependencies/generateBackToAlbumLink.js index 6648b463..08d33348 100644 --- a/src/content/dependencies/generateBackToAlbumLink.js +++ b/src/content/dependencies/generateBackToAlbumLink.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkAlbum'], - extraDependencies: ['language'], - relations: (relation, track) => ({ trackLink: relation('linkAlbum', track), diff --git a/src/content/dependencies/generateBackToTrackLink.js b/src/content/dependencies/generateBackToTrackLink.js index 8677d811..90dfb6d5 100644 --- a/src/content/dependencies/generateBackToTrackLink.js +++ b/src/content/dependencies/generateBackToTrackLink.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkTrack'], - extraDependencies: ['language'], - relations: (relation, track) => ({ trackLink: relation('linkTrack', track), diff --git a/src/content/dependencies/generateBanner.js b/src/content/dependencies/generateBanner.js index 15eb08eb..509b15c2 100644 --- a/src/content/dependencies/generateBanner.js +++ b/src/content/dependencies/generateBanner.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'to'], - slots: { path: { validate: v => v.validateArrayItems(v.isString), diff --git a/src/content/dependencies/generateCollapsedContentEntrySection.js b/src/content/dependencies/generateCollapsedContentEntrySection.js new file mode 100644 index 00000000..aec5fe28 --- /dev/null +++ b/src/content/dependencies/generateCollapsedContentEntrySection.js @@ -0,0 +1,37 @@ +export default { + relations: (relation, entries, thing) => ({ + contentContentHeading: + relation('generateContentContentHeading', thing), + + entries: + entries + .map(entry => relation('generateCommentaryEntry', entry)), + }), + + slots: { + id: {type: 'string'}, + string: {type: 'string'}, + }, + + generate: (relations, slots, {html}) => + html.tag('details', + {[html.onlyIfContent]: true}, + + slots.id && [ + {class: 'memorable'}, + {'data-memorable-id': slots.id}, + ], + + [ + relations.contentContentHeading.slots({ + attributes: [ + slots.id && {id: slots.id}, + ], + + string: slots.string, + summary: true, + }), + + relations.entries, + ]), +}; diff --git a/src/content/dependencies/generateColorStyleAttribute.js b/src/content/dependencies/generateColorStyleAttribute.js index 03d95ac5..277ec434 100644 --- a/src/content/dependencies/generateColorStyleAttribute.js +++ b/src/content/dependencies/generateColorStyleAttribute.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateColorStyleVariables'], - extraDependencies: ['html'], - relations: (relation) => ({ colorVariables: relation('generateColorStyleVariables'), 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..b378fd1d --- /dev/null +++ b/src/content/dependencies/generateColorStyleTag.js @@ -0,0 +1,48 @@ +export default { + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + + variables: + relation('generateColorStyleVariables'), + }), + + data: (color) => ({ + color: + color ?? null, + }), + + slots: { + color: { + validate: v => v.isColor, + }, + }, + + generate(data, relations, slots, {html}) { + const color = + data.color ?? slots.color; + + if (!color) { + return html.blank(); + } + + return relations.styleTag.slots({ + attributes: [ + {class: 'color-style'}, + {'data-color': color}, + ], + + rules: [ + { + select: ':root', + declare: + relations.variables.slots({ + color, + context: 'page-root', + mode: 'declarations', + }).content, + }, + ], + }); + }, +}; diff --git a/src/content/dependencies/generateColorStyleVariables.js b/src/content/dependencies/generateColorStyleVariables.js index 5270dbe4..0865ed3e 100644 --- a/src/content/dependencies/generateColorStyleVariables.js +++ b/src/content/dependencies/generateColorStyleVariables.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'getColors'], - slots: { color: { validate: v => v.isColor, @@ -18,7 +16,7 @@ export default { }, mode: { - validate: v => v.is('style', 'property-list'), + validate: v => v.is('style', 'declarations'), default: 'style', }, }, @@ -50,15 +48,15 @@ export default { `--shadow-color: ${shadow}`, ]; - let selectedProperties; + let selectedDeclarations; switch (slots.context) { case 'any-content': - selectedProperties = anyContent; + selectedDeclarations = anyContent; break; case 'image-box': - selectedProperties = [ + selectedDeclarations = [ `--primary-color: ${primary}`, `--dim-color: ${dim}`, `--deep-color: ${deep}`, @@ -67,14 +65,14 @@ export default { break; case 'page-root': - selectedProperties = [ + selectedDeclarations = [ ...anyContent, `--page-primary-color: ${primary}`, ]; break; case 'primary-only': - selectedProperties = [ + selectedDeclarations = [ `--primary-color: ${primary}`, ]; break; @@ -82,10 +80,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/generateCommentaryContentHeading.js b/src/content/dependencies/generateCommentaryContentHeading.js new file mode 100644 index 00000000..691762aa --- /dev/null +++ b/src/content/dependencies/generateCommentaryContentHeading.js @@ -0,0 +1,43 @@ +import {empty} from '#sugar'; + +export default { + query: (thing) => ({ + entries: + (thing.isTrack + ? [...thing.commentary, ...thing.commentaryFromMainRelease] + : thing.commentary), + }), + + relations: (relation, _query, thing) => ({ + contentContentHeading: + relation('generateContentContentHeading', thing), + }), + + data: (query, _thing) => ({ + hasWikiEditorCommentary: + query.entries.some(entry => entry.isWikiEditorCommentary), + + onlyWikiEditorCommentary: + !empty(query.entries) && + query.entries.every(entry => entry.isWikiEditorCommentary), + + hasAnyCommentary: + !empty(query.entries), + }), + + generate: (data, relations, {language}) => + relations.contentContentHeading.slots({ + // It's #artist-commentary for legacy reasons... Sorry... + attributes: {id: 'artist-commentary'}, + + string: + language.encapsulate('misc.artistCommentary', capsule => + (data.onlyWikiEditorCommentary + ? language.encapsulate(capsule, 'onlyWikiCommentary') + : data.hasWikiEditorCommentary + ? language.encapsulate(capsule, 'withWikiCommentary') + : data.hasAnyCommentary + ? capsule + : null)), + }), +}; diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index c93020f3..38eb6b43 100644 --- a/src/content/dependencies/generateCommentaryEntry.js +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -1,25 +1,16 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateCommentaryEntryDate', - 'generateColorStyleAttribute', - 'linkArtist', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - 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: @@ -39,11 +30,16 @@ export default { relation('generateCommentaryEntryDate', entry), }), + data: (entry) => ({ + isWikiEditorCommentary: + entry.isWikiEditorCommentary, + }), + slots: { color: {validate: v => v.isColor}, }, - generate: (relations, slots, {html, language}) => + generate: (data, relations, slots, {html, language}) => language.encapsulate('misc.artistCommentary.entry', entryCapsule => html.tags([ html.tag('p', {class: 'commentary-entry-heading'}, @@ -107,6 +103,9 @@ export default { relations.colorStyle.clone() .slot('color', slots.color), + data.isWikiEditorCommentary && + {class: 'wiki-commentary'}, + relations.bodyContent.slot('mode', 'multiline')), ])), }; diff --git a/src/content/dependencies/generateCommentaryEntryDate.js b/src/content/dependencies/generateCommentaryEntryDate.js index f1cf5cb3..e924f244 100644 --- a/src/content/dependencies/generateCommentaryEntryDate.js +++ b/src/content/dependencies/generateCommentaryEntryDate.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateTextWithTooltip', 'generateTooltip'], - extraDependencies: ['html', 'language'], - relations: (relation, _entry) => ({ textWithTooltip: relation('generateTextWithTooltip'), diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js index d68ba42e..8cc30913 100644 --- a/src/content/dependencies/generateCommentaryIndexPage.js +++ b/src/content/dependencies/generateCommentaryIndexPage.js @@ -1,13 +1,11 @@ +import multilingualWordCount from 'word-count'; + import {sortChronologically} from '#sort'; import {accumulateSum, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generatePageLayout', 'linkAlbumCommentary'], - extraDependencies: ['html', 'language', 'wikiData'], - - sprawl({albumData}) { - return {albumData}; - }, + sprawl: ({albumData}) => + ({albumData}), query(sprawl) { const query = {}; @@ -21,44 +19,52 @@ export default { .filter(({commentary}) => commentary) .flatMap(({commentary}) => commentary)); - query.wordCounts = - entries.map(entries => - accumulateSum( - entries, - entry => entry.body.split(' ').length)); + query.bodies = + entries.map(entries => entries.map(entry => entry.body)); query.entryCounts = entries.map(entries => entries.length); - filterMultipleArrays(query.albums, query.wordCounts, query.entryCounts, - (album, wordCount, entryCount) => entryCount >= 1); + filterMultipleArrays(query.albums, query.bodies, query.entryCounts, + (album, bodies, entryCount) => entryCount >= 1); return query; }, - relations(relation, query) { - return { - layout: - relation('generatePageLayout'), + relations: (relation, query) => ({ + layout: + relation('generatePageLayout'), - albumLinks: - query.albums - .map(album => relation('linkAlbumCommentary', album)), - }; - }, + albumLinks: + query.albums + .map(album => relation('linkAlbumCommentary', album)), - data(query) { - return { - wordCounts: query.wordCounts, - entryCounts: query.entryCounts, + albumBodies: + query.bodies + .map(bodies => bodies + .map(body => relation('transformContent', body))), + }), - totalWordCount: accumulateSum(query.wordCounts), - totalEntryCount: accumulateSum(query.entryCounts), - }; - }, + data: (query) => ({ + entryCounts: query.entryCounts, + totalEntryCount: accumulateSum(query.entryCounts), + }), - generate: (data, relations, {html, language}) => - language.encapsulate('commentaryIndex', pageCapsule => + generate(data, relations, {html, language}) { + const wordCounts = + relations.albumBodies.map(bodies => + accumulateSum(bodies, body => + multilingualWordCount( + html.resolve( + body.slot('mode', 'multiline'), + {normalize: 'plain'})))); + + const totalWordCount = + accumulateSum(wordCounts); + + const {entryCounts, totalEntryCount} = data; + + return language.encapsulate('commentaryIndex', pageCapsule => relations.layout.slots({ title: language.$(pageCapsule, 'title'), @@ -69,11 +75,11 @@ export default { html.tag('p', language.$(pageCapsule, 'infoLine', { words: html.tag('b', - language.formatWordCount(data.totalWordCount, {unit: true})), + language.formatWordCount(totalWordCount, {unit: true})), entries: html.tag('b', - language.countCommentaryEntries(data.totalEntryCount, {unit: true})), + language.countCommentaryEntries(totalEntryCount, {unit: true})), })), language.encapsulate(pageCapsule, 'albumList', listCapsule => [ @@ -83,8 +89,8 @@ export default { html.tag('ul', stitchArrays({ albumLink: relations.albumLinks, - wordCount: data.wordCounts, - entryCount: data.entryCounts, + wordCount: wordCounts, + entryCount: entryCounts, }).map(({albumLink, wordCount, entryCount}) => html.tag('li', language.$(listCapsule, 'item', { @@ -100,5 +106,6 @@ export default { {auto: 'home'}, {auto: 'current'}, ], - })), + })); + }, }; diff --git a/src/content/dependencies/generateContentContentHeading.js b/src/content/dependencies/generateContentContentHeading.js new file mode 100644 index 00000000..9ed2d9f0 --- /dev/null +++ b/src/content/dependencies/generateContentContentHeading.js @@ -0,0 +1,73 @@ +export default { + relations: (relation, _thing) => ({ + contentHeading: + relation('generateContentHeading'), + }), + + data: (thing) => ({ + name: + (thing + ? thing.name + : null), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + string: { + type: 'string', + }, + + summary: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots, {html, language}) => + relations.contentHeading.slots({ + attributes: slots.attributes, + + title: + (() => { + if (!slots.string) return html.blank(); + + const options = {}; + + if (slots.summary) { + options.cue = + html.tag('span', {class: 'cue'}, + language.$(slots.string, 'cue')); + } + + if (data.name) { + options.thing = html.tag('i', data.name); + } + + if (slots.summary) { + return html.tags([ + html.tag('span', {class: 'when-open'}, + language.$(slots.string, options)), + + html.tag('span', {class: 'when-collapsed'}, + language.$(slots.string, 'collapsed', options)), + ]); + } else { + return language.$(slots.string, options); + } + })(), + + stickyTitle: + (slots.string + ? language.$(slots.string, 'sticky') + : html.blank()), + + tag: + (slots.summary + ? 'summary' + : 'p'), + }), +}; diff --git a/src/content/dependencies/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js index f52bc043..a7cf201f 100644 --- a/src/content/dependencies/generateContentHeading.js +++ b/src/content/dependencies/generateContentHeading.js @@ -1,7 +1,5 @@ export default { extraDependencies: ['html'], - contentDependencies: ['generateColorStyleAttribute'], - relations: (relation) => ({ colorStyle: relation('generateColorStyleAttribute'), }), diff --git a/src/content/dependencies/generateContributionList.js b/src/content/dependencies/generateContributionList.js index d1c3de0f..3716bcd6 100644 --- a/src/content/dependencies/generateContributionList.js +++ b/src/content/dependencies/generateContributionList.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkContribution'], - extraDependencies: ['html'], - relations: (relation, contributions) => ({ contributionLinks: contributions diff --git a/src/content/dependencies/generateContributionTooltip.js b/src/content/dependencies/generateContributionTooltip.js index 3a31014d..fd16371b 100644 --- a/src/content/dependencies/generateContributionTooltip.js +++ b/src/content/dependencies/generateContributionTooltip.js @@ -1,21 +1,79 @@ -export default { - contentDependencies: [ - 'generateContributionTooltipChronologySection', - 'generateContributionTooltipExternalLinkSection', - 'generateTooltip', - ], +function compareReleaseContributions(a, b) { + if (a === b) { + return true; + } + + const {previous: aPrev, next: aNext} = getSiblings(a); + const {previous: bPrev, next: bNext} = getSiblings(b); + + const effective = contrib => + (contrib?.thing.isAlbum && contrib.thing.style === 'single' + ? contrib.thing.tracks[0] + : contrib?.thing); + + return ( + effective(aPrev) === effective(bPrev) && + effective(aNext) === effective(bNext) + ); +} - extraDependencies: ['html'], +function getSiblings(contribution) { + let previous = contribution; + while (previous && previous.thing === contribution.thing) { + previous = previous.previousBySameArtist; + } - relations: (relation, contribution) => ({ + let next = contribution; + while (next && next.thing === contribution.thing) { + next = next.nextBySameArtist; + } + + return {previous, next}; +} + +export default { + query: (contribution) => ({ + albumArtistContribution: + (contribution.thing.isTrack + ? contribution.thing.album.artistContribs + .find(artistContrib => artistContrib.artist === contribution.artist) + : null), + }), + + relations: (relation, query, contribution) => ({ tooltip: relation('generateTooltip'), externalLinkSection: relation('generateContributionTooltipExternalLinkSection', contribution), - chronologySection: + ownChronologySection: relation('generateContributionTooltipChronologySection', contribution), + + artistReleaseChronologySection: + (query.albumArtistContribution + ? relation('generateContributionTooltipChronologySection', + query.albumArtistContribution) + : null), + }), + + data: (query, contribution) => ({ + artistName: + contribution.artist.name, + + isAlbumArtistContribution: + contribution.thing.isAlbum && + contribution.thingProperty === 'artistContribs', + + isSingleTrackArtistContribution: + contribution.thing.isTrack && + contribution.thingProperty === 'artistContribs' && + contribution.thing.album.style === 'single', + + artistReleaseChronologySectionDiffers: + (query.albumArtistContribution + ? !compareReleaseContributions(contribution, query.albumArtistContribution) + : null), }), slots: { @@ -25,24 +83,64 @@ export default { 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, - }), - ], - }), + generate: (data, relations, slots, {html, language}) => + language.encapsulate('misc.artistLink', capsule => + relations.tooltip.slots({ + attributes: + {class: 'contribution-tooltip'}, + + contentAttributes: { + [html.joinChildren]: + html.tag('span', {class: 'tooltip-divider'}), + }, + + content: [ + slots.showExternalLinks && + relations.externalLinkSection, + + slots.showChronology && + language.encapsulate(capsule, 'chronology', capsule => { + const chronologySections = []; + + if (data.isAlbumArtistContribution) { + relations.ownChronologySection.setSlots({ + kind: 'release', + heading: + language.$(capsule, 'heading.artistReleases', { + artist: data.artistName, + }), + }); + } else { + relations.ownChronologySection.setSlot('kind', slots.chronologyKind); + } + + if ( + data.isSingleTrackArtistContribution && + relations.artistReleaseChronologySection + ) { + relations.artistReleaseChronologySection.setSlot('kind', 'release'); + + relations.artistReleaseChronologySection.setSlot('heading', + language.$(capsule, 'heading.artistReleases', { + artist: data.artistName, + })); + + chronologySections.push(relations.artistReleaseChronologySection); + + if (data.artistReleaseChronologySectionDiffers) { + relations.ownChronologySection.setSlot('heading', + language.$(capsule, 'heading.artistTracks', { + artist: data.artistName, + })); + + chronologySections.push(relations.ownChronologySection); + } + } else { + chronologySections.push(relations.ownChronologySection); + } + + return chronologySections; + }), + ], + })), }; diff --git a/src/content/dependencies/generateContributionTooltipChronologySection.js b/src/content/dependencies/generateContributionTooltipChronologySection.js index 78c9051c..e4b9bfda 100644 --- a/src/content/dependencies/generateContributionTooltipChronologySection.js +++ b/src/content/dependencies/generateContributionTooltipChronologySection.js @@ -1,20 +1,33 @@ -export default { - contentDependencies: ['linkAnythingMan'], - extraDependencies: ['html', 'language'], +function getName(thing) { + if (!thing) { + return null; + } - query(contribution) { - let previous = contribution; - while (previous && previous.thing === contribution.thing) { - previous = previous.previousBySameArtist; - } + if (thing.isArtwork) { + return thing.thing.name; + } - let next = contribution; - while (next && next.thing === contribution.thing) { - next = next.nextBySameArtist; - } + return thing.name; +} - return {previous, next}; - }, +function getSiblings(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}; +} + +export default { + query: (contribution) => ({ + ...getSiblings(contribution), + }), relations: (relation, query, _contribution) => ({ previousLink: @@ -30,34 +43,26 @@ export default { data: (query, _contribution) => ({ previousName: - (query.previous - ? query.previous.thing.name - : null), + getName(query.previous?.thing), nextName: - (query.next - ? query.next.thing.name - : null), + getName(query.next?.thing), }), slots: { - kind: { - validate: v => - v.is( - 'album', - 'bannerArt', - 'coverArt', - 'flash', - 'track', - 'trackArt', - 'trackContribution', - 'wallpaperArt'), - }, + heading: {type: 'html', mutable: false}, + kind: {type: 'string'}, }, generate: (data, relations, slots, {html, language}) => language.encapsulate('misc.artistLink.chronology', capsule => html.tags([ + html.tag('span', {class: 'chronology-heading'}, + {[html.onlyIfContent]: true}, + {[html.onlyIfSiblings]: true}, + + slots.heading), + html.tags([ relations.previousLink?.slots({ attributes: {class: 'chronology-link'}, diff --git a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js index 4f9a23ed..210db1e9 100644 --- a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js +++ b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js @@ -1,14 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateExternalHandle', - 'generateExternalIcon', - 'generateExternalPlatform', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, contribution) => ({ icons: contribution.artist.urls diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index 06972d6b..616b3c95 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -1,11 +1,49 @@ export default { - contentDependencies: ['image'], - extraDependencies: ['html'], + relations: (relation, artwork) => ({ + colorStyleAttribute: + relation('generateColorStyleAttribute'), + + image: + relation('image', artwork), + + originDetails: + relation('generateCoverArtworkOriginDetails', artwork), + + artTagDetails: + relation('generateCoverArtworkArtTagDetails', artwork), + + artistDetails: + relation('generateCoverArtworkArtistDetails', artwork), + + referenceDetails: + relation('generateCoverArtworkReferenceDetails', artwork), + }), + + data: (artwork) => ({ + attachAbove: + artwork.attachAbove, + + attachedArtworkIsMainArtwork: + (artwork.attachAbove + ? artwork.attachedArtwork.isMainArtwork + : null), + + color: + artwork.thing.color ?? null, + + dimensions: + artwork.dimensions, + + style: + artwork.style, + }), slots: { - image: { - type: 'html', - mutable: true, + alt: {type: 'string'}, + + color: { + validate: v => v.anyOf(v.isBoolean, v.isColor), + default: false, }, mode: { @@ -13,13 +51,10 @@ export default { default: 'primary', }, - dimensions: { - validate: v => v.isDimensions, - }, - - warnings: { - validate: v => v.looseArrayOf(v.isString), - }, + showOriginDetails: {type: 'boolean', default: false}, + showArtTagDetails: {type: 'boolean', default: false}, + showArtistDetails: {type: 'boolean', default: false}, + showReferenceDetails: {type: 'boolean', default: false}, details: { type: 'html', @@ -27,60 +62,120 @@ export default { }, }, - generate(slots, {html}) { + generate(data, relations, slots, {html}) { + const {image} = relations; + + const imgAttributes = html.attributes(); + + if (data.style) { + imgAttributes.add('style', data.style.split('\n').join(' ')); + } + + image.setSlot('imgAttributes', imgAttributes); + + image.setSlot('alt', slots.alt); + const square = - (slots.dimensions - ? slots.dimensions[0] === slots.dimensions[1] + (data.dimensions + ? data.dimensions[0] === data.dimensions[1] : true); - const sizeSlots = - (square - ? {square: true} - : {dimensions: slots.dimensions}); - - switch (slots.mode) { - case 'primary': - return html.tags([ - slots.image.slots({ - thumb: 'medium', - reveal: true, - link: true, - - warnings: slots.warnings, - ...sizeSlots, - }), - - slots.details, - ]); - - case 'thumbnail': - return ( - slots.image.slots({ - thumb: 'small', - reveal: false, - link: false, - - warnings: slots.warnings, - ...sizeSlots, - })); - - case 'commentary': - return ( - slots.image.slots({ - thumb: 'medium', - reveal: true, - link: true, - lazy: true, - - warnings: slots.warnings, - ...sizeSlots, - - attributes: - {class: 'commentary-art'}, - })); - - default: - return html.blank(); + if (square) { + image.setSlot('square', true); + } else { + image.setSlot('dimensions', data.dimensions); } + + const attributes = html.attributes(); + + let color = null; + if (typeof slots.color === 'boolean') { + if (slots.color) { + color = data.color; + } + } else if (slots.color) { + color = slots.color; + } + + if (color) { + relations.colorStyleAttribute.setSlot('color', color); + attributes.add(relations.colorStyleAttribute); + } + + return html.tags([ + data.attachAbove && + html.tag('div', {class: 'cover-artwork-joiner'}), + + html.tag('div', {class: 'cover-artwork'}, + slots.mode === 'commentary' && + {class: 'commentary-art'}, + + data.attachAbove && + data.attachedArtworkIsMainArtwork && + {class: 'attached-artwork-is-main-artwork'}, + + attributes, + + (slots.mode === 'primary' + ? [ + relations.image.slots({ + thumb: 'medium', + reveal: true, + link: true, + + responsiveThumb: true, + responsiveSizes: + // No clamp(), min(), or max() here because Safari. + // The boundaries here are mostly experimental, apart from + // the ones which flat-out switch layouts. + + // Layout - Thin (phones) + // Most of viewport width + '(max-width: 600px) 90vw,\n' + + + // Layout - Medium + // Sidebar is hidden; content area is by definition + // most of the viewport + '(max-width: 640px) 220px,\n' + + '(max-width: 800px) 36vw,\n' + + '(max-width: 850px) 280px,\n' + + + // Layout - Wide + // Sidebar is visible; content area has its own maximum + // Assume the sidebar is at minimum width + '(max-width: 880px) 220px,\n' + + '(max-width: 1050pz) calc(0.40 * (90vw - 150px - 10px)),\n' + + '280px', + }), + + 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 index b4edbbdd..50571a4f 100644 --- a/src/content/dependencies/generateCoverArtworkArtTagDetails.js +++ b/src/content/dependencies/generateCoverArtworkArtTagDetails.js @@ -1,22 +1,39 @@ -import {stitchArrays} from '#sugar'; +import {compareArrays, empty, stitchArrays} from '#sugar'; -export default { - contentDependencies: ['linkArtTagGallery'], - extraDependencies: ['html'], +function linkable(tag) { + return !tag.isContentWarning; +} - query: (artTags) => ({ +export default { + query: (artwork) => ({ linkableArtTags: - artTags - .filter(tag => !tag.isContentWarning), + artwork.artTags.filter(linkable), + + mainArtworkLinkableArtTags: + (artwork.mainArtwork + ? artwork.mainArtwork.artTags.filter(linkable) + : null), }), - relations: (relation, query, _artTags) => ({ + relations: (relation, query, _artwork) => ({ artTagLinks: query.linkableArtTags .map(tag => relation('linkArtTagGallery', tag)), }), - data: (query, _artTags) => { + 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(); @@ -28,23 +45,28 @@ export default { } } - const preferShortName = + data.preferShortName = query.linkableArtTags .map(artTag => !duplicateShortNames.has(artTag.nameShort)); - return {preferShortName}; + return data; }, - generate: (data, relations, {html}) => - html.tag('ul', {class: 'image-details'}, - {[html.onlyIfContent]: true}, + generate: (data, relations, {html, language}) => + language.encapsulate('misc.coverArtwork', capsule => + html.tag('ul', {class: 'image-details'}, + {[html.onlyIfContent]: true}, - {class: 'art-tag-details'}, + {class: 'art-tag-details'}, - stitchArrays({ - artTagLink: relations.artTagLinks, - preferShortName: data.preferShortName, - }).map(({artTagLink, preferShortName}) => - html.tag('li', - artTagLink.slot('preferShortName', preferShortName)))), + (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 index 5b235353..2773c6fc 100644 --- a/src/content/dependencies/generateCoverArtworkArtistDetails.js +++ b/src/content/dependencies/generateCoverArtworkArtistDetails.js @@ -1,10 +1,7 @@ export default { - contentDependencies: ['linkArtistGallery'], - extraDependencies: ['html', 'language'], - - relations: (relation, contributions) => ({ + relations: (relation, artwork) => ({ artistLinks: - contributions + artwork.artistContribs .map(contrib => contrib.artist) .map(artist => relation('linkArtistGallery', artist)), @@ -17,6 +14,8 @@ export default { {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..e489eea6 --- /dev/null +++ b/src/content/dependencies/generateCoverArtworkOriginDetails.js @@ -0,0 +1,173 @@ +export default { + query: (artwork) => ({ + 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: + (artwork.thing.isAlbum + ? relation('linkAlbum', artwork.thing) + : null), + + datetimestamp: + relation('generateAbsoluteDatetimestamp', + artwork.date, + artwork.thing.date), + }), + + + data: (query, artwork) => ({ + label: + artwork.label, + + forAlbum: + artwork.thing.isAlbum, + + forSingleStyleAlbum: + artwork.thing.isAlbum && + artwork.thing.style === 'single', + + showFilename: + artwork.showFilename, + }), + + generate: (data, relations, {html, language, pagePath}) => + language.encapsulate('misc.coverArtwork', capsule => + html.tag('p', {class: 'image-details'}, + {[html.onlyIfContent]: true}, + + {class: 'origin-details'}, + + (() => { + relations.datetimestamp.setSlot('style', 'year-difference'); + + const artworkBy = + language.encapsulate(capsule, 'artworkBy', workingCapsule => { + const workingOptions = {}; + + if (data.label) { + workingCapsule += '.customLabel'; + workingOptions.label = data.label; + } + + if (!html.isBlank(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.forAlbum && + !data.forSingleStyleAlbum && + 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) && !html.isBlank(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 (!html.isBlank(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 originDetailsLine = + html.tag('span', {class: 'origin-details-line'}, + {[html.onlyIfContent]: true}, + + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + })); + + const filenameLine = + html.tag('span', {class: 'filename-line'}, + {[html.onlyIfContent]: true}, + + html.tag('code', {class: 'filename'}, + {[html.onlyIfContent]: true}, + + language.sanitize(data.showFilename))); + + return [ + html.tags([ + artworkBy, + trackArtFromAlbum, + source, + label, + year, + ], {[html.joinChildren]: html.tag('br')}), + + originDetailsLine, + filenameLine, + ]; + })())), +}; diff --git a/src/content/dependencies/generateCoverArtworkReferenceDetails.js b/src/content/dependencies/generateCoverArtworkReferenceDetails.js index 006b2b4b..d4e4e7e4 100644 --- a/src/content/dependencies/generateCoverArtworkReferenceDetails.js +++ b/src/content/dependencies/generateCoverArtworkReferenceDetails.js @@ -1,20 +1,21 @@ export default { - extraDependencies: ['html', 'language'], + relations: (relation, artwork) => ({ + referencedArtworksLink: + relation('linkReferencedArtworks', artwork), - data: (referenced, referencedBy) => ({ + referencingArtworksLink: + relation('linkReferencingArtworks', artwork), + }), + + data: (artwork) => ({ referenced: - referenced.length, + artwork.referencedArtworks.length, referencedBy: - referencedBy.length, + artwork.referencedByArtworks.length, }), - slots: { - referencedLink: {type: 'html', mutable: true}, - referencingLink: {type: 'html', mutable: true}, - }, - - generate: (data, slots, {html, language}) => + generate: (data, relations, {html, language}) => language.encapsulate('releaseInfo', capsule => { const referencedText = language.$(capsule, 'referencesArtworks', { @@ -47,10 +48,10 @@ export default { [ !html.isBlank(referencedText) && - slots.referencedLink.slot('content', referencedText), + relations.referencedArtworksLink.slot('content', referencedText), !html.isBlank(referencingText) && - slots.referencingLink.slot('content', referencingText), + relations.referencingArtworksLink.slot('content', referencingText), ])); }), } diff --git a/src/content/dependencies/generateCoverCarousel.js b/src/content/dependencies/generateCoverCarousel.js index 430f651e..1ffeff8e 100644 --- a/src/content/dependencies/generateCoverCarousel.js +++ b/src/content/dependencies/generateCoverCarousel.js @@ -2,8 +2,6 @@ import {empty, repeat, stitchArrays} from '#sugar'; import {getCarouselLayoutForNumberOfItems} from '#wiki-data'; export default { - extraDependencies: ['html'], - slots: { images: {validate: v => v.strictArrayOf(v.isHTML)}, links: {validate: v => v.strictArrayOf(v.isHTML)}, @@ -19,7 +17,7 @@ export default { }); if (empty(stitched)) { - return; + return html.blank(); } const layout = getCarouselLayoutForNumberOfItems(stitched.length); diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 1898832f..091833a9 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -1,20 +1,23 @@ -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateGridActionLinks'], - extraDependencies: ['html', 'language'], + relations: (relation) => ({ + actionLinks: + relation('generateGridActionLinks'), - relations(relation) { - return { - actionLinks: relation('generateGridActionLinks'), - }; - }, + expando: + relation('generateGridExpando'), + }), slots: { + attributes: {type: 'attributes', mutable: false}, + images: {validate: v => v.strictArrayOf(v.isHTML)}, links: {validate: v => v.strictArrayOf(v.isHTML)}, names: {validate: v => v.strictArrayOf(v.isHTML)}, info: {validate: v => v.strictArrayOf(v.isHTML)}, + tab: {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 @@ -29,32 +32,115 @@ export default { v.isString))), }, + itemAttributes: { + validate: v => + v.strictArrayOf( + v.optional(v.isAttributes)), + }, + lazy: {validate: v => v.anyOf(v.isWholeNumber, v.isBoolean)}, actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, + + revealAllWarnings: { + validate: v => v.looseArrayOf(v.isString), + }, + + bottomCaption: { + type: 'html', + mutable: false, + }, + + cutIndex: {validate: v => v.isWholeNumber}, }, - generate(relations, slots, {html, language}) { - return ( - html.tag('div', {class: 'grid-listing'}, [ + generate: (relations, slots, {html, language}) => + html.tag('div', {class: 'grid-listing'}, + slots.attributes, + {[html.onlyIfContent]: true}, + + [ + !empty((slots.revealAllWarnings ?? []).filter(Boolean)) && + language.encapsulate('misc.coverGrid.revealAll', capsule => + html.tag('div', {class: 'reveal-all-container'}, + ((slots.tab ?? []) + .slice(0, 4) + .some(tab => tab && !html.isBlank(tab))) && + + {class: 'has-nearby-tab'}, + + html.tag('p', {class: 'reveal-all'}, [ + html.tag('a', {href: '#'}, [ + html.tag('span', {class: 'reveal-label'}, + language.$(capsule, 'reveal')), + + html.tag('span', {class: 'conceal-label'}, + {style: 'display: none'}, + language.$(capsule, 'conceal')), + ]), + + html.tag('br'), + + html.tag('span', {class: 'warnings'}, + language.$(capsule, 'warnings', { + warnings: + language.formatUnitList( + unique(slots.revealAllWarnings.filter(Boolean)) + .sort() + .map(warning => html.tag('b', warning))), + })), + ]))), + stitchArrays({ classes: slots.classes, + attributes: slots.itemAttributes, image: slots.images, link: slots.links, name: slots.names, info: slots.info, - }).map(({classes, image, link, name, info}, index) => + tab: slots.tab, + + notFromThisGroup: + slots.notFromThisGroup ?? + Array.from(slots.links).fill(null) + }).map(({ + classes, + attributes, + image, + link, + name, + info, + tab, + notFromThisGroup, + }, index) => link.slots({ attributes: [ + link.getSlotValue('attributes'), + {class: ['grid-item', 'box']}, + tab && + !html.isBlank(tab) && + {class: 'has-tab'}, + + attributes, + (classes ? {class: classes} : null), + + slots.cutIndex >= 1 && + index >= slots.cutIndex && + {class: 'hidden-by-expandable-cut'}, ], colorContext: 'image-box', content: [ + html.tag('span', + {[html.onlyIfContent]: true}, + + tab), + image.slots({ thumb: 'medium', square: true, @@ -69,7 +155,15 @@ export default { html.tag('span', {[html.onlyIfContent]: true}, - language.sanitize(name)), + (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}, @@ -84,6 +178,17 @@ export default { relations.actionLinks .slot('actionLinks', slots.actionLinks), - ])); - }, + + (slots.cutIndex >= 1 && + slots.cutIndex < slots.links.length + ? relations.expando.slots({ + caption: slots.bottomCaption, + }) + + : !html.isBlank(relations.bottomCaption) + ? html.tag('p', {class: 'grid-caption'}, + slots.caption) + + : html.blank()), + ]), }; diff --git a/src/content/dependencies/generateDatetimestampTemplate.js b/src/content/dependencies/generateDatetimestampTemplate.js index a92d15fc..56b2e595 100644 --- a/src/content/dependencies/generateDatetimestampTemplate.js +++ b/src/content/dependencies/generateDatetimestampTemplate.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateTextWithTooltip'], - extraDependencies: ['html'], - relations: (relation) => ({ textWithTooltip: relation('generateTextWithTooltip'), diff --git a/src/content/dependencies/generateDotSwitcherTemplate.js b/src/content/dependencies/generateDotSwitcherTemplate.js index 22205922..561a44bc 100644 --- a/src/content/dependencies/generateDotSwitcherTemplate.js +++ b/src/content/dependencies/generateDotSwitcherTemplate.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { attributes: { type: 'attributes', diff --git a/src/content/dependencies/generateExternalHandle.js b/src/content/dependencies/generateExternalHandle.js index 8c0368a4..8653b177 100644 --- a/src/content/dependencies/generateExternalHandle.js +++ b/src/content/dependencies/generateExternalHandle.js @@ -1,8 +1,6 @@ import {isExternalLinkContext} from '#external-links'; export default { - extraDependencies: ['html', 'language'], - data: (url) => ({url}), slots: { diff --git a/src/content/dependencies/generateExternalIcon.js b/src/content/dependencies/generateExternalIcon.js index 637af658..03af643e 100644 --- a/src/content/dependencies/generateExternalIcon.js +++ b/src/content/dependencies/generateExternalIcon.js @@ -1,8 +1,6 @@ import {isExternalLinkContext} from '#external-links'; export default { - extraDependencies: ['html', 'language', 'to'], - data: (url) => ({url}), slots: { diff --git a/src/content/dependencies/generateExternalPlatform.js b/src/content/dependencies/generateExternalPlatform.js index c4f63ecf..b2822d64 100644 --- a/src/content/dependencies/generateExternalPlatform.js +++ b/src/content/dependencies/generateExternalPlatform.js @@ -1,8 +1,6 @@ import {isExternalLinkContext} from '#external-links'; export default { - extraDependencies: ['html', 'language'], - data: (url) => ({url}), slots: { diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js index 8f174b21..896ee224 100644 --- a/src/content/dependencies/generateFlashActGalleryPage.js +++ b/src/content/dependencies/generateFlashActGalleryPage.js @@ -1,21 +1,6 @@ -import {stitchArrays} from '#sugar'; - import striptags from 'striptags'; export default { - contentDependencies: [ - 'generateCoverGrid', - 'generateFlashActNavAccent', - 'generateFlashActSidebar', - 'generatePageLayout', - 'image', - 'linkFlash', - 'linkFlashAct', - 'linkFlashIndex', - ], - - extraDependencies: ['language'], - relations: (relation, act) => ({ layout: relation('generatePageLayout'), @@ -37,7 +22,7 @@ export default { coverGridImages: act.flashes - .map(_flash => relation('image')), + .map(flash => relation('image', flash.coverArtwork)), flashLinks: act.flashes @@ -50,10 +35,6 @@ export default { flashNames: act.flashes.map(flash => flash.name), - - flashCoverPaths: - act.flashes.map(flash => - ['media.flashArt', flash.directory, flash.coverArtFileExtension]) }), generate: (data, relations, {language}) => @@ -71,15 +52,9 @@ export default { mainContent: [ relations.coverGrid.slots({ links: relations.flashLinks, + images: relations.coverGridImages, names: data.flashNames, lazy: 6, - - images: - stitchArrays({ - image: relations.coverGridImages, - path: data.flashCoverPaths, - }).map(({image, path}) => - image.slot('path', path)), }), ], diff --git a/src/content/dependencies/generateFlashActNavAccent.js b/src/content/dependencies/generateFlashActNavAccent.js index c4ec77b8..7ad46051 100644 --- a/src/content/dependencies/generateFlashActNavAccent.js +++ b/src/content/dependencies/generateFlashActNavAccent.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkFlashAct', - ], - - extraDependencies: ['wikiData'], - sprawl: ({flashActData}) => ({flashActData}), diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js index 1421dde9..0d952077 100644 --- a/src/content/dependencies/generateFlashActSidebar.js +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -1,10 +1,4 @@ export default { - contentDependencies: [ - 'generateFlashActSidebarCurrentActBox', - 'generateFlashActSidebarSideMapBox', - 'generatePageSidebar', - ], - relations: (relation, act, flash) => ({ sidebar: relation('generatePageSidebar'), diff --git a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js index 6d152c7c..e08582fe 100644 --- a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js +++ b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generatePageSidebarBox', - 'linkFlash', - 'linkFlashAct', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, act, _flash) => ({ box: relation('generatePageSidebarBox'), diff --git a/src/content/dependencies/generateFlashActSidebarSideMapBox.js b/src/content/dependencies/generateFlashActSidebarSideMapBox.js index 7b26ef31..4b97f21d 100644 --- a/src/content/dependencies/generateFlashActSidebarSideMapBox.js +++ b/src/content/dependencies/generateFlashActSidebarSideMapBox.js @@ -1,15 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generatePageSidebarBox', - 'linkFlashAct', - 'linkFlashIndex', - ], - - extraDependencies: ['html', 'wikiData'], - sprawl: ({flashSideData}) => ({flashSideData}), relations: (relation, sprawl, _act, _flash) => ({ diff --git a/src/content/dependencies/generateFlashArtworkColumn.js b/src/content/dependencies/generateFlashArtworkColumn.js new file mode 100644 index 00000000..207c3bf3 --- /dev/null +++ b/src/content/dependencies/generateFlashArtworkColumn.js @@ -0,0 +1,9 @@ +export default { + 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 4b0e5242..00000000 --- a/src/content/dependencies/generateFlashCoverArtwork.js +++ /dev/null @@ -1,41 +0,0 @@ -export default { - contentDependencies: ['generateCoverArtwork', 'image'], - extraDependencies: ['html', 'language'], - - relations: (relation) => ({ - coverArtwork: - relation('generateCoverArtwork'), - - image: - relation('image'), - }), - - data: (flash) => ({ - path: - ['media.flashArt', flash.directory, flash.coverArtFileExtension], - - color: - flash.color, - - dimensions: - flash.coverArtDimensions, - }), - - slots: { - mode: {type: 'string'}, - }, - - generate: (data, relations, slots, {language}) => - relations.coverArtwork.slots({ - mode: slots.mode, - - image: - relations.image.slots({ - path: data.path, - color: data.color, - alt: language.$('misc.alt.flashArt'), - }), - - dimensions: data.dimensions, - }), -}; diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js index a21bb49e..1fb286c6 100644 --- a/src/content/dependencies/generateFlashIndexPage.js +++ b/src/content/dependencies/generateFlashIndexPage.js @@ -1,17 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkFlash', - 'linkFlashAct', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({flashActData}) => ({flashActData}), query(sprawl) { @@ -53,7 +42,7 @@ export default { actCoverGridImages: query.flashActs .map(act => act.flashes - .map(() => relation('image'))), + .map(flash => relation('image', flash.coverArtwork))), }), data: (query) => ({ @@ -73,11 +62,6 @@ export default { query.flashActs .map(act => act.flashes .map(flash => flash.name)), - - actCoverGridPaths: - query.flashActs - .map(act => act.flashes - .map(flash => ['media.flashArt', flash.directory, flash.coverArtFileExtension])), }), generate: (data, relations, {html, language}) => @@ -116,7 +100,6 @@ export default { coverGridImages: relations.actCoverGridImages, coverGridLinks: relations.actCoverGridLinks, coverGridNames: data.actCoverGridNames, - coverGridPaths: data.actCoverGridPaths, }).map(({ colorStyle, actLink, @@ -126,7 +109,6 @@ export default { coverGridImages, coverGridLinks, coverGridNames, - coverGridPaths, }, index) => [ html.tag('h2', {id: anchor}, @@ -135,15 +117,9 @@ export default { coverGrid.slots({ links: coverGridLinks, + images: coverGridImages, names: coverGridNames, lazy: index === 0 ? 4 : true, - - images: - stitchArrays({ - image: coverGridImages, - path: coverGridPaths, - }).map(({image, path}) => - image.slot('path', path)), }), ]), ], diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js index 350a0fc5..935ffdc6 100644 --- a/src/content/dependencies/generateFlashInfoPage.js +++ b/src/content/dependencies/generateFlashInfoPage.js @@ -1,22 +1,19 @@ import {empty} from '#sugar'; -export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateCommentaryEntry', - 'generateContentHeading', - 'generateContributionList', - 'generateFlashActSidebar', - 'generateFlashCoverArtwork', - 'generateFlashNavAccent', - 'generatePageLayout', - 'generateTrackList', - 'linkExternal', - 'linkFlashAct', - ], - - extraDependencies: ['html', 'language'], +function checkInterrupted(which, relations, {html}) { + if ( + !html.isBlank(relations.contributorContributionList) || + !html.isBlank(relations.featuredTracksList) + ) return true; + + if (which === 'crediting-sources') { + if (!html.isBlank(relations.artistCommentaryEntries)) return true; + } + + return false; +} +export default { query(flash) { const query = {}; @@ -47,12 +44,18 @@ export default { query.urls .map(url => relation('linkExternal', url)), - cover: - relation('generateFlashCoverArtwork', flash), + artworkColumn: + relation('generateFlashArtworkColumn', flash), contentHeading: relation('generateContentHeading'), + commentaryContentHeading: + relation('generateCommentaryContentHeading', flash), + + readCommentaryLine: + relation('generateReadCommentaryLine', flash), + flashActLink: relation('linkFlashAct', flash.act), @@ -60,7 +63,7 @@ export default { relation('generateFlashNavAccent', flash), featuredTracksList: - relation('generateTrackList', flash.featuredTracks), + relation('generateTrackList', flash.featuredTracks, []), contributorContributionList: relation('generateContributionList', flash.contributorContribs), @@ -69,9 +72,10 @@ export default { flash.commentary .map(entry => relation('generateCommentaryEntry', entry)), - creditSourceEntries: - flash.commentary - .map(entry => relation('generateCommentaryEntry', entry)), + creditingSourcesSection: + relation('generateCollapsedContentEntrySection', + flash.creditingSources, + flash), }), data: (_query, flash) => ({ @@ -98,7 +102,7 @@ export default { additionalNames: relations.additionalNamesBox, - cover: relations.cover, + artworkColumnContent: relations.artworkColumn, mainContent: [ html.tag('p', @@ -123,21 +127,16 @@ export default { {[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')), - })), + checkInterrupted('commentary', relations, {html}) && + relations.readCommentaryLine, - !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + checkInterrupted('crediting-sources', relations, {html}) && + !html.isBlank(relations.creditingSourcesSection) && + language.encapsulate(capsule, 'readCreditingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#crediting-sources'}, language.$(capsule, 'link')), })), ])), @@ -168,24 +167,14 @@ export default { ]), html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), - }), - + relations.commentaryContentHeading, relations.artistCommentaryEntries, ]), - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), - }), - - relations.creditSourceEntries, - ]), + relations.creditingSourcesSection.slots({ + id: 'crediting-sources', + string: 'misc.creditingSources', + }), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateFlashNavAccent.js b/src/content/dependencies/generateFlashNavAccent.js index 0f5d2d6b..db9d3c1e 100644 --- a/src/content/dependencies/generateFlashNavAccent.js +++ b/src/content/dependencies/generateFlashNavAccent.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkFlash', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({flashActData}) => ({flashActData}), diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js index dfd83aef..efa1972a 100644 --- a/src/content/dependencies/generateFooterLocalizationLinks.js +++ b/src/content/dependencies/generateFooterLocalizationLinks.js @@ -2,15 +2,6 @@ import {sortByName} from '#sort'; import {stitchArrays} from '#sugar'; export default { - extraDependencies: [ - 'defaultLanguage', - 'html', - 'language', - 'languages', - 'pagePath', - 'to', - ], - generate({ defaultLanguage, html, diff --git a/src/content/dependencies/generateGridActionLinks.js b/src/content/dependencies/generateGridActionLinks.js index 585a02b9..5b3f9c1e 100644 --- a/src/content/dependencies/generateGridActionLinks.js +++ b/src/content/dependencies/generateGridActionLinks.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, diff --git a/src/content/dependencies/generateGridExpando.js b/src/content/dependencies/generateGridExpando.js new file mode 100644 index 00000000..5a0cbce5 --- /dev/null +++ b/src/content/dependencies/generateGridExpando.js @@ -0,0 +1,37 @@ +export default { + slots: { + caption: {type: 'html', mutable: false}, + }, + + generate: (slots, {html, language}) => + language.encapsulate('misc.coverGrid', capsule => + html.tag('div', {class: 'grid-expando'}, + {[html.onlyIfSiblings]: true}, + + html.tag('p', {class: 'grid-expando-content'}, + {[html.joinChildren]: html.tag('br')}, + + [ + html.tag('span', {class: 'grid-caption'}, + slots.caption), + + !html.isBlank(slots.contentBelowCut) && + language.$(capsule, 'expandCollapseCue', { + cue: + html.tag('a', {class: 'grid-expando-toggle'}, + {href: '#'}, + + {[html.joinChildren]: ''}, + {[html.noEdgeWhitespace]: true}, + + [ + html.tag('span', {class: 'grid-expand-cue'}, + language.$(capsule, 'expand')), + + html.tag('span', {class: 'grid-collapse-cue'}, + {style: 'display: none'}, + language.$(capsule, 'collapse')), + ]), + }), + ]))), +}; diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index 206c495d..e378f8a2 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -1,115 +1,77 @@ import {sortChronologically} from '#sort'; -import {empty, stitchArrays} from '#sugar'; import {filterItemsForCarousel, getTotalDuration} from '#wiki-data'; export default { - contentDependencies: [ - 'generateCoverCarousel', - 'generateCoverGrid', - 'generateGroupNavLinks', - 'generateGroupSecondaryNav', - 'generateGroupSidebar', - 'generatePageLayout', - 'generateQuickDescription', - 'image', - 'linkAlbum', - 'linkListing', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - 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'); - - relations.navLinks = - relation('generateGroupNavLinks', group); - - if (sprawl.enableGroupUI) { - relations.secondaryNav = - relation('generateGroupSecondaryNav', group); - - relations.sidebar = - relation('generateGroupSidebar', group); - } - - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); + query.allTracks = + query.allAlbums.flatMap((album) => album.tracks); - if (!empty(carouselAlbums)) { - relations.coverCarousel = - relation('generateCoverCarousel'); + query.carouselAlbums = + filterItemsForCarousel(group.featuredAlbums); - relations.carouselLinks = - carouselAlbums - .map(album => relation('linkAlbum', album)); + return query; + }, - relations.carouselImages = - carouselAlbums - .map(album => relation('image', album.artTags)); - } + relations: (relation, query, sprawl, group) => ({ + layout: + relation('generatePageLayout'), - relations.quickDescription = - relation('generateQuickDescription', group); + navLinks: + relation('generateGroupNavLinks', group), - relations.coverGrid = - relation('generateCoverGrid'); + secondaryNav: + (sprawl.enableGroupUI + ? relation('generateGroupSecondaryNav', group) + : null), - relations.gridLinks = - albums - .map(album => relation('linkAlbum', album)); + coverCarousel: + relation('generateCoverCarousel'), - relations.gridImages = - albums.map(album => - (album.hasCoverArt - ? relation('image', album.artTags) - : relation('image'))); + carouselLinks: + query.carouselAlbums + .map(album => relation('linkAlbum', album)), - return relations; - }, + carouselImages: + query.carouselAlbums + .map(album => relation('image', album.coverArtworks[0])), - data(sprawl, group) { - const data = {}; + quickDescription: + relation('generateQuickDescription', group), - data.name = group.name; - data.color = group.color; + albumViewSwitcher: + relation('generateIntrapageDotSwitcher'), - const albums = sortChronologically(group.albums.slice(), {latestFirst: true}); - const tracks = albums.flatMap((album) => album.tracks); + albumsBySeriesView: + relation('generateGroupGalleryPageAlbumsBySeriesView', group), - data.numAlbums = albums.length; - data.numTracks = tracks.length; - data.totalDuration = getTotalDuration(tracks, {mainReleasesOnly: true}); + albumsByDateView: + relation('generateGroupGalleryPageAlbumsByDateView', group), + }), - data.gridNames = albums.map(album => album.name); - data.gridDurations = albums.map(album => getTotalDuration(album.tracks)); - data.gridNumTracks = albums.map(album => album.tracks.length); + data: (query, _sprawl, group) => ({ + name: + group.name, - data.gridPaths = - albums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); + color: + group.color, - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); + numAlbums: + query.allAlbums.length, - if (!empty(group.featuredAlbums)) { - data.carouselPaths = - carouselAlbums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); - } + numTracks: + query.allTracks.length, - return data; - }, + totalDuration: + getTotalDuration(query.allTracks, {mainReleasesOnly: true}), + }), generate: (data, relations, {html, language}) => language.encapsulate('groupGalleryPage', pageCapsule => @@ -121,16 +83,10 @@ export default { mainClasses: ['top-index'], mainContent: [ - relations.coverCarousel - ?.slots({ - links: relations.carouselLinks, - images: - stitchArrays({ - image: relations.carouselImages, - path: data.carouselPaths, - }).map(({image, path}) => - image.slot('path', path)), - }), + relations.coverCarousel.slots({ + links: relations.carouselLinks, + images: relations.carouselImages, + }), relations.quickDescription, @@ -155,49 +111,81 @@ 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.coverGrid.noCoverArt', { - album: name, - }), - })), - info: - stitchArrays({ - numTracks: data.gridNumTracks, - duration: data.gridDurations, - }).map(({numTracks, duration}) => - language.$('misc.coverGrid.details.albumLength', { - 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.slots({ + showTitle: + !html.isBlank(relations.albumsBySeriesView), + }), + + relations.albumsBySeriesView.slots({ + attributes: [ + !html.isBlank(relations.albumsBySeriesView) && + {style: 'display: none'}, + ], + }), ], - leftSidebar: - (relations.sidebar - ? relations.sidebar - .slot('currentExtra', 'gallery') - .content /* TODO: Kludge. */ - : null), - navLinkStyle: 'hierarchical', navLinks: relations.navLinks .slot('currentExtra', 'gallery') .content, - secondaryNav: - relations.secondaryNav ?? null, + secondaryNav: relations.secondaryNav, })), }; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js new file mode 100644 index 00000000..37c1951d --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js @@ -0,0 +1,97 @@ +import {stitchArrays} from '#sugar'; +import {getTotalDuration} from '#wiki-data'; + +export default { + query: (albums, _group) => ({ + artworks: + albums.map(album => + (album.hasCoverArt + ? album.coverArtworks[0] + : null)), + }), + + relations: (relation, query, albums, group) => ({ + coverGrid: + relation('generateCoverGrid'), + + links: + albums + .map(album => relation('linkAlbum', album)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + + tabs: + albums + .map(album => + relation('generateGroupGalleryPageAlbumGridTab', album, group)), + }), + + data: (query, albums, group) => ({ + names: + albums.map(album => album.name), + + styles: + albums.map(album => album.style), + + tracks: + albums.map(album => album.tracks.length), + + allWarnings: + query.artworks.flatMap(artwork => artwork?.contentWarnings), + + durations: + albums.map(album => + (album.hideDuration + ? null + : getTotalDuration(album.tracks))), + + 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, + }), + })), + + itemAttributes: + data.styles.map(style => ({'data-style': style})), + + tab: relations.tabs, + + info: + stitchArrays({ + style: data.styles, + tracks: data.tracks, + duration: data.durations, + }).map(({style, tracks, duration}) => + (style === 'single' && duration + ? language.$(capsule, 'details.albumLength.single', { + time: language.formatDuration(duration), + }) + : duration + ? language.$(capsule, 'details.albumLength', { + tracks: language.countTracks(tracks, {unit: true}), + time: language.formatDuration(duration), + }) + : null)), + + revealAllWarnings: data.allWarnings, + })), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js b/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js new file mode 100644 index 00000000..c3b860e4 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js @@ -0,0 +1,79 @@ +import {empty} from '#sugar'; + +export default { + query(album, group) { + if (album.groups.length > 1) { + const contextGroup = group; + + const candidateGroupCategory = + album.groups + .filter(group => !group.excludeFromGalleryTabs) + .find(group => group.category !== contextGroup.category) + ?.category ?? + null; + + const candidateGroups = + album.groups + .filter(group => !group.excludeFromGalleryTabs) + .filter(group => group.category === candidateGroupCategory); + + if (!empty(candidateGroups)) { + return { + mode: 'groups', + notedGroups: candidateGroups, + }; + } + } + + if (!empty(album.artistContribs)) { + if ( + album.artistContribs.length === 1 && + !empty(group.closelyLinkedArtists) && + (album.artistContribs[0].artist.name === + group.closelyLinkedArtists[0].artist.name) + ) { + return {mode: null}; + } + + return { + mode: 'artists', + notedArtistContribs: album.artistContribs, + }; + } + + return {mode: null};; + }, + + relations: (relation, query, _album, _group) => ({ + artistCredit: + (query.mode === 'artists' + ? relation('generateArtistCredit', query.notedArtistContribs, []) + : null), + }), + + data: (query, _album, _group) => ({ + mode: query.mode, + + groupNames: + (query.mode === 'groups' + ? query.notedGroups.map(group => group.name) + : null), + }), + + generate: (data, relations, {language}) => + language.encapsulate('misc.coverGrid.tab', capsule => + (data.mode === 'groups' + ? language.$(capsule, 'groups', { + groups: + language.formatUnitList(data.groupNames), + }) + : data.mode === 'artists' + ? relations.artistCredit.slots({ + normalStringKey: + capsule + '.artists', + + normalFeaturingStringKey: + capsule + '.artists.featuring', + }) + : null)), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js new file mode 100644 index 00000000..75ef1048 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js @@ -0,0 +1,48 @@ +import {sortChronologically} from '#sort'; + +export default { + query: (group) => ({ + albums: + sortChronologically(group.albums.slice(), {latestFirst: true}), + }), + + relations: (relation, query, group) => ({ + styleSelector: + (group.divideAlbumsByStyle + ? relation('generateGroupGalleryPageStyleSelector', group) + : null), + + albumGrid: + relation('generateGroupGalleryPageAlbumGrid', + query.albums, + group), + }), + + slots: { + showTitle: { + type: 'boolean', + }, + + 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', [ + slots.showTitle && + html.tag('h2', + language.$(capsule, 'title')), + + relations.styleSelector, + + relations.albumGrid, + ]))), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js new file mode 100644 index 00000000..68cf249f --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js @@ -0,0 +1,23 @@ +export default { + 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..1aa835d6 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js @@ -0,0 +1,138 @@ +import {sortChronologically} from '#sort'; + +export default { + query(series) { + const query = {}; + + query.albums = + sortChronologically(series.albums.slice(), {latestFirst: true}); + + query.allAlbumsDated = + series.albums.every(album => album.date); + + query.anyAlbumNotFromThisGroup = + series.albums.some(album => !album.groups.includes(series.group)); + + query.latestAlbum = + query.albums + .filter(album => album.date) + .at(0) ?? + null; + + query.earliestAlbum = + query.albums + .filter(album => album.date) + .at(-1) ?? + null; + + return query; + }, + + relations: (relation, query, series) => ({ + contentHeading: + relation('generateContentHeading'), + + grid: + relation('generateGroupGalleryPageAlbumGrid', + query.albums, + 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 => + html.tags([ + relations.contentHeading.slots({ + tag: 'h2', + title: language.sanitize(data.name), + }), + + relations.grid.slots({ + cutIndex: 4, + + bottomCaption: + 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')})), + }), + ])), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageStyleSelector.js b/src/content/dependencies/generateGroupGalleryPageStyleSelector.js new file mode 100644 index 00000000..9342e50f --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageStyleSelector.js @@ -0,0 +1,60 @@ +import {unique} from '#sugar'; + +export default { + query: (group) => ({ + styles: + unique(group.albums.map(album => album.style)), + }), + + data: (query, group) => ({ + albums: + group.albums.length, + + styles: + query.styles, + }), + + generate: (data, {html, language}) => + language.encapsulate('groupGalleryPage', pageCapsule => + (data.styles.length <= 1 + ? html.blank() + : html.tag('p', {class: 'gallery-style-selector'}, + {class: ['drop', 'shiny']}, + + language.encapsulate(pageCapsule, 'albumStyleSwitcher', capsule => [ + html.tag('span', + language.$(capsule)), + + html.tag('br'), + + html.tag('span', {class: 'styles'}, + data.styles.map(style => + html.tag('label', {'data-style': style}, [ + html.tag('input', {type: 'checkbox'}, + {checked: true}), + + html.tag('span', + language.$(capsule, style)), + ]))), + + html.tag('br'), + + html.tag('span', {class: ['count', 'all']}, + language.$(capsule, 'count.all', { + total: data.albums, + })), + + html.tag('span', {class: ['count', 'filtered']}, + {style: 'display: none'}, + + language.$(capsule, 'count.filtered', { + count: html.tag('span'), + total: data.albums, + })), + + html.tag('span', {class: ['count', 'none']}, + {style: 'display: none'}, + + language.$(capsule, 'count.none')), + ])))), +}; diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js index 7b9c2afa..0f3093b2 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -1,20 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateGroupInfoPageAlbumsSection', - 'generateGroupNavLinks', - 'generateGroupSecondaryNav', - 'generateGroupSidebar', - 'generatePageLayout', - 'linkArtist', - 'linkExternal', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ enableGroupUI: wikiInfo.enableGroupUI, diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js b/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js index df42598d..de55f33a 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js @@ -1,10 +1,6 @@ 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 diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js b/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js index bcd5d288..f8314d71 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js @@ -1,13 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateContentHeading', - 'generateGroupInfoPageAlbumsListItem', - ], - - extraDependencies: ['html', 'language'], - query: (group) => ({ closelyLinkedArtists: group.closelyLinkedArtists diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js b/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js index 99e7e8ff..1211dfb8 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js @@ -1,16 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateArtistCredit', - 'generateColorStyleAttribute', - 'linkAlbum', - 'linkGroup', - ], - - extraDependencies: ['html', 'language'], - query: (album, group) => { const otherCategory = album.groups @@ -76,7 +66,7 @@ export default { workingOptions.yearAccent = language.$(yearCapsule, 'accent', { year: - relations.datetimestamp.slots({style: 'year', tooltip: true}), + relations.datetimestamp.slot('style', 'year'), }); } @@ -127,8 +117,7 @@ export default { workingCapsule += '.withArtists'; workingOptions.by = html.tag('span', {class: 'by'}, - html.metatag('chunkwrap', {split: ','}, - html.resolve(artistCredit))); + artistCredit); } return language.$(workingCapsule, workingOptions); diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js index 0b678e9d..4470eb2f 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js @@ -1,14 +1,4 @@ export default { - contentDependencies: [ - 'generateContentHeading', - 'generateGroupInfoPageAlbumsListByDate', - 'generateGroupInfoPageAlbumsListBySeries', - 'generateIntrapageDotSwitcher', - 'linkGroupGallery', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, group) => ({ contentHeading: relation('generateContentHeading'), diff --git a/src/content/dependencies/generateGroupNavAccent.js b/src/content/dependencies/generateGroupNavAccent.js index 0e4ebe8a..18281bf0 100644 --- a/src/content/dependencies/generateGroupNavAccent.js +++ b/src/content/dependencies/generateGroupNavAccent.js @@ -1,14 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'linkGroup', - 'linkGroupGallery', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, group) => ({ switcher: relation('generateInterpageDotSwitcher'), diff --git a/src/content/dependencies/generateGroupNavLinks.js b/src/content/dependencies/generateGroupNavLinks.js index bdc3ee4c..4f13e474 100644 --- a/src/content/dependencies/generateGroupNavLinks.js +++ b/src/content/dependencies/generateGroupNavLinks.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateGroupNavAccent', 'linkGroup'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({groupCategoryData, wikiInfo}) => ({ groupCategoryData, enableGroupUI: wikiInfo.enableGroupUI, diff --git a/src/content/dependencies/generateGroupSecondaryNav.js b/src/content/dependencies/generateGroupSecondaryNav.js index c48f3142..6b4347dd 100644 --- a/src/content/dependencies/generateGroupSecondaryNav.js +++ b/src/content/dependencies/generateGroupSecondaryNav.js @@ -1,9 +1,4 @@ export default { - contentDependencies: [ - 'generateSecondaryNav', - 'generateGroupSecondaryNavCategoryPart', - ], - relations: (relation, group) => ({ secondaryNav: relation('generateSecondaryNav'), diff --git a/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js b/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js index b2adb9f8..df627c99 100644 --- a/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js +++ b/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateSecondaryNavParentSiblingsPart', - 'linkGroupDynamically', - 'linkListing', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({listingSpec, wikiInfo}) => ({ groupsByCategoryListing: (wikiInfo.enableListings diff --git a/src/content/dependencies/generateGroupSidebar.js b/src/content/dependencies/generateGroupSidebar.js index 0888cbbe..1359eaca 100644 --- a/src/content/dependencies/generateGroupSidebar.js +++ b/src/content/dependencies/generateGroupSidebar.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateGroupSidebarCategoryDetails', - 'generatePageSidebar', - 'generatePageSidebarBox', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({groupCategoryData}) => ({groupCategoryData}), relations: (relation, sprawl, group) => ({ diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js index 208ccd07..a7e1f240 100644 --- a/src/content/dependencies/generateGroupSidebarCategoryDetails.js +++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js @@ -1,14 +1,6 @@ import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'linkGroup', - 'linkGroupGallery', - ], - - extraDependencies: ['html', 'language'], - relations(relation, category) { return { colorStyle: diff --git a/src/content/dependencies/generateImageOverlay.js b/src/content/dependencies/generateImageOverlay.js index cfb78a1b..006cfcce 100644 --- a/src/content/dependencies/generateImageOverlay.js +++ b/src/content/dependencies/generateImageOverlay.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - generate: ({html, language}) => html.tag('div', {id: 'image-overlay-container'}, html.tag('div', {id: 'image-overlay-content-container'}, [ diff --git a/src/content/dependencies/generateInterpageDotSwitcher.js b/src/content/dependencies/generateInterpageDotSwitcher.js index 5a33444e..ddb7cb37 100644 --- a/src/content/dependencies/generateInterpageDotSwitcher.js +++ b/src/content/dependencies/generateInterpageDotSwitcher.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateDotSwitcherTemplate'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ template: relation('generateDotSwitcherTemplate'), diff --git a/src/content/dependencies/generateIntrapageDotSwitcher.js b/src/content/dependencies/generateIntrapageDotSwitcher.js index 3f300676..943d862c 100644 --- a/src/content/dependencies/generateIntrapageDotSwitcher.js +++ b/src/content/dependencies/generateIntrapageDotSwitcher.js @@ -1,9 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateDotSwitcherTemplate'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ template: relation('generateDotSwitcherTemplate'), @@ -39,9 +36,32 @@ export default { stitchArrays({ title: slots.titles, targetID: slots.targetIDs, - }).map(({title, targetID}) => - html.tag('a', {href: '#'}, - {'data-target-id': targetID}, - language.sanitize(title))), + }).map(({title, targetID}) => { + const {content} = html.smush(title); + + const customCue = + content.find(item => + item?.tagName === 'span' && + item.attributes.has('class', 'dot-switcher-interaction-cue')); + + const cue = + (customCue && !html.isBlank(customCue) + ? customCue.content + : language.sanitize(title)); + + const a = + html.tag('a', {href: '#'}, + {'data-target-id': targetID}, + {[html.onlyIfContent]: true}, + + cue); + + if (customCue) { + content.splice(content.indexOf(customCue), 1, a); + return html.tags(content, {[html.joinChildren]: ''}); + } else { + return a; + } + }), }), }; diff --git a/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js b/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js new file mode 100644 index 00000000..e381a745 --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js @@ -0,0 +1,19 @@ +export default { + 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..0f14f12c --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js @@ -0,0 +1,42 @@ +export default { + 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 deb8c4ea..d68e3bc1 100644 --- a/src/content/dependencies/generateListAllAdditionalFilesChunk.js +++ b/src/content/dependencies/generateListAllAdditionalFilesChunk.js @@ -1,7 +1,22 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { - 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: { @@ -9,82 +24,73 @@ export default { mutable: false, }, - additionalFileTitles: { - validate: v => v.strictArrayOf(v.isHTML), - }, - - additionalFileLinks: { - validate: v => v.strictArrayOf(v.strictArrayOf(v.isHTML)), - }, - - additionalFileFiles: { - validate: v => v.strictArrayOf(v.strictArrayOf(v.isString)), - }, - stringsKey: {type: 'string'}, }, - generate(slots, {html, language}) { - if (empty(slots.additionalFileLinks)) { - return html.blank(); - } + generate: (data, relations, slots, {html, language}) => + language.encapsulate('listingPage', slots.stringsKey, pageCapsule => + html.tags([ + html.tag('dt', + {[html.onlyIfSiblings]: true}, + slots.title), - return html.tags([ - html.tag('dt', slots.title), - html.tag('dd', - html.tag('ul', - stitchArrays({ - additionalFileTitle: slots.additionalFileTitles, - additionalFileLinks: slots.additionalFileLinks, - additionalFileFiles: slots.additionalFileFiles, - }).map(({ - additionalFileTitle, - additionalFileLinks, - additionalFileFiles, - }) => - language.encapsulate('listingPage', slots.stringsKey, 'file', capsule => - (additionalFileLinks.length === 1 - ? html.tag('li', - additionalFileLinks[0].slots({ - content: - language.$(capsule, { - title: additionalFileTitle, - }), - })) + html.tag('dd', + {[html.onlyIfContent]: true}, - : additionalFileLinks.length === 0 - ? html.tag('li', - language.$(capsule, 'withNoFiles', { - title: additionalFileTitle, - })) + html.tag('ul', + {[html.onlyIfContent]: true}, - : html.tag('li', {class: 'has-details'}, - html.tag('details', [ - html.tag('summary', - html.tag('span', - language.$(capsule, 'withMultipleFiles', { - title: - html.tag('b', additionalFileTitle), + stitchArrays({ + title: data.titles, + links: relations.links, + filenames: data.filenames, + }).map(({ + title, + links, + filenames, + }) => + language.encapsulate(pageCapsule, 'file', capsule => + (links.length === 1 + ? html.tag('li', + links[0].slots({ + content: + language.$(capsule, { + title: title, + }), + })) - files: - language.countAdditionalFiles( - additionalFileLinks.length, - {unit: true}), - }))), + : links.length === 0 + ? html.tag('li', + language.$(capsule, 'withNoFiles', { + title: title, + })) - html.tag('ul', - stitchArrays({ - additionalFileLink: additionalFileLinks, - additionalFileFile: additionalFileFiles, - }).map(({additionalFileLink, additionalFileFile}) => - html.tag('li', - additionalFileLink.slots({ - content: - language.$(capsule, { - title: additionalFileFile, - }), - })))), - ]))))))), - ]); - }, + : html.tag('li', {class: 'has-details'}, + html.tag('details', [ + html.tag('summary', + html.tag('span', + language.$(capsule, 'withMultipleFiles', { + title: + html.tag('b', title), + + files: + language.countAdditionalFiles( + links.length, + {unit: true}), + }))), + + html.tag('ul', + stitchArrays({ + link: links, + filename: filenames, + }).map(({link, filename}) => + html.tag('li', + link.slots({ + content: + language.$(capsule, { + title: filename, + }), + })))), + ]))))))), + ])), }; diff --git a/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js b/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js new file mode 100644 index 00000000..9ac79bb5 --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js @@ -0,0 +1,20 @@ +export default { + 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/generateListRandomPageLinksAlbumLink.js b/src/content/dependencies/generateListRandomPageLinksAlbumLink.js index b3560aca..29e7b1c9 100644 --- a/src/content/dependencies/generateListRandomPageLinksAlbumLink.js +++ b/src/content/dependencies/generateListRandomPageLinksAlbumLink.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkAlbum'], - data: (album) => ({directory: album.directory}), diff --git a/src/content/dependencies/generateListingIndexList.js b/src/content/dependencies/generateListingIndexList.js index 78622e6e..db494f37 100644 --- a/src/content/dependencies/generateListingIndexList.js +++ b/src/content/dependencies/generateListingIndexList.js @@ -1,9 +1,6 @@ import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: ['linkListing'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({listingTargetSpec, wikiInfo}) { return {listingTargetSpec, wikiInfo}; }, diff --git a/src/content/dependencies/generateListingPage.js b/src/content/dependencies/generateListingPage.js index 5f9a99a9..987008eb 100644 --- a/src/content/dependencies/generateListingPage.js +++ b/src/content/dependencies/generateListingPage.js @@ -1,67 +1,36 @@ -import {bindOpts, empty, stitchArrays} from '#sugar'; +import {bindOpts, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateContentHeading', - 'generateListingSidebar', - 'generatePageLayout', - 'linkListing', - 'linkListingIndex', - 'linkTemplate', - ], + relations: (relation, listing) => ({ + layout: + relation('generatePageLayout'), - extraDependencies: ['html', 'language', 'wikiData'], + sidebar: + relation('generateListingSidebar', listing), - relations(relation, listing) { - const relations = {}; + listingsIndexLink: + relation('linkListingIndex'), - relations.layout = - relation('generatePageLayout'); + chunkHeading: + relation('generateContentHeading'), - relations.sidebar = - relation('generateListingSidebar', listing); + showSkipToSectionLinkTemplate: + relation('linkTemplate'), - relations.listingsIndexLink = - relation('linkListingIndex'); + sameTargetListingsLine: + (listing.target.listings.length > 1 + ? relation('generateListingPageSameTargetListingsLine', listing) + : null), - relations.chunkHeading = - relation('generateContentHeading'); + seeAlsoLinks: + listing.seeAlso + .map(listing => relation('linkListing', listing)), + }), - relations.showSkipToSectionLinkTemplate = - relation('linkTemplate'); - - if (listing.target.listings.length > 1) { - relations.sameTargetListingLinks = - listing.target.listings - .map(listing => relation('linkListing', listing)); - } else { - relations.sameTargetListingLinks = []; - } - - relations.seeAlsoLinks = - (!empty(listing.seeAlso) - ? listing.seeAlso - .map(listing => relation('linkListing', listing)) - : []); - - return relations; - }, - - data(listing) { - return { - stringsKey: listing.stringsKey, - - targetStringsKey: listing.target.stringsKey, - - sameTargetListingStringsKeys: - listing.target.listings - .map(listing => listing.stringsKey), - - sameTargetListingsCurrentIndex: - listing.target.listings - .indexOf(listing), - }; - }, + data: (listing) => ({ + stringsKey: + listing.stringsKey, + }), slots: { type: { @@ -169,29 +138,7 @@ export default { headingMode: 'sticky', mainContent: [ - 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'), - })))), - })), + relations.sameTargetListingsLine, html.tag('p', {[html.onlyIfContent]: true}, diff --git a/src/content/dependencies/generateListingPageSameTargetListingsLine.js b/src/content/dependencies/generateListingPageSameTargetListingsLine.js new file mode 100644 index 00000000..2146b1eb --- /dev/null +++ b/src/content/dependencies/generateListingPageSameTargetListingsLine.js @@ -0,0 +1,46 @@ +import {stitchArrays} from '#sugar'; + +export default { + relations: (relation, listing) => ({ + listingLinks: + listing.target.listings + .map(listing => relation('linkListing', listing)), + }), + + data: (listing) => ({ + targetStringsKey: + listing.target.stringsKey, + + listingStringsKeys: + listing.target.listings.map(listing => listing.stringsKey), + + currentIndex: + listing.target.listings.indexOf(listing), + }), + + generate: (data, relations, {html, language}) => + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$('listingPage.listingsFor', { + [language.onlyIfOptions]: ['listings'], + + target: + language.$('listingPage.target', data.targetStringsKey), + + listings: + language.formatUnitList( + stitchArrays({ + link: relations.listingLinks, + stringsKey: data.listingStringsKeys, + }).map(({link, stringsKey}, index) => + html.tag('span', + index === data.currentIndex && + {class: 'current'}, + + link.slots({ + attributes: {class: 'nowrap'}, + content: language.$('listingPage', stringsKey, 'title.short'), + })))), + })), +}; diff --git a/src/content/dependencies/generateListingSidebar.js b/src/content/dependencies/generateListingSidebar.js index aeac05cf..2d9429cf 100644 --- a/src/content/dependencies/generateListingSidebar.js +++ b/src/content/dependencies/generateListingSidebar.js @@ -1,13 +1,4 @@ export default { - contentDependencies: [ - 'generateListingIndexList', - 'generatePageSidebar', - 'generatePageSidebarBox', - 'linkListingIndex', - ], - - extraDependencies: ['html'], - relations: (relation, currentListing) => ({ sidebar: relation('generatePageSidebar'), diff --git a/src/content/dependencies/generateListingsIndexPage.js b/src/content/dependencies/generateListingsIndexPage.js index b57ebe15..80963d12 100644 --- a/src/content/dependencies/generateListingsIndexPage.js +++ b/src/content/dependencies/generateListingsIndexPage.js @@ -1,20 +1,14 @@ import {getTotalDuration} from '#wiki-data'; export default { - contentDependencies: [ - 'generateListingIndexList', - 'generateListingSidebar', - 'generatePageLayout', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({albumData, trackData, wikiInfo}) { return { wikiName: wikiInfo.name, numTracks: trackData.length, numAlbums: albumData.length, - totalDuration: getTotalDuration(trackData), + totalDuration: + getTotalDuration( + trackData.filter(track => track.countInArtistTotals)), }; }, diff --git a/src/content/dependencies/generateLyricsEntry.js b/src/content/dependencies/generateLyricsEntry.js new file mode 100644 index 00000000..15f84b27 --- /dev/null +++ b/src/content/dependencies/generateLyricsEntry.js @@ -0,0 +1,119 @@ +export default { + 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, + + numStanzas: + 1 + + + (Array.from( + entry.body + .matchAll(/\n\n|<br><br>/g)) + + .length) + + + (entry.body.includes('<br') + ? entry.body.split('\n').length + : 0), + + numLines: + 1 + + + (Array.from( + entry.body + .replaceAll(/(<br>){1,}/g, '\n') + .replaceAll(/\n{2,}/g, '\n') + .matchAll(/\n/g)) + + .length), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('misc.lyrics', capsule => + html.tag('blockquote', {class: 'lyrics-entry'}, + slots.attributes, + + {'data-stanzas': data.numStanzas}, + {'data-lines': data.numLines}, + + (data.numStanzas > 1 || + data.numLines > 8) && + {class: 'long-lyrics'}, + + [ + 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..bbc3a776 --- /dev/null +++ b/src/content/dependencies/generateLyricsSection.js @@ -0,0 +1,85 @@ +import {stitchArrays} from '#sugar'; + +export default { + relations: (relation, entries) => ({ + heading: + relation('generateContentHeading'), + + switcher: + relation('generateIntrapageDotSwitcher'), + + entries: + entries + .map(entry => relation('generateLyricsEntry', entry)), + + annotationParts: + entries + .map(entry => entry.annotationParts + .map(part => relation('transformContent', part))), + }), + + 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.annotationParts + .map(([first, ...rest]) => + language.formatUnitList([ + html.tag('span', + {class: 'dot-switcher-interaction-cue'}, + {[html.onlyIfContent]: true}, + + first?.slots({ + mode: 'inline', + textOnly: true, + })), + + ...rest.map(part => + part.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/generateName.js b/src/content/dependencies/generateName.js new file mode 100644 index 00000000..e0d0c6d3 --- /dev/null +++ b/src/content/dependencies/generateName.js @@ -0,0 +1,33 @@ +export default { + contentDependencies: ['transformContent'], + extraDependencies: ['html', 'language'], + + relations: (relation, thing) => ({ + customName: + (thing.nameText + ? relation('transformContent', thing.nameText) + : null), + }), + + data: (thing) => ({ + normalName: + thing.name, + + shortName: + thing.nameShort, + }), + + slots: { + preferShortName: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots, {language}) => + (relations.customName + ? relations.customName.slot('mode', 'inline') + : slots.preferShortName && data.shortName + ? language.sanitize(data.shortName) + : language.sanitize(data.normalName)), +}; diff --git a/src/content/dependencies/generateNearbyTrackList.js b/src/content/dependencies/generateNearbyTrackList.js new file mode 100644 index 00000000..56ab2df5 --- /dev/null +++ b/src/content/dependencies/generateNearbyTrackList.js @@ -0,0 +1,44 @@ +export default { + query: (tracks, contextTrack, _contextContributions) => ({ + presentedTracks: + (contextTrack + ? tracks.map(track => + track.otherReleases.find(({album}) => album === contextTrack.album) ?? + track) + : tracks), + }), + + relations: (relation, query, _tracks, _contextTrack, contextContributions) => ({ + items: + query.presentedTracks + .map(track => relation('generateTrackListItem', track, contextContributions)), + }), + + slots: { + showArtists: { + validate: v => v.is(true, false, 'auto'), + default: 'auto', + }, + + showDuration: { + type: 'boolean', + default: false, + }, + + colorMode: { + validate: v => v.is('none', 'track', 'line'), + default: 'track', + }, + }, + + generate: (relations, slots, {html}) => + html.tag('ul', + {[html.onlyIfContent]: true}, + + relations.items.map(item => + item.slots({ + showArtists: slots.showArtists, + showDuration: slots.showDuration, + colorMode: slots.colorMode, + }))), +}; diff --git a/src/content/dependencies/generateNewsEntryNavAccent.js b/src/content/dependencies/generateNewsEntryNavAccent.js index 5d168e41..05248eb3 100644 --- a/src/content/dependencies/generateNewsEntryNavAccent.js +++ b/src/content/dependencies/generateNewsEntryNavAccent.js @@ -1,11 +1,4 @@ export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkNewsEntry', - ], - relations: (relation, previousEntry, nextEntry) => ({ switcher: relation('generateInterpageDotSwitcher'), diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js index 4abd87d1..bbfb886d 100644 --- a/src/content/dependencies/generateNewsEntryPage.js +++ b/src/content/dependencies/generateNewsEntryPage.js @@ -2,16 +2,6 @@ import {sortChronologically} from '#sort'; import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateNewsEntryNavAccent', - 'generateNewsEntryReadAnotherLinks', - 'generatePageLayout', - 'linkNewsIndex', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({newsData}) { return {newsData}; }, diff --git a/src/content/dependencies/generateNewsEntryReadAnotherLinks.js b/src/content/dependencies/generateNewsEntryReadAnotherLinks.js index d978b0e4..1f6ee6d4 100644 --- a/src/content/dependencies/generateNewsEntryReadAnotherLinks.js +++ b/src/content/dependencies/generateNewsEntryReadAnotherLinks.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateRelativeDatetimestamp', - 'linkNewsEntry', - ], - - extraDependencies: ['html', 'language'], - relations(relation, currentEntry, previousEntry, nextEntry) { const relations = {}; @@ -57,10 +49,7 @@ export default { if (relations.previousEntryDatetimestamp) { parts.push('withDate'); options.date = - relations.previousEntryDatetimestamp.slots({ - style: 'full', - tooltip: true, - }); + relations.previousEntryDatetimestamp.slot('style', 'full'); } entryLines.push(language.$(...parts, options)); @@ -75,10 +64,7 @@ export default { if (relations.nextEntryDatetimestamp) { parts.push('withDate'); options.date = - relations.nextEntryDatetimestamp.slots({ - style: 'full', - tooltip: true, - }); + relations.nextEntryDatetimestamp.slot('style', 'full'); } entryLines.push(language.$(...parts, options)); diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js index 02964ce8..d88bfdba 100644 --- a/src/content/dependencies/generateNewsIndexPage.js +++ b/src/content/dependencies/generateNewsIndexPage.js @@ -2,14 +2,6 @@ import {sortChronologically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generatePageLayout', - 'linkNewsEntry', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({newsData}) { return {newsData}; }, diff --git a/src/content/dependencies/generateNextLink.js b/src/content/dependencies/generateNextLink.js index 2e48cd2b..2c497e12 100644 --- a/src/content/dependencies/generateNextLink.js +++ b/src/content/dependencies/generateNextLink.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['generatePreviousNextLink'], - relations: (relation) => ({ link: relation('generatePreviousNextLink'), diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index d667a6f4..23d5932d 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -1,27 +1,9 @@ +import striptags from 'striptags'; + import {openAggregate} from '#aggregate'; -import {empty, repeat} from '#sugar'; +import {atOffset, empty, repeat} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleRules', - 'generateFooterLocalizationLinks', - 'generateImageOverlay', - 'generatePageSidebar', - 'generateSearchSidebarBox', - 'generateStickyHeadingContainer', - 'transformContent', - ], - - extraDependencies: [ - 'getColors', - 'html', - 'language', - 'pagePath', - 'pagePathStringFromRoot', - 'to', - 'wikiData', - ], - sprawl: ({wikiInfo}) => ({ enableSearch: wikiInfo.enableSearch, footerContent: wikiInfo.footerContent, @@ -58,8 +40,14 @@ export default { relation('transformContent', sprawl.footerContent); } - relations.colorStyleRules = - relation('generateColorStyleRules'); + relations.colorStyleTag = + relation('generateColorStyleTag'); + + relations.staticURLStyleTag = + relation('generateStaticURLStyleTag'); + + relations.wikiWallpaperStyleTag = + relation('generateWikiWallpaperStyleTag'); relations.imageOverlay = relation('generateImageOverlay'); @@ -93,7 +81,7 @@ export default { mutable: false, }, - cover: { + artworkColumnContent: { type: 'html', mutable: false, }, @@ -107,9 +95,9 @@ export default { color: {validate: v => v.isColor}, - styleRules: { - validate: v => v.sparseArrayOf(v.isHTML), - default: [], + styleTags: { + type: 'html', + mutable: false, }, mainClasses: { @@ -262,15 +250,43 @@ export default { ? 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 - : html.isBlank(slots.additionalNames) - ? language.sanitize(slots.title) - : html.tag('a', { + + : (!html.isBlank(slots.additionalNames) && + !html.resolve(slots.additionalNames, {slots: ['alwaysVisible']}) + .getSlotValue('alwaysVisible')) + + ? html.tag('a', { href: '#additional-names-box', title: language.$('misc.additionalNames.tooltip').toString(), - }, language.sanitize(slots.title))); + }, language.sanitize(slots.title)) + + : language.sanitize(slots.title)); const titleHTML = (html.isBlank(slots.title) @@ -279,7 +295,7 @@ export default { ? [ relations.stickyHeadingContainer.slots({ title: titleContentsHTML, - cover: slots.cover, + cover: primaryCover, }), relations.stickyHeadingContainer.clone().slots({ @@ -316,9 +332,11 @@ export default { [ 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, @@ -361,7 +379,7 @@ export default { slots.navLinks ?.filter(Boolean) - ?.map((cur, i) => { + ?.map((cur, i, entries) => { let content; if (cur.html) { @@ -395,20 +413,13 @@ export default { (slots.navLinkStyle === 'hierarchical' && i === slots.navLinks.length - 1); - return ( + const navLink = html.tag('span', {class: 'nav-link'}, showAsCurrent && {class: 'current'}, [ html.tag('span', {class: 'nav-link-content'}, - // Use inline-block styling on the content span, - // rather than wrapping the whole nav-link in a proper - // blockwrap, so that if the content spans multiple - // lines, it'll kick the accent down beneath it. - i > 0 && - {class: 'blockwrap'}, - content), html.tag('span', {class: 'nav-link-accent'}, @@ -419,7 +430,25 @@ export default { [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'}, @@ -545,24 +574,33 @@ export default { {id: 'additional-files', string: 'additionalFiles'}, {id: 'commentary', string: 'commentary'}, {id: 'artist-commentary', string: 'artistCommentary'}, - {id: 'credit-sources', string: 'creditSources'}, + {id: 'crediting-sources', string: 'creditingSources'}, + {id: 'referencing-sources', string: 'referencingSources'}, ])), ]); - const styleRulesCSS = - html.resolve(slots.styleRules, {normalize: 'string'}); + const slottedStyleTags = + html.smush(slots.styleTags); + + const slottedWallpaperStyleTag = + slottedStyleTags.content + .find(tag => tag.attributes.has('class', 'wallpaper-style')); + + const fallbackWallpaperStyleTag = + (slottedWallpaperStyleTag + ? html.blank() + : relations.wikiWallpaperStyleTag); - const fallbackBackgroundStyleRule = - (styleRulesCSS.match(/body::before[^}]*background-image:/) - ? '' - : `body::before {\n` + - ` background-image: url("${to('media.path', 'bg.jpg')}");\n` + - `}`); + const usingWallpaperStyleTag = + (slottedWallpaperStyleTag + ? slottedWallpaperStyleTag + : html.resolve(fallbackWallpaperStyleTag, {normalize: 'tag'})); const numWallpaperParts = - html.resolve(slots.styleRules, {normalize: 'string'}) - .match(/\.wallpaper-part:nth-child/g) - ?.length ?? 0; + (usingWallpaperStyleTag && + usingWallpaperStyleTag.attributes.has('data-wallpaper-mode', 'parts') + ? parseInt(usingWallpaperStyleTag.attributes.get('data-num-wallpaper-parts')) + : 0); const wallpaperPartsHTML = html.tag('div', {class: 'wallpaper-parts'}, @@ -618,11 +656,25 @@ export default { language.encapsulate('misc.pageTitle', workingCapsule => { const workingOptions = {}; - workingOptions.title = slots.title; + // Slightly jank: The output of striptags is, of course, a string, + // and as far as language.formatString() is concerned, that means + // it needs to be sanitized - including turning ampersands into + // &'s. But the title is already HTML that has implicitly been + // sanitized, however it got here, and includes HTML entities that + // are properly escaped. Those need to get included as they are, + // so we wrap the title in a tag and pass it off as good to go. + workingOptions.title = + html.tags([ + striptags(slots.title.toString()), + ]); if (!html.isBlank(slots.subtitle)) { + // Same shenanigans here, as far as wrapping striptags goes. workingCapsule += '.withSubtitle'; - workingOptions.subtitle = slots.subtitle; + workingOptions.subtitle = + html.tags([ + striptags(slots.subtitle.toString()), + ]); } const showWikiName = @@ -704,13 +756,14 @@ export default { href: to('staticCSS.path', 'site.css'), }), - html.tag('style', [ - relations.colorStyleRules - .slot('color', slots.color ?? data.wikiColor), + relations.colorStyleTag + .slot('color', slots.color ?? data.wikiColor), - fallbackBackgroundStyleRule, - slots.styleRules, - ]), + relations.staticURLStyleTag, + + fallbackWallpaperStyleTag, + + slottedStyleTags, html.tag('script', { src: to('staticLib.path', 'chroma-js/chroma.min.js'), diff --git a/src/content/dependencies/generatePageSidebar.js b/src/content/dependencies/generatePageSidebar.js index d3b55580..dfe85632 100644 --- a/src/content/dependencies/generatePageSidebar.js +++ b/src/content/dependencies/generatePageSidebar.js @@ -1,6 +1,4 @@ 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 diff --git a/src/content/dependencies/generatePageSidebarBox.js b/src/content/dependencies/generatePageSidebarBox.js index 26b30494..3133aa64 100644 --- a/src/content/dependencies/generatePageSidebarBox.js +++ b/src/content/dependencies/generatePageSidebarBox.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { content: { type: 'html', diff --git a/src/content/dependencies/generatePageSidebarConjoinedBox.js b/src/content/dependencies/generatePageSidebarConjoinedBox.js index 7974c707..4ed0ff22 100644 --- a/src/content/dependencies/generatePageSidebarConjoinedBox.js +++ b/src/content/dependencies/generatePageSidebarConjoinedBox.js @@ -4,9 +4,6 @@ // templates' resolved content), take care when slotting into this. export default { - contentDependencies: ['generatePageSidebarBox'], - extraDependencies: ['html'], - relations: (relation) => ({ box: relation('generatePageSidebarBox'), diff --git a/src/content/dependencies/generatePreviousLink.js b/src/content/dependencies/generatePreviousLink.js index 775367f9..29146a21 100644 --- a/src/content/dependencies/generatePreviousLink.js +++ b/src/content/dependencies/generatePreviousLink.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['generatePreviousNextLink'], - relations: (relation) => ({ link: relation('generatePreviousNextLink'), diff --git a/src/content/dependencies/generatePreviousNextLink.js b/src/content/dependencies/generatePreviousNextLink.js index afae1228..1e98358f 100644 --- a/src/content/dependencies/generatePreviousNextLink.js +++ b/src/content/dependencies/generatePreviousNextLink.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - slots: { link: { type: 'html', diff --git a/src/content/dependencies/generateQuickDescription.js b/src/content/dependencies/generateQuickDescription.js index e144503e..f67f9514 100644 --- a/src/content/dependencies/generateQuickDescription.js +++ b/src/content/dependencies/generateQuickDescription.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['transformContent'], - extraDependencies: ['html', 'language'], - query: (thing) => ({ hasDescription: !!thing.description, diff --git a/src/content/dependencies/generateReadCommentaryLine.js b/src/content/dependencies/generateReadCommentaryLine.js new file mode 100644 index 00000000..05700536 --- /dev/null +++ b/src/content/dependencies/generateReadCommentaryLine.js @@ -0,0 +1,43 @@ +import {empty} from '#sugar'; + +export default { + query: (thing) => ({ + entries: + (thing.isTrack + ? [...thing.commentary, ...thing.commentaryFromMainRelease] + : thing.commentary), + }), + + data: (query, _thing) => ({ + hasWikiEditorCommentary: + query.entries.some(entry => entry.isWikiEditorCommentary), + + onlyWikiEditorCommentary: + !empty(query.entries) && + query.entries.every(entry => entry.isWikiEditorCommentary), + + hasAnyCommentary: + !empty(query.entries), + }), + + generate: (data, {html, language}) => + language.encapsulate('releaseInfo.readCommentary', capsule => + language.$(capsule, { + [language.onlyIfOptions]: ['link'], + + link: + html.tag('a', + {[html.onlyIfContent]: true}, + + {href: '#artist-commentary'}, + + language.encapsulate(capsule, 'link', capsule => + (data.onlyWikiEditorCommentary + ? language.$(capsule, 'onlyWikiCommentary') + : data.hasWikiEditorCommentary + ? language.$(capsule, 'withWikiCommentary') + : data.hasAnyCommentary + ? language.$(capsule) + : html.blank()))), + })), +}; diff --git a/src/content/dependencies/generateReferencedArtworksPage.js b/src/content/dependencies/generateReferencedArtworksPage.js index 3d21b15d..2f47b7a5 100644 --- a/src/content/dependencies/generateReferencedArtworksPage.js +++ b/src/content/dependencies/generateReferencedArtworksPage.js @@ -1,67 +1,45 @@ -import {stitchArrays} from '#sugar'; - export default { - contentDependencies: [ - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - - relations: (relation, referencedArtworks) => ({ + relations: (relation, artwork) => ({ layout: relation('generatePageLayout'), + cover: + relation('generateCoverArtwork', artwork), + coverGrid: relation('generateCoverGrid'), links: - referencedArtworks.map(({thing}) => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))), + artwork.referencedArtworks.map(({artwork}) => + relation('linkAnythingMan', artwork.thing)), images: - referencedArtworks.map(({thing}) => - relation('image', thing.artTags)), + artwork.referencedArtworks.map(({artwork}) => + relation('image', artwork)), }), - data: (referencedArtworks) => ({ + data: (artwork) => ({ + color: + artwork.thing.color, + count: - referencedArtworks.length, + artwork.referencedArtworks.length, names: - referencedArtworks - .map(({thing}) => thing.name), - - paths: - referencedArtworks - .map(({thing}) => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])), - - dimensions: - referencedArtworks - .map(({thing}) => thing.coverArtDimensions), + artwork.referencedArtworks + .map(({artwork}) => artwork.thing.name), coverArtistNames: - referencedArtworks - .map(({thing}) => - thing.coverArtistContribs + artwork.referencedArtworks + .map(({artwork}) => + artwork.artistContribs .map(contrib => contrib.artist.name)), }), slots: { - color: {validate: v => v.isColor}, - - styleRules: {type: 'html', mutable: false}, + styleTags: {type: 'html', mutable: false}, title: {type: 'html', mutable: false}, - cover: {type: 'html', mutable: true}, navLinks: {validate: v => v.isArray}, navBottomRowContent: {type: 'html', mutable: false}, @@ -73,11 +51,13 @@ export default { title: slots.title, subtitle: language.$(pageCapsule, 'subtitle'), - color: slots.color, - styleRules: slots.styleRules, + color: data.color, + styleTags: slots.styleTags, - cover: - slots.cover.slot('details', 'artists'), + artworkColumnContent: + relations.cover.slots({ + showArtistDetails: true, + }), mainClasses: ['top-index'], mainContent: [ @@ -91,19 +71,9 @@ export default { relations.coverGrid.slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, - })), - info: data.coverArtistNames.map(names => language.$('misc.coverGrid.details.coverArtists', { diff --git a/src/content/dependencies/generateReferencedTracksList.js b/src/content/dependencies/generateReferencedTracksList.js new file mode 100644 index 00000000..1d566ce9 --- /dev/null +++ b/src/content/dependencies/generateReferencedTracksList.js @@ -0,0 +1,29 @@ +export default { + relations: (relation, track) => ({ + previousProductionTrackList: + relation('generateNearbyTrackList', + track.previousProductionTracks, + track, + track.artistContribs), + + referencedTrackList: + relation('generateNearbyTrackList', + track.referencedTracks, + track, + []), + }), + + generate: (relations, {html, language}) => + html.tag('ul', {[html.onlyIfContent]: true}, [ + html.inside(relations.previousProductionTrackList) + .map(li => html.inside(li)) + .map(label => + html.tag('li', + language.$('trackList.item.previousProduction', + {track: label}))), + + html.inside(relations.referencedTrackList), + ]), +}; + + diff --git a/src/content/dependencies/generateReferencingArtworksPage.js b/src/content/dependencies/generateReferencingArtworksPage.js index 2fe2e93d..abb92732 100644 --- a/src/content/dependencies/generateReferencingArtworksPage.js +++ b/src/content/dependencies/generateReferencingArtworksPage.js @@ -1,67 +1,45 @@ -import {stitchArrays} from '#sugar'; - export default { - contentDependencies: [ - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - - relations: (relation, referencingArtworks) => ({ + relations: (relation, artwork) => ({ layout: relation('generatePageLayout'), + cover: + relation('generateCoverArtwork', artwork), + coverGrid: relation('generateCoverGrid'), links: - referencingArtworks.map(({thing}) => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))), + artwork.referencedByArtworks.map(({artwork}) => + relation('linkAnythingMan', artwork.thing)), images: - referencingArtworks.map(({thing}) => - relation('image', thing.artTags)), + artwork.referencedByArtworks.map(({artwork}) => + relation('image', artwork)), }), - data: (referencingArtworks) => ({ + data: (artwork) => ({ + color: + artwork.thing.color, + count: - referencingArtworks.length, + artwork.referencedByArtworks.length, names: - referencingArtworks - .map(({thing}) => thing.name), - - paths: - referencingArtworks - .map(({thing}) => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])), - - dimensions: - referencingArtworks - .map(({thing}) => thing.coverArtDimensions), + artwork.referencedByArtworks + .map(({artwork}) => artwork.thing.name), coverArtistNames: - referencingArtworks - .map(({thing}) => - thing.coverArtistContribs + artwork.referencedByArtworks + .map(({artwork}) => + artwork.artistContribs .map(contrib => contrib.artist.name)), }), slots: { - color: {validate: v => v.isColor}, - - styleRules: {type: 'html', mutable: false}, + styleTags: {type: 'html', mutable: false}, title: {type: 'html', mutable: false}, - cover: {type: 'html', mutable: true}, navLinks: {validate: v => v.isArray}, navBottomRowContent: {type: 'html', mutable: false}, @@ -73,11 +51,13 @@ export default { title: slots.title, subtitle: language.$(pageCapsule, 'subtitle'), - color: slots.color, - styleRules: slots.styleRules, + color: data.color, + styleTags: slots.styleTags, - cover: - slots.cover.slot('details', 'artists'), + artworkColumnContent: + relations.cover.slots({ + showArtistDetails: true, + }), mainClasses: ['top-index'], mainContent: [ @@ -91,19 +71,9 @@ export default { relations.coverGrid.slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, - })), - info: data.coverArtistNames.map(names => language.$('misc.coverGrid.details.coverArtists', { diff --git a/src/content/dependencies/generateRelativeDatetimestamp.js b/src/content/dependencies/generateRelativeDatetimestamp.js index a997de0e..1415564e 100644 --- a/src/content/dependencies/generateRelativeDatetimestamp.js +++ b/src/content/dependencies/generateRelativeDatetimestamp.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateDatetimestampTemplate', - 'generateTooltip', - ], - - extraDependencies: ['html', 'language'], - data: (currentDate, referenceDate) => (currentDate.getTime() === referenceDate.getTime() ? {equal: true, date: currentDate} @@ -28,19 +20,11 @@ export default { validate: v => v.is('full', 'year'), default: 'full', }, - - tooltip: { - type: 'boolean', - default: false, - }, }, generate(data, relations, slots, {language}) { if (data.equal) { - return relations.fallback.slots({ - style: slots.style, - tooltip: slots.tooltip, - }); + return relations.fallback.slot('style', slots.style); } return relations.template.slots({ @@ -52,15 +36,14 @@ export default { : null), tooltip: - slots.tooltip && - relations.tooltip.slots({ - content: - language.formatRelativeDate(data.currentDate, data.referenceDate, { - considerRoundingDays: true, - approximate: true, - absolute: slots.style === 'year', - }), - }), + relations.tooltip.slots({ + content: + language.formatRelativeDate(data.currentDate, data.referenceDate, { + considerRoundingDays: true, + approximate: true, + absolute: slots.style === 'year', + }), + }), datetime: data.currentDate.toISOString(), diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js index 016e0a2c..4353ccf4 100644 --- a/src/content/dependencies/generateReleaseInfoContributionsLine.js +++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js @@ -1,16 +1,15 @@ export default { - contentDependencies: ['generateArtistCredit'], - extraDependencies: ['html'], - - relations: (relation, contributions) => ({ + relations: (relation, contributions, formatText) => ({ credit: - relation('generateArtistCredit', contributions, []), + relation('generateArtistCredit', contributions, [], formatText), }), slots: { stringKey: {type: 'string'}, featuringStringKey: {type: 'string'}, + additionalStringOptions: {validate: v => v.isObject}, + chronologyKind: {type: 'string'}, }, @@ -27,5 +26,6 @@ export default { normalStringKey: slots.stringKey, normalFeaturingStringKey: slots.featuringStringKey, + additionalStringOptions: slots.additionalStringOptions, }), }; diff --git a/src/content/dependencies/generateReleaseInfoListenLine.js b/src/content/dependencies/generateReleaseInfoListenLine.js new file mode 100644 index 00000000..97f248d6 --- /dev/null +++ b/src/content/dependencies/generateReleaseInfoListenLine.js @@ -0,0 +1,156 @@ +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 { + query(thing) { + const query = {}; + + query.album = + (thing.album + ? thing.album + : thing); + + query.urls = + (!empty(thing.urls) + ? thing.urls + : thing.album && + thing.album.style === 'single' && + thing.album.tracks[0] === thing + ? thing.album.urls + : []); + + 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: + query.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 = + query.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 = + query.urls.map(() => null); + } + + data.releaseContexts = releaseContexts; + + return data; + }, + + slots: { + visibleWithoutLinks: { + type: 'boolean', + default: false, + }, + + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('releaseInfo.listenOn', capsule => + (empty(relations.links) && slots.visibleWithoutLinks + ? language.$(capsule, 'noLinks', { + name: + html.tag('i', data.name), + }) + + : language.$('releaseInfo.listenOn', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatDisjunctionList( + stitchArrays({ + link: relations.links, + releaseContext: data.releaseContexts, + }).map(({link, releaseContext}) => + link.slot('context', [ + ... + (Array.isArray(slots.context) + ? slots.context + : [slots.context]), + + releaseContext, + ]))), + }))), +}; diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js index 188a678f..701a01ac 100644 --- a/src/content/dependencies/generateSearchSidebarBox.js +++ b/src/content/dependencies/generateSearchSidebarBox.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generatePageSidebarBox'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ sidebarBox: relation('generatePageSidebarBox'), @@ -57,6 +54,43 @@ export default { html.tag('template', {class: 'wiki-search-tag-result-kind-string'}, language.$(capsule, 'artTag')), ]), + + language.encapsulate(capsule, 'resultDisambiguator', capsule => [ + html.tag('template', {class: 'wiki-search-group-result-disambiguator-string'}, + language.$(capsule, 'group', { + disambiguator: html.tag('slot', {name: 'disambiguator'}), + })), + + html.tag('template', {class: 'wiki-search-flash-result-disambiguator-string'}, + language.$(capsule, 'flash', { + disambiguator: html.tag('slot', {name: 'disambiguator'}), + })), + + html.tag('template', {class: 'wiki-search-track-result-disambiguator-string'}, + language.$(capsule, 'track', { + disambiguator: html.tag('slot', {name: 'disambiguator'}), + })), + ]), + + 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 9ce7ce9b..63b3839b 100644 --- a/src/content/dependencies/generateSecondaryNav.js +++ b/src/content/dependencies/generateSecondaryNav.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { content: { type: 'html', diff --git a/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js b/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js index f204f1fb..fe7c17ac 100644 --- a/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js +++ b/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js @@ -1,15 +1,4 @@ export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkAlbumDynamically', - 'linkGroup', - ], - - extraDependencies: ['html', 'language'], - relations: (relation) => ({ switcher: relation('generateInterpageDotSwitcher'), diff --git a/src/content/dependencies/generateSocialEmbed.js b/src/content/dependencies/generateSocialEmbed.js index 513ea518..5fa9376c 100644 --- a/src/content/dependencies/generateSocialEmbed.js +++ b/src/content/dependencies/generateSocialEmbed.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['absoluteTo', 'html', 'language', 'wikiData'], - sprawl({wikiInfo}) { return { canonicalBase: wikiInfo.canonicalBase, diff --git a/src/content/dependencies/generateStaticPage.js b/src/content/dependencies/generateStaticPage.js index 226152c7..485b802e 100644 --- a/src/content/dependencies/generateStaticPage.js +++ b/src/content/dependencies/generateStaticPage.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generatePageLayout', 'transformContent'], - extraDependencies: ['html'], - relations(relation, staticPage) { return { layout: relation('generatePageLayout'), @@ -23,17 +20,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..443a4d08 --- /dev/null +++ b/src/content/dependencies/generateStaticURLStyleTag.js @@ -0,0 +1,20 @@ +export default { + 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 ec3062a3..f7388d60 100644 --- a/src/content/dependencies/generateStickyHeadingContainer.js +++ b/src/content/dependencies/generateStickyHeadingContainer.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { rootAttributes: { type: 'attributes', diff --git a/src/content/dependencies/generateStyleTag.js b/src/content/dependencies/generateStyleTag.js new file mode 100644 index 00000000..cdeadcfe --- /dev/null +++ b/src/content/dependencies/generateStyleTag.js @@ -0,0 +1,46 @@ +import {empty} from '#sugar'; + +const indent = text => + text + .split('\n') + .map(line => ' '.repeat(4) + line) + .join('\n'); + +export default { + 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 49ce1f61..360cfebc 100644 --- a/src/content/dependencies/generateTextWithTooltip.js +++ b/src/content/dependencies/generateTextWithTooltip.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { attributes: { type: 'attributes', diff --git a/src/content/dependencies/generateTooltip.js b/src/content/dependencies/generateTooltip.js index b09ee230..6f23af6d 100644 --- a/src/content/dependencies/generateTooltip.js +++ b/src/content/dependencies/generateTooltip.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { attributes: { type: 'attributes', diff --git a/src/content/dependencies/generateTrackArtistCommentarySection.js b/src/content/dependencies/generateTrackArtistCommentarySection.js index e3041d3a..39a3e145 100644 --- a/src/content/dependencies/generateTrackArtistCommentarySection.js +++ b/src/content/dependencies/generateTrackArtistCommentarySection.js @@ -1,15 +1,6 @@ import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateCommentaryEntry', - 'generateContentHeading', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - query: (track) => ({ otherSecondaryReleasesWithCommentary: track.otherReleases @@ -18,8 +9,8 @@ export default { }), relations: (relation, query, track) => ({ - contentHeading: - relation('generateContentHeading'), + commentaryContentHeading: + relation('generateCommentaryContentHeading', track), mainReleaseTrackLink: (track.isSecondaryRelease @@ -28,7 +19,7 @@ export default { mainReleaseArtistCommentaryEntries: (track.isSecondaryRelease - ? track.mainReleaseTrack.commentary + ? track.commentaryFromMainRelease .map(entry => relation('generateCommentaryEntry', entry)) : null), @@ -78,54 +69,40 @@ export default { generate: (data, relations, {html, language}) => language.encapsulate('misc.artistCommentary', capsule => html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), - }), + relations.commentaryContentHeading, + relations.artistCommentaryEntries, data.isSecondaryRelease && - html.tags([ - html.tag('p', {class: ['drop', 'commentary-drop']}, - {[html.onlyIfSiblings]: true}, - - language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => { - const workingOptions = {}; - - workingOptions.album = - relations.mainReleaseTrackLink.slots({ - content: - data.mainReleaseAlbumName, - - color: - data.mainReleaseAlbumColor, - }); - - if (data.name !== data.mainReleaseName) { - workingCapsule += '.namedDifferently'; - workingOptions.name = - html.tag('i', data.mainReleaseName); - } - - return language.$(workingCapsule, workingOptions); - })), - - relations.mainReleaseArtistCommentaryEntries, - ]), - - html.tags([ - data.isSecondaryRelease && - !html.isBlank(relations.mainReleaseArtistCommentaryEntries) && - html.tag('p', {class: ['drop', 'commentary-drop']}, - {[html.onlyIfSiblings]: true}, - - language.$(capsule, 'info.releaseSpecific', { - album: - relations.thisReleaseAlbumLink, - })), - - relations.artistCommentaryEntries, - ]), + html.tag('div', {class: 'inherited-commentary-section'}, + {[html.onlyIfContent]: true}, + + [ + html.tag('p', {class: ['drop', 'commentary-drop']}, + {[html.onlyIfSiblings]: true}, + + language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => { + const workingOptions = {}; + + workingOptions.album = + relations.mainReleaseTrackLink.slots({ + content: + data.mainReleaseAlbumName, + + color: + data.mainReleaseAlbumColor, + }); + + if (data.name !== data.mainReleaseName) { + workingCapsule += '.namedDifferently'; + workingOptions.name = + html.tag('i', data.mainReleaseName); + } + + return language.$(workingCapsule, workingOptions); + })), + + relations.mainReleaseArtistCommentaryEntries, + ]), html.tag('p', {class: ['drop', 'commentary-drop']}, {[html.onlyIfContent]: true}, diff --git a/src/content/dependencies/generateTrackArtworkColumn.js b/src/content/dependencies/generateTrackArtworkColumn.js new file mode 100644 index 00000000..234586e0 --- /dev/null +++ b/src/content/dependencies/generateTrackArtworkColumn.js @@ -0,0 +1,30 @@ +export default { + 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 9153e2fc..00000000 --- a/src/content/dependencies/generateTrackCoverArtwork.js +++ /dev/null @@ -1,143 +0,0 @@ -export default { - contentDependencies: [ - 'generateCoverArtwork', - 'generateCoverArtworkArtTagDetails', - 'generateCoverArtworkArtistDetails', - 'generateCoverArtworkReferenceDetails', - 'image', - 'linkAlbum', - 'linkTrackReferencedArtworks', - 'linkTrackReferencingArtworks', - ], - - extraDependencies: ['html', 'language'], - - query: (track) => ({ - artTags: - (track.hasUniqueCoverArt - ? track.artTags - : track.album.artTags), - - coverArtistContribs: - (track.hasUniqueCoverArt - ? track.coverArtistContribs - : track.album.coverArtistContribs), - }), - - relations: (relation, query, track) => ({ - coverArtwork: - relation('generateCoverArtwork'), - - image: - relation('image'), - - artTagDetails: - relation('generateCoverArtworkArtTagDetails', - query.artTags), - - artistDetails: - relation('generateCoverArtworkArtistDetails', - query.coverArtistContribs), - - referenceDetails: - relation('generateCoverArtworkReferenceDetails', - track.referencedArtworks, - track.referencedByArtworks), - - referencedArtworksLink: - relation('linkTrackReferencedArtworks', track), - - referencingArtworksLink: - relation('linkTrackReferencingArtworks', track), - - albumLink: - relation('linkAlbum', track.album), - }), - - data: (query, track) => ({ - path: - (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : ['media.albumCover', track.album.directory, track.album.coverArtFileExtension]), - - color: - track.color, - - dimensions: - (track.hasUniqueCoverArt - ? track.coverArtDimensions - : track.album.coverArtDimensions), - - nonUnique: - !track.hasUniqueCoverArt, - - warnings: - query.artTags - .filter(tag => tag.isContentWarning) - .map(tag => tag.name), - }), - - slots: { - mode: {type: 'string'}, - - details: { - validate: v => v.is('tags', 'artists'), - default: 'tags', - }, - - showReferenceLinks: { - type: 'boolean', - default: false, - }, - - showNonUniqueLine: { - type: 'boolean', - default: false, - }, - }, - - generate: (data, relations, slots, {html, language}) => - relations.coverArtwork.slots({ - mode: slots.mode, - - image: - relations.image.slots({ - path: data.path, - color: data.color, - alt: language.$('misc.alt.trackCover'), - }), - - dimensions: data.dimensions, - warnings: data.warnings, - - details: [ - slots.details === 'tags' && - relations.artTagDetails, - - slots.details === 'artists'&& - relations.artistDetails, - - slots.showReferenceLinks && - relations.referenceDetails.slots({ - referencedLink: - relations.referencedArtworksLink, - - referencingLink: - relations.referencingArtworksLink, - }), - - slots.showNonUniqueLine && - data.nonUnique && - html.tag('p', {class: 'image-details'}, - {class: 'non-unique-details'}, - - language.$('misc.trackArtFromAlbum', { - album: - relations.albumLink.slots({ - color: false, - }), - })), - ], - }), -}; - diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 1c349c2e..d3c2d766 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -1,44 +1,46 @@ -export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateAlbumAdditionalFilesList', - 'generateAlbumNavAccent', - 'generateAlbumSecondaryNav', - 'generateAlbumSidebar', - 'generateAlbumStyleRules', - 'generateCommentaryEntry', - 'generateContentHeading', - 'generateContributionList', - 'generatePageLayout', - 'generateTrackArtistCommentarySection', - 'generateTrackCoverArtwork', - 'generateTrackInfoPageFeaturedByFlashesList', - 'generateTrackInfoPageOtherReleasesList', - 'generateTrackList', - 'generateTrackListDividedByGroups', - 'generateTrackNavLinks', - 'generateTrackReleaseInfo', - 'generateTrackSocialEmbed', - 'linkAlbum', - 'linkTrack', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], +function checkInterrupted(which, relations, {html}) { + if ( + !html.isBlank(relations.additionalFilesList) || + !html.isBlank(relations.contributorContributionList) || + !html.isBlank(relations.flashesThatFeatureList) || + !html.isBlank(relations.lyricsSection) || + !html.isBlank(relations.midiProjectFilesList) || + !html.isBlank(relations.referencedByTracksList) || + !html.isBlank(relations.referencedTracksList) || + !html.isBlank(relations.sampledByTracksList) || + !html.isBlank(relations.sampledTracksList) || + !html.isBlank(relations.sheetMusicFilesList) + ) return true; + + if (which === 'crediting-sources' || which === 'referencing-sources') { + if (!html.isBlank(relations.artistCommentarySection)) return true; + } + + return false; +} +export default { query: (track) => ({ mainReleaseTrack: (track.isMainRelease ? track : track.mainReleaseTrack), + + singleTrackSingle: + track.album.style === 'single' && + track.album.tracks.length === 1, + + firstTrackInSingle: + track.album.style === 'single' && + track === track.album.tracks[0], }), relations: (relation, query, track) => ({ layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), socialEmbed: relation('generateTrackSocialEmbed', track), @@ -46,6 +48,9 @@ export default { navLinks: relation('generateTrackNavLinks', track), + albumNavLink: + relation('linkAlbum', track.album), + albumNavAccent: relation('generateAlbumNavAccent', track.album, track), @@ -58,72 +63,95 @@ export default { additionalNamesBox: relation('generateAdditionalNamesBox', track.additionalNames), - cover: - (track.hasUniqueCoverArt || track.album.hasCoverArt - ? relation('generateTrackCoverArtwork', track) - : null), + artworkColumn: + (query.firstTrackInSingle + ? relation('generateAlbumArtworkColumn', track.album) + : relation('generateTrackArtworkColumn', track)), contentHeading: relation('generateContentHeading'), + name: + relation('generateName', track), + releaseInfo: relation('generateTrackReleaseInfo', track), - otherReleasesList: - relation('generateTrackInfoPageOtherReleasesList', track), + readCommentaryLine: + relation('generateReadCommentaryLine', track), + + otherReleasesLine: + relation('generateTrackInfoPageOtherReleasesLine', track), + + previousProductionLine: + relation('generateTrackInfoPagePreviousProductionLine', track), contributorContributionList: relation('generateContributionList', track.contributorContribs), referencedTracksList: - relation('generateTrackList', track.referencedTracks), + relation('generateReferencedTracksList', track), sampledTracksList: - relation('generateTrackList', track.sampledTracks), + relation('generateNearbyTrackList', track.sampledTracks, track, []), referencedByTracksList: relation('generateTrackListDividedByGroups', - query.mainReleaseTrack.referencedByTracks), + query.mainReleaseTrack.referencedByTracks, + track), sampledByTracksList: relation('generateTrackListDividedByGroups', - query.mainReleaseTrack.sampledByTracks), + query.mainReleaseTrack.sampledByTracks, + track), flashesThatFeatureList: relation('generateTrackInfoPageFeaturedByFlashesList', track), - lyrics: - relation('transformContent', track.lyrics), + lyricsSection: + relation('generateLyricsSection', track.lyrics), sheetMusicFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.sheetMusicFiles), + relation('generateAdditionalFilesList', track.sheetMusicFiles), midiProjectFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.midiProjectFiles), + relation('generateAdditionalFilesList', track.midiProjectFiles), additionalFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.additionalFiles), + relation('generateAdditionalFilesList', track.additionalFiles), artistCommentarySection: relation('generateTrackArtistCommentarySection', track), - creditSourceEntries: - track.creditSources - .map(entry => relation('generateCommentaryEntry', entry)), + creditingSourcesSection: + relation('generateCollapsedContentEntrySection', + track.creditingSources, + track), + + referencingSourcesSection: + relation('generateCollapsedContentEntrySection', + track.referencingSources, + track), }), - data: (_query, track) => ({ + data: (query, track) => ({ name: track.name, color: track.color, + + dateAlbumAddedToWiki: + track.album.dateAddedToWiki, + + needsLyrics: + track.needsLyrics, + + singleTrackSingle: + query.singleTrackSingle, + + firstTrackInSingle: + query.firstTrackInSingle, }), generate: (data, relations, {html, language}) => @@ -131,7 +159,7 @@ export default { relations.layout.slots({ title: language.$(pageCapsule, 'title', { - track: data.name, + track: relations.name, }), headingMode: 'sticky', @@ -139,15 +167,10 @@ export default { additionalNames: relations.additionalNamesBox, color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, - cover: - (relations.cover - ? relations.cover.slots({ - showReferenceLinks: true, - showNonUniqueLine: true, - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ relations.releaseInfo, @@ -184,26 +207,35 @@ export default { language.$(capsule, 'link')), })), - !html.isBlank(relations.artistCommentarySection) && - language.encapsulate(capsule, 'readCommentary', capsule => + checkInterrupted('commentary', relations, {html}) && + relations.readCommentaryLine, + + !html.isBlank(relations.creditingSourcesSection) && + checkInterrupted('crediting-sources', relations, {html}) && + language.encapsulate(capsule, 'readCreditingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#artist-commentary'}, + {href: '#crediting-sources'}, language.$(capsule, 'link')), })), - !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + !html.isBlank(relations.referencingSourcesSection) && + checkInterrupted('referencing-sources', relations, {html}) && + language.encapsulate(capsule, 'readReferencingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#referencing-sources'}, language.$(capsule, 'link')), })), ])), - relations.otherReleasesList, + html.tag('p', {[html.onlyIfContent]: true}, + relations.otherReleasesLine), + + html.tag('p', {[html.onlyIfContent]: true}, + relations.previousProductionLine), html.tags([ relations.contentHeading.clone() @@ -315,17 +347,26 @@ export default { relations.flashesThatFeatureList, ]), - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'lyrics'}, - title: language.$('releaseInfo.lyrics'), - }), - - html.tag('blockquote', + data.firstTrackInSingle && + html.tag('p', {[html.onlyIfContent]: true}, - relations.lyrics.slot('mode', 'lyrics')), - ]), + + language.$('releaseInfo.addedToWiki', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.dateAlbumAddedToWiki), + })), + + data.firstTrackInSingle && + (!html.isBlank(relations.lyricsSection) || + !html.isBlank(relations.artistCommentarySection)) && + html.tag('hr', {class: 'main-separator'}), + + data.needsLyrics && + html.isBlank(relations.lyricsSection) && + html.tag('p', + language.$(pageCapsule, 'needsLyrics')), + + relations.lyricsSection, html.tags([ relations.contentHeading.clone() @@ -359,29 +400,40 @@ export default { relations.artistCommentarySection, - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), - }), + relations.creditingSourcesSection.slots({ + id: 'crediting-sources', + string: 'misc.creditingSources', + }), - relations.creditSourceEntries, - ]), + relations.referencingSourcesSection.slots({ + id: 'referencing-sources', + string: 'misc.referencingSources', + }), ], navLinkStyle: 'hierarchical', - navLinks: html.resolve(relations.navLinks), + navLinks: + (data.singleTrackSingle + ? [ + {auto: 'home'}, + { + html: relations.albumNavLink, + accent: language.$(pageCapsule, 'nav.singleAccent'), + }, + ] + : html.resolve(relations.navLinks)), navBottomRowContent: - relations.albumNavAccent.slots({ - showTrackNavigation: true, - showExtraLinks: false, - }), + (data.singleTrackSingle + ? null + : relations.albumNavAccent.slots({ + showTrackNavigation: true, + showExtraLinks: false, + })), secondaryNav: relations.secondaryNav - .slot('mode', 'track'), + .slot('mode', data.firstTrackInSingle ? 'album' : 'track'), leftSidebar: relations.sidebar, diff --git a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js index 61654512..cd7bb014 100644 --- a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js +++ b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js @@ -2,9 +2,6 @@ import {sortFlashesChronologically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['linkFlash', 'linkTrack'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ enableFlashesAndGames: wikiInfo.enableFlashesAndGames, diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesLine.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesLine.js new file mode 100644 index 00000000..1793b73f --- /dev/null +++ b/src/content/dependencies/generateTrackInfoPageOtherReleasesLine.js @@ -0,0 +1,80 @@ +import {onlyItem, stitchArrays} from '#sugar'; + +export default { + query(track) { + const query = {}; + + query.singleSingle = + onlyItem( + track.otherReleases.filter(track => track.album.style === 'single')); + + query.regularReleases = + (query.singleSingle + ? track.otherReleases.filter(track => track !== query.singleSingle) + : track.otherReleases); + + return query; + }, + + relations: (relation, query, _track) => ({ + singleLink: + (query.singleSingle + ? relation('linkTrack', query.singleSingle) + : null), + + trackLinks: + query.regularReleases + .map(track => relation('linkTrack', track)), + }), + + data: (query, _track) => ({ + albumNames: + query.regularReleases + .map(track => track.album.name), + + albumColors: + query.regularReleases + .map(track => track.album.color), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('releaseInfo.alsoReleased', capsule => + language.encapsulate(capsule, workingCapsule => { + const workingOptions = {}; + + let any = false; + + const albumList = + language.formatConjunctionList( + stitchArrays({ + trackLink: relations.trackLinks, + albumName: data.albumNames, + albumColor: data.albumColors, + }).map(({trackLink, albumName, albumColor}) => + trackLink.slots({ + content: language.sanitize(albumName), + color: albumColor, + }))); + + if (!html.isBlank(albumList)) { + any = true; + workingCapsule += '.onAlbums'; + workingOptions.albums = albumList; + } + + if (relations.singleLink) { + any = true; + workingCapsule += '.asSingle'; + workingOptions.single = + relations.singleLink.slots({ + content: language.$(capsule, 'single'), + }); + } + + if (any) { + return language.$(workingCapsule, workingOptions); + } else { + return html.blank(); + } + })), +}; diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js deleted file mode 100644 index ebd76577..00000000 --- a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js +++ /dev/null @@ -1,42 +0,0 @@ -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/generateTrackInfoPagePreviousProductionLine.js b/src/content/dependencies/generateTrackInfoPagePreviousProductionLine.js new file mode 100644 index 00000000..f7f455c1 --- /dev/null +++ b/src/content/dependencies/generateTrackInfoPagePreviousProductionLine.js @@ -0,0 +1,38 @@ +import {stitchArrays} from '#sugar'; +import {getKebabCase} from '#wiki-data'; + +export default { + relations: (relation, track) => ({ + trackLinks: + track.followingProductionTracks + .map(track => relation('linkTrack', track)), + + albumLinks: + track.followingProductionTracks + .map(following => + (following.album !== track.album && + getKebabCase(following.name) === getKebabCase(track.name) + + ? relation('linkAlbum', following.album) + : null)), + }), + + generate: (relations, {language}) => + language.encapsulate('releaseInfo.previousProduction', capsule => + language.$(capsule, { + [language.onlyIfOptions]: ['tracks'], + + tracks: + language.formatConjunctionList( + stitchArrays({ + trackLink: relations.trackLinks, + albumLink: relations.albumLinks, + }).map(({trackLink, albumLink}) => + (albumLink + ? language.$(capsule, 'trackOnAlbum', { + track: trackLink, + album: albumLink, + }) + : trackLink))), + })), +}; diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js index 53a32536..c259c914 100644 --- a/src/content/dependencies/generateTrackList.js +++ b/src/content/dependencies/generateTrackList.js @@ -1,14 +1,21 @@ export default { - contentDependencies: ['generateTrackListItem'], - extraDependencies: ['html'], - - relations: (relation, tracks) => ({ + relations: (relation, tracks, contextContributions) => ({ items: - tracks - .map(track => relation('generateTrackListItem', track, [])), + tracks.map(track => + relation('generateTrackListItem', track, contextContributions)), }), slots: { + showArtists: { + validate: v => v.is(true, false, 'auto'), + default: 'auto', + }, + + showDuration: { + type: 'boolean', + default: false, + }, + colorMode: { validate: v => v.is('none', 'track', 'line'), default: 'track', @@ -21,8 +28,8 @@ export default { relations.items.map(item => item.slots({ - showArtists: true, - showDuration: false, + showArtists: slots.showArtists, + showDuration: slots.showDuration, colorMode: slots.colorMode, }))), }; diff --git a/src/content/dependencies/generateTrackListDividedByGroups.js b/src/content/dependencies/generateTrackListDividedByGroups.js index 230868d6..419d7c0f 100644 --- a/src/content/dependencies/generateTrackListDividedByGroups.js +++ b/src/content/dependencies/generateTrackListDividedByGroups.js @@ -1,20 +1,12 @@ import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateContentHeading', - 'generateTrackList', - 'linkGroup', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ divideTrackListsByGroups: wikiInfo.divideTrackListsByGroups, }), - query(sprawl, tracks) { + query(sprawl, tracks, _contextTrack) { const dividingGroups = sprawl.divideTrackListsByGroups; const groupings = new Map(); @@ -50,10 +42,10 @@ export default { return {groups, groupedTracks, ungroupedTracks}; }, - relations: (relation, query, sprawl, tracks) => ({ + relations: (relation, query, sprawl, tracks, contextTrack) => ({ flatList: (empty(sprawl.divideTrackListsByGroups) - ? relation('generateTrackList', tracks) + ? relation('generateNearbyTrackList', tracks, contextTrack, []) : null), contentHeading: @@ -65,12 +57,12 @@ export default { groupedTrackLists: query.groupedTracks - .map(tracks => relation('generateTrackList', tracks)), + .map(tracks => relation('generateNearbyTrackList', tracks, contextTrack, [])), ungroupedTrackList: (empty(query.ungroupedTracks) ? null - : relation('generateTrackList', query.ungroupedTracks)), + : relation('generateNearbyTrackList', query.ungroupedTracks, contextTrack, [])), }), data: (query, _sprawl, _tracks) => ({ diff --git a/src/content/dependencies/generateTrackListItem.js b/src/content/dependencies/generateTrackListItem.js index 887b6f03..c8c57534 100644 --- a/src/content/dependencies/generateTrackListItem.js +++ b/src/content/dependencies/generateTrackListItem.js @@ -1,21 +1,19 @@ export default { - contentDependencies: [ - 'generateArtistCredit', - 'generateColorStyleAttribute', - 'generateTrackListMissingDuration', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, track, contextContributions) => ({ trackLink: relation('linkTrack', track), - credit: + contextualCredit: + relation('generateArtistCredit', + track.artistContribs, + contextContributions, + track.artistTextInLists), + + acontextualCredit: relation('generateArtistCredit', track.artistContribs, - contextContributions), + [], + track.artistTextInLists), colorStyle: relation('generateColorStyleAttribute', track.color), @@ -35,12 +33,11 @@ export default { }), slots: { - // showArtists enables showing artists *at all.* It doesn't take precedence - // over behavior which automatically collapses (certain) artists because of - // provided context contributions. + // true always shows artists, false never does; 'auto' shows only if + // the track's artists differ from the given context contributions. showArtists: { - type: 'boolean', - default: true, + validate: v => v.is(true, false, 'auto'), + default: 'auto', }, // If true and the track doesn't have a duration, a missing-duration cue @@ -80,25 +77,33 @@ export default { : 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'}, - html.metatag('chunkwrap', {split: ','}, - html.resolve(relations.credit))); + const chosenCredit = + (slots.showArtists === true + ? relations.acontextualCredit + : slots.showArtists === 'auto' + ? relations.contextualCredit + : null); + + if (chosenCredit) { + const artistCapsule = language.encapsulate(itemCapsule, 'withArtists'); + + chosenCredit.setSlots({ + normalStringKey: + artistCapsule + '.by', + + featuringStringKey: + artistCapsule + '.featuring', + + normalFeaturingStringKey: + artistCapsule + '.by.featuring', + }); + + if (!html.isBlank(chosenCredit)) { + workingCapsule += '.withArtists'; + workingOptions.by = + html.tag('span', {class: 'by'}, + chosenCredit); + } } return language.$(workingCapsule, workingOptions); diff --git a/src/content/dependencies/generateTrackListMissingDuration.js b/src/content/dependencies/generateTrackListMissingDuration.js index b5917982..da3113a2 100644 --- a/src/content/dependencies/generateTrackListMissingDuration.js +++ b/src/content/dependencies/generateTrackListMissingDuration.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateTextWithTooltip', 'generateTooltip'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ textWithTooltip: relation('generateTextWithTooltip'), diff --git a/src/content/dependencies/generateTrackNavLinks.js b/src/content/dependencies/generateTrackNavLinks.js index e01653f0..d18e6cad 100644 --- a/src/content/dependencies/generateTrackNavLinks.js +++ b/src/content/dependencies/generateTrackNavLinks.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkAlbum', 'linkTrack'], - extraDependencies: ['html', 'language'], - relations: (relation, track) => ({ albumLink: relation('linkAlbum', track.album), @@ -11,11 +8,14 @@ export default { }), data: (track) => ({ + albumStyle: + track.album.style, + hasTrackNumbers: track.album.hasTrackNumbers, trackNumber: - track.album.tracks.indexOf(track) + 1, + track.trackNumber, }), slots: { @@ -28,7 +28,13 @@ export default { language.encapsulate('trackPage.nav', navCapsule => [ {auto: 'home'}, - {html: relations.albumLink.slot('color', false)}, + { + html: relations.albumLink.slot('color', false), + accent: + (data.albumStyle === 'single' + ? language.$(navCapsule, 'singleAccent') + : null), + }, { html: diff --git a/src/content/dependencies/generateTrackReferencedArtworksPage.js b/src/content/dependencies/generateTrackReferencedArtworksPage.js index ac81e525..a2612067 100644 --- a/src/content/dependencies/generateTrackReferencedArtworksPage.js +++ b/src/content/dependencies/generateTrackReferencedArtworksPage.js @@ -1,37 +1,21 @@ export default { - contentDependencies: [ - 'generateAlbumStyleRules', - 'generateBackToTrackLink', - 'generateReferencedArtworksPage', - 'generateTrackCoverArtwork', - 'generateTrackNavLinks', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, track) => ({ page: - relation('generateReferencedArtworksPage', track.referencedArtworks), + relation('generateReferencedArtworksPage', track.trackArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), navLinks: relation('generateTrackNavLinks', track), backToTrackLink: relation('generateBackToTrackLink', track), - - cover: - relation('generateTrackCoverArtwork', track), }), data: (track) => ({ name: track.name, - - color: - track.color, }), generate: (data, relations, {html, language}) => @@ -42,10 +26,7 @@ export default { data.name, }), - color: data.color, - styleRules: [relations.albumStyleRules], - - cover: relations.cover, + styleTags: relations.albumStyleTags, navLinks: html.resolve( diff --git a/src/content/dependencies/generateTrackReferencingArtworksPage.js b/src/content/dependencies/generateTrackReferencingArtworksPage.js index 097ee929..be13dd79 100644 --- a/src/content/dependencies/generateTrackReferencingArtworksPage.js +++ b/src/content/dependencies/generateTrackReferencingArtworksPage.js @@ -1,37 +1,21 @@ export default { - contentDependencies: [ - 'generateAlbumStyleRules', - 'generateBackToTrackLink', - 'generateReferencingArtworksPage', - 'generateTrackCoverArtwork', - 'generateTrackNavLinks', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, track) => ({ page: - relation('generateReferencingArtworksPage', track.referencedByArtworks), + relation('generateReferencingArtworksPage', track.trackArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), navLinks: relation('generateTrackNavLinks', track), backToTrackLink: relation('generateBackToTrackLink', track), - - cover: - relation('generateTrackCoverArtwork', track), }), data: (track) => ({ name: track.name, - - color: - track.color, }), generate: (data, relations, {html, language}) => @@ -42,10 +26,7 @@ export default { data.name, }), - color: data.color, - styleRules: [relations.albumStyleRules], - - cover: relations.cover, + styleTags: relations.albumStyleTags, navLinks: html.resolve( diff --git a/src/content/dependencies/generateTrackReleaseBox.js b/src/content/dependencies/generateTrackReleaseBox.js index ef02e2b9..c880fe63 100644 --- a/src/content/dependencies/generateTrackReleaseBox.js +++ b/src/content/dependencies/generateTrackReleaseBox.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generatePageSidebarBox', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, track) => ({ box: relation('generatePageSidebarBox'), diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js index 38b8383f..0207e574 100644 --- a/src/content/dependencies/generateTrackReleaseInfo.js +++ b/src/content/dependencies/generateTrackReleaseInfo.js @@ -1,29 +1,19 @@ -import {empty} from '#sugar'; +import {compareArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateReleaseInfoContributionsLine', - 'linkExternal', - ], - - extraDependencies: ['html', 'language'], - relations(relation, track) { const relations = {}; - relations.artistContributionLinks = - relation('generateReleaseInfoContributionsLine', track.artistContribs); + relations.artistContributionsLine = + relation('generateReleaseInfoContributionsLine', + track.artistContribs, + track.artistText); - if (track.hasUniqueCoverArt) { - relations.coverArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', track.coverArtistContribs); - } + relations.listenLine = + relation('generateReleaseInfoListenLine', track); - if (!empty(track.urls)) { - relations.externalLinks = - track.urls.map(url => - relation('linkExternal', url)); - } + relations.albumLink = + relation('linkAlbum', track.album); return relations; }, @@ -35,9 +25,18 @@ export default { data.date = track.date; data.duration = track.duration; + const {album} = track; + + data.showAlbum = + album.showAlbumInTracksWithoutArtists && + track.artistContribs.every(({annotation}) => !annotation) && + compareArrays( + track.artistContribs.map(({artist}) => artist), + album.artistContribs.map(({artist}) => artist), + {checkOrder: true}); + if ( track.hasUniqueCoverArt && - track.coverArtDate && +track.coverArtDate !== +track.date ) { data.coverArtDate = track.coverArtDate; @@ -54,15 +53,21 @@ export default { {[html.joinChildren]: html.tag('br')}, [ - relations.artistContributionLinks.slots({ - stringKey: capsule + '.by', - featuringStringKey: capsule + '.by.featuring', - chronologyKind: 'track', - }), + language.encapsulate(capsule, 'by', capsule => { + const withAlbum = + (data.showAlbum ? '.withAlbum' : ''); - relations.coverArtistContributionsLine?.slots({ - stringKey: capsule + '.coverArtBy', - chronologyKind: 'trackArt', + const albumOptions = + (data.showAlbum ? {album: relations.albumLink} : {}); + + return relations.artistContributionsLine.slots({ + stringKey: capsule + withAlbum, + featuringStringKey: capsule + '.featuring' + withAlbum, + + additionalStringOptions: albumOptions, + + chronologyKind: 'track', + }); }), language.$(capsule, 'released', { @@ -70,11 +75,6 @@ export default { date: language.formatDate(data.date), }), - language.$(capsule, 'artReleased', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.coverArtDate), - }), - language.$(capsule, 'duration', { [language.onlyIfOptions]: ['duration'], duration: language.formatDuration(data.duration), @@ -82,17 +82,9 @@ export default { ]), html.tag('p', - language.encapsulate(capsule, 'listenOn', capsule => - (relations.externalLinks - ? language.$(capsule, { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => link.slot('context', 'track'))), - }) - : language.$(capsule, 'noLinks', { - name: - html.tag('i', data.name), - })))), + relations.listenLine.slots({ + visibleWithoutLinks: true, + context: ['track'], + })), ])), }; diff --git a/src/content/dependencies/generateTrackSocialEmbed.js b/src/content/dependencies/generateTrackSocialEmbed.js index 7cb37af2..94453f7d 100644 --- a/src/content/dependencies/generateTrackSocialEmbed.js +++ b/src/content/dependencies/generateTrackSocialEmbed.js @@ -1,11 +1,4 @@ export default { - contentDependencies: [ - 'generateSocialEmbed', - 'generateTrackSocialEmbedDescription', - ], - - extraDependencies: ['absoluteTo', 'language'], - relations(relation, track) { return { socialEmbed: @@ -26,14 +19,12 @@ 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; @@ -59,10 +50,8 @@ export default { absoluteTo('localized.album', data.albumDirectory), imagePath: - (data.imageSource === 'album' - ? ['media.albumCover', data.albumDirectory, data.coverArtFileExtension] - : data.imageSource === 'track' - ? ['media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension] + (data.hasImage + ? data.imagePath : null), })), }; diff --git a/src/content/dependencies/generateTrackSocialEmbedDescription.js b/src/content/dependencies/generateTrackSocialEmbedDescription.js index 4706aa26..97a4017f 100644 --- a/src/content/dependencies/generateTrackSocialEmbedDescription.js +++ b/src/content/dependencies/generateTrackSocialEmbedDescription.js @@ -1,8 +1,6 @@ import {empty} from '#sugar'; export default { - extraDependencies: ['html', 'language'], - data: (track) => ({ artistNames: track.artistContribs diff --git a/src/content/dependencies/generateUnsafeMunchy.js b/src/content/dependencies/generateUnsafeMunchy.js index c11aadc7..df8231ef 100644 --- a/src/content/dependencies/generateUnsafeMunchy.js +++ b/src/content/dependencies/generateUnsafeMunchy.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { contentSource: {type: 'string'}, }, diff --git a/src/content/dependencies/generateWallpaperStyleTag.js b/src/content/dependencies/generateWallpaperStyleTag.js new file mode 100644 index 00000000..b89f01c2 --- /dev/null +++ b/src/content/dependencies/generateWallpaperStyleTag.js @@ -0,0 +1,77 @@ +import {empty, stitchArrays} from '#sugar'; + +export default { + 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/generateWikiHomepageActionsRow.js b/src/content/dependencies/generateWikiHomepageActionsRow.js index 9f501099..5e3ff381 100644 --- a/src/content/dependencies/generateWikiHomepageActionsRow.js +++ b/src/content/dependencies/generateWikiHomepageActionsRow.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['generateGridActionLinks', 'transformContent'], - relations: (relation, row) => ({ template: relation('generateGridActionLinks'), diff --git a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js index 3068d951..8f4b3400 100644 --- a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js +++ b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js @@ -1,8 +1,4 @@ -import {stitchArrays} from '#sugar'; - export default { - contentDependencies: ['generateCoverCarousel', 'image', 'linkAlbum'], - relations: (relation, row) => ({ coverCarousel: relation('generateCoverCarousel'), @@ -13,27 +9,12 @@ export default { images: row.albums - .map(album => relation('image', album.artTags)), + .map(album => relation('image', album.coverArtworks[0])), }), - data: (row) => ({ - paths: - row.albums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)), - }), - - generate: (data, relations) => + generate: (relations) => relations.coverCarousel.slots({ - links: - relations.links, - - images: - stitchArrays({ - image: relations.images, - path: data.paths, - }).map(({image, path}) => - image.slot('path', path)), + links: relations.links, + images: relations.images, }), }; diff --git a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js index c1d2c79d..eb3417d7 100644 --- a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js +++ b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js @@ -2,9 +2,6 @@ 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 = {}; @@ -21,8 +18,7 @@ export default { sprawl.albums = (row.sourceGroup ? row.sourceGroup.albums - .slice() - .reverse() + .toReversed() .filter(album => album.isListedOnHomepage) .slice(0, row.countAlbumsFromGroup) : []); @@ -45,20 +41,17 @@ export default { images: sprawl.albums - .map(album => relation('image', album.artTags)), + .map(album => + relation('image', + (album.hasCoverArt + ? album.coverArtworks[0] + : null))), }), data: (sprawl, _row) => ({ names: sprawl.albums .map(album => album.name), - - paths: - sprawl.albums - .map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)), }), generate: (data, relations, {language}) => @@ -69,11 +62,9 @@ export default { images: stitchArrays({ image: relations.images, - path: data.paths, name: data.names, - }).map(({image, path, name}) => + }).map(({image, name}) => image.slots({ - path, missingSourceContent: language.$('misc.coverGrid.noCoverArt', { album: name, diff --git a/src/content/dependencies/generateWikiHomepageNewsBox.js b/src/content/dependencies/generateWikiHomepageNewsBox.js index 83a27695..3a06a7c3 100644 --- a/src/content/dependencies/generateWikiHomepageNewsBox.js +++ b/src/content/dependencies/generateWikiHomepageNewsBox.js @@ -1,14 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generatePageSidebarBox', - 'linkNewsEntry', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({newsData}) => ({ entries: newsData.slice(0, 3), diff --git a/src/content/dependencies/generateWikiHomepagePage.js b/src/content/dependencies/generateWikiHomepagePage.js index 8c09a007..9029131b 100644 --- a/src/content/dependencies/generateWikiHomepagePage.js +++ b/src/content/dependencies/generateWikiHomepagePage.js @@ -1,15 +1,4 @@ export default { - contentDependencies: [ - 'generatePageLayout', - 'generatePageSidebar', - 'generatePageSidebarBox', - 'generateWikiHomepageNewsBox', - 'generateWikiHomepageSection', - 'transformContent', - ], - - extraDependencies: ['wikiData'], - sprawl: ({wikiInfo}) => ({ wikiName: wikiInfo.name, diff --git a/src/content/dependencies/generateWikiHomepageSection.js b/src/content/dependencies/generateWikiHomepageSection.js index 49a474da..5fc0c76f 100644 --- a/src/content/dependencies/generateWikiHomepageSection.js +++ b/src/content/dependencies/generateWikiHomepageSection.js @@ -1,13 +1,4 @@ export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateWikiHomepageActionsRow', - 'generateWikiHomepageAlbumCarouselRow', - 'generateWikiHomepageAlbumGridRow', - ], - - extraDependencies: ['html'], - relations: (relation, homepageSection) => ({ colorStyle: relation('generateColorStyleAttribute', homepageSection.color), diff --git a/src/content/dependencies/generateWikiWallpaperStyleTag.js b/src/content/dependencies/generateWikiWallpaperStyleTag.js new file mode 100644 index 00000000..be52bcc1 --- /dev/null +++ b/src/content/dependencies/generateWikiWallpaperStyleTag.js @@ -0,0 +1,35 @@ +export default { + 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 bc268ec1..aacf2fed 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -2,82 +2,85 @@ import {logWarn} from '#cli'; import {empty} from '#sugar'; export default { - extraDependencies: [ - 'checkIfImagePathHasCachedThumbnails', - 'getDimensionsOfImagePath', - 'getSizeOfMediaFile', - 'getThumbnailEqualOrSmaller', - 'getThumbnailsAvailableForDimensions', - 'html', - 'language', - 'missingImagePaths', - 'to', - ], - - contentDependencies: ['generateColorStyleAttribute'], - - relations: (relation) => ({ + relations: (relation, _artwork) => ({ colorStyle: relation('generateColorStyleAttribute'), }), - data(artTags) { - const data = {}; - - if (artTags) { - data.contentWarnings = - artTags - .filter(artTag => artTag.isContentWarning) - .map(artTag => artTag.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'}, + responsiveThumb: {type: 'boolean', default: false}, + responsiveSizes: {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}, - - dimensions: { - validate: v => v.isDimensions, + // Added to the <img>. + imgAttributes: { + type: 'attributes', + mutable: false, }, + // Added to the <img> itself. alt: {type: 'string'}, - 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, { @@ -91,27 +94,25 @@ 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 // src string directly when a parts-formed path *is* available seems wrong. // It should be possible to do urls.from(slots.path[0]).to(...slots.path), // for example, but will require reworking the control flow here a little. - let mediaSrc = null; + let mediaSrc = decodeURIComponent(originalSrc); if (originalSrc.startsWith(to('media.root'))) { - mediaSrc = - originalSrc - .slice(to('media.root').length) - .replace(/^\//, ''); + mediaSrc = mediaSrc + .slice(to('media.root').length) + .replace(/^\//, ''); } const isMissingImageFile = @@ -121,29 +122,29 @@ export default { !isMissingImageFile && (typeof slots.link === 'string' || slots.link); - const contentWarnings = - slots.warnings ?? - data.contentWarnings; + const warnings = slots.warnings ?? data.warnings; + const dimensions = slots.dimensions ?? data.dimensions; const willReveal = slots.reveal && originalSrc && !isMissingImageFile && - !empty(contentWarnings); - - const willSquare = - slots.square; + !empty(warnings); const imgAttributes = html.attributes([ {class: 'image'}, + slots.imgAttributes, + slots.alt && {alt: slots.alt}, - slots.dimensions?.[0] && - {width: slots.dimensions[0]}, + dimensions && + dimensions[0] && + {width: dimensions[0]}, - slots.dimensions?.[1] && - {height: slots.dimensions[1]}, + dimensions && + dimensions[1] && + {height: dimensions[1]}, ]); const isPlaceholder = @@ -169,7 +170,7 @@ export default { html.tag('span', {class: 'reveal-warnings'}, language.$('misc.contentWarnings.warnings', { - warnings: language.formatUnitList(contentWarnings), + warnings: language.formatUnitList(warnings), })), html.tag('br'), @@ -199,31 +200,29 @@ export default { // so it won't be set if thumbnails aren't available. let revealSrc = null; + let originalDimensions; + let availableThumbs; + let selectedThumbtack; + + const getThumbSrc = (thumbtack) => + to('thumb.path', mediaSrc.replace(/\.(png|jpg)$/, `.${thumbtack}.jpg`)); + // If thumbnails are available *and* being used, calculate thumbSrc, // and provide some attributes relevant to the large image overlay. if (hasThumbnails && slots.thumb) { - const selectedSize = + selectedThumbtack = getThumbnailEqualOrSmaller(slots.thumb, mediaSrc); - const mediaSrcJpeg = - mediaSrc.replace(/\.(png|jpg)$/, `.${selectedSize}.jpg`); - displaySrc = - to('thumb.path', mediaSrcJpeg); + getThumbSrc(selectedThumbtack); if (willReveal) { - const miniSize = - getThumbnailEqualOrSmaller('mini', mediaSrc); - - const mediaSrcJpeg = - mediaSrc.replace(/\.(png|jpg)$/, `.${miniSize}.jpg`); - revealSrc = - to('thumb.path', mediaSrcJpeg); + getThumbSrc(getThumbnailEqualOrSmaller('mini', mediaSrc)); } - const originalDimensions = getDimensionsOfImagePath(mediaSrc); - const availableThumbs = getThumbnailsAvailableForDimensions(originalDimensions); + originalDimensions = getDimensionsOfImagePath(mediaSrc); + availableThumbs = getThumbnailsAvailableForDimensions(originalDimensions); const fileSize = (willLink && mediaSrc @@ -239,11 +238,54 @@ export default { !empty(availableThumbs) && {'data-thumbs': availableThumbs - .map(([name, size]) => `${name}:${size}`) + .map(([tack, size]) => `${tack}:${size}`) .join(' ')}, ]); } + let displayStaticImg = + html.tag('img', + imgAttributes, + {src: displaySrc}); + + if (hasThumbnails && slots.responsiveThumb) responsive: { + if (slots.lazy) { + logWarn`${'responsiveThumb'} and ${'lazy'} are used together, but not compatible`; + break responsive; + } + + if (!slots.thumb) { + logWarn`${'responsiveThumb'} must be used alongside a default ${'thumb'}`; + break responsive; + } + + const srcset = [ + // Never load the original source, which might be a very large + // uncompressed file. Bah! + /* [originalSrc, `${Math.min(...originalDimensions)}w`], */ + + ...availableThumbs.map(([tack, size]) => + [getThumbSrc(tack), `${Math.floor(0.95 * size)}w`]), + + // fallback + [displaySrc], + ].map(line => line.join(' ')).join(',\n'); + + displayStaticImg = + html.tag('img', + imgAttributes, + + {sizes: + (slots.responsiveSizes.match(/(?=(?:,|^))\s*\S/) + // slot provided fallback size + ? slots.responsiveSizes + // default fallback size + : slots.responsiveSizes + ',\n' + + new Map(availableThumbs).get(selectedThumbtack) + 'px')}, + + {srcset}); + } + if (!displaySrc) { return ( prepare( @@ -252,10 +294,7 @@ export default { } const images = { - displayStatic: - html.tag('img', - imgAttributes, - {src: displaySrc}), + displayStatic: displayStaticImg, displayLazy: slots.lazy && @@ -323,14 +362,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..cfa6346c 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); @@ -87,6 +94,8 @@ export function watchContentDependencies({ const filePaths = files.map(file => path.join(watchPath, file)); for (const filePath of filePaths) { if (filePath === metaPath) continue; + if (filePath.endsWith('.DS_Store')) continue; + const functionName = getFunctionName(filePath); if (!isMocked(functionName)) { contentDependencies[functionName] = null; @@ -98,8 +107,9 @@ export function watchContentDependencies({ watcher.on('all', (event, filePath) => { if (!['add', 'change'].includes(event)) return; if (filePath === metaPath) return; - handlePathUpdated(filePath); + if (filePath.endsWith('.DS_Store')) return; + handlePathUpdated(filePath); }); watcher.on('unlink', (filePath) => { @@ -108,6 +118,8 @@ export function watchContentDependencies({ return; } + if (filePath.endsWith('.DS_Store')) return; + handlePathRemoved(filePath); }); diff --git a/src/content/dependencies/linkAdditionalFile.js b/src/content/dependencies/linkAdditionalFile.js new file mode 100644 index 00000000..1b5e650f --- /dev/null +++ b/src/content/dependencies/linkAdditionalFile.js @@ -0,0 +1,27 @@ +export default { + 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/linkAlbum.js b/src/content/dependencies/linkAlbum.js index 36b0d13a..085d5f62 100644 --- a/src/content/dependencies/linkAlbum.js +++ b/src/content/dependencies/linkAlbum.js @@ -1,8 +1,18 @@ export default { - contentDependencies: ['linkThing'], + relations: (relation, album) => ({ + link: + (album.style === 'single' + ? relation('linkTrack', album.tracks[0]) + : relation('linkThing', 'localized.album', album)), + }), - relations: (relation, album) => - ({link: relation('linkThing', 'localized.album', album)}), + data: (album) => ({ + style: album.style, + name: album.name, + }), - generate: (relations) => relations.link, + generate: (data, relations, {language}) => + (data.style === 'single' + ? relations.link.slot('content', language.sanitize(data.name)) + : relations.link), }; 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/linkAlbumCommentary.js b/src/content/dependencies/linkAlbumCommentary.js index ab519fd6..f1917345 100644 --- a/src/content/dependencies/linkAlbumCommentary.js +++ b/src/content/dependencies/linkAlbumCommentary.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, album) => ({link: relation('linkThing', 'localized.albumCommentary', album)}), diff --git a/src/content/dependencies/linkAlbumDynamically.js b/src/content/dependencies/linkAlbumDynamically.js index 45f8c2a9..ba572c8d 100644 --- a/src/content/dependencies/linkAlbumDynamically.js +++ b/src/content/dependencies/linkAlbumDynamically.js @@ -1,14 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'linkAlbumCommentary', - 'linkAlbumGallery', - 'linkAlbum', - ], - - extraDependencies: ['html', 'pagePath'], - relations: (relation, album) => ({ galleryLink: relation('linkAlbumGallery', album), diff --git a/src/content/dependencies/linkAlbumGallery.js b/src/content/dependencies/linkAlbumGallery.js index e3f30a29..efba66d1 100644 --- a/src/content/dependencies/linkAlbumGallery.js +++ b/src/content/dependencies/linkAlbumGallery.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, album) => ({link: relation('linkThing', 'localized.albumGallery', album)}), diff --git a/src/content/dependencies/linkAlbumReferencedArtworks.js b/src/content/dependencies/linkAlbumReferencedArtworks.js index ba51b5e3..411bd2ab 100644 --- a/src/content/dependencies/linkAlbumReferencedArtworks.js +++ b/src/content/dependencies/linkAlbumReferencedArtworks.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, album) => ({link: relation('linkThing', 'localized.albumReferencedArtworks', album)}), diff --git a/src/content/dependencies/linkAlbumReferencingArtworks.js b/src/content/dependencies/linkAlbumReferencingArtworks.js index 4d5e799d..3aee9a4b 100644 --- a/src/content/dependencies/linkAlbumReferencingArtworks.js +++ b/src/content/dependencies/linkAlbumReferencingArtworks.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, album) => ({link: relation('linkThing', 'localized.albumReferencingArtworks', album)}), diff --git a/src/content/dependencies/linkAnythingMan.js b/src/content/dependencies/linkAnythingMan.js index d4697403..cb22baee 100644 --- a/src/content/dependencies/linkAnythingMan.js +++ b/src/content/dependencies/linkAnythingMan.js @@ -1,21 +1,13 @@ export default { - contentDependencies: [ - 'linkAlbum', - 'linkFlash', - 'linkTrack', - ], - - query: (thing) => ({ - referenceType: thing.constructor[Symbol.for('Thing.referenceType')], - }), - - relations: (relation, query, thing) => ({ + relations: (relation, thing) => ({ link: - (query.referenceType === 'album' + (thing.isAlbum ? relation('linkAlbum', thing) - : query.referenceType === 'flash' + : thing.isArtwork + ? relation('linkArtwork', thing) + : thing.isFlash ? relation('linkFlash', thing) - : query.referenceType === 'track' + : thing.isTrack ? relation('linkTrack', thing) : null), }), diff --git a/src/content/dependencies/linkArtTagDynamically.js b/src/content/dependencies/linkArtTagDynamically.js index 964258e1..4514b7c1 100644 --- a/src/content/dependencies/linkArtTagDynamically.js +++ b/src/content/dependencies/linkArtTagDynamically.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkArtTagGallery', 'linkArtTagInfo'], - extraDependencies: ['pagePath'], - relations: (relation, artTag) => ({ galleryLink: relation('linkArtTagGallery', artTag), infoLink: relation('linkArtTagInfo', artTag), diff --git a/src/content/dependencies/linkArtTagGallery.js b/src/content/dependencies/linkArtTagGallery.js index a92b69c1..92ab1ed3 100644 --- a/src/content/dependencies/linkArtTagGallery.js +++ b/src/content/dependencies/linkArtTagGallery.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, artTag) => ({link: relation('linkThing', 'localized.artTagGallery', artTag)}), diff --git a/src/content/dependencies/linkArtTagInfo.js b/src/content/dependencies/linkArtTagInfo.js index 409cb3c0..5eb2ac56 100644 --- a/src/content/dependencies/linkArtTagInfo.js +++ b/src/content/dependencies/linkArtTagInfo.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, artTag) => ({link: relation('linkThing', 'localized.artTagInfo', artTag)}), diff --git a/src/content/dependencies/linkArtist.js b/src/content/dependencies/linkArtist.js index 718ee6fa..917ae6b6 100644 --- a/src/content/dependencies/linkArtist.js +++ b/src/content/dependencies/linkArtist.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, artist) => ({link: relation('linkThing', 'localized.artist', artist)}), diff --git a/src/content/dependencies/linkArtistGallery.js b/src/content/dependencies/linkArtistGallery.js index 66dc172d..001eec1f 100644 --- a/src/content/dependencies/linkArtistGallery.js +++ b/src/content/dependencies/linkArtistGallery.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, artist) => ({link: relation('linkThing', 'localized.artistGallery', artist)}), diff --git a/src/content/dependencies/linkArtistRollingWindow.js b/src/content/dependencies/linkArtistRollingWindow.js new file mode 100644 index 00000000..6ab516ac --- /dev/null +++ b/src/content/dependencies/linkArtistRollingWindow.js @@ -0,0 +1,6 @@ +export default { + relations: (relation, artist) => + ({link: relation('linkThing', 'localized.artistRollingWindow', artist)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkArtwork.js b/src/content/dependencies/linkArtwork.js new file mode 100644 index 00000000..fce89229 --- /dev/null +++ b/src/content/dependencies/linkArtwork.js @@ -0,0 +1,13 @@ +export default { + relations: (relation, artwork) => ({ + link: + (artwork.thing.isAlbum + ? relation('linkAlbum', artwork.thing) + : artwork.thing.isTrack + ? relation('linkTrack', artwork.thing) + : null), + }), + + generate: (relations) => + relations.link, +}; diff --git a/src/content/dependencies/linkCommentaryIndex.js b/src/content/dependencies/linkCommentaryIndex.js index 5568ff84..e59b3641 100644 --- a/src/content/dependencies/linkCommentaryIndex.js +++ b/src/content/dependencies/linkCommentaryIndex.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkStationaryIndex'], - relations: (relation) => ({link: relation( diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js index c658d461..aa9bdef9 100644 --- a/src/content/dependencies/linkContribution.js +++ b/src/content/dependencies/linkContribution.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateContributionTooltip', - 'generateTextWithTooltip', - 'linkArtist', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, contribution) => ({ artistLink: relation('linkArtist', contribution.artist), @@ -24,13 +16,15 @@ export default { }), slots: { + content: {type: 'html', mutable: false}, + showAnnotation: {type: 'boolean', default: false}, showExternalLinks: {type: 'boolean', default: false}, showChronology: {type: 'boolean', default: false}, trimAnnotation: {type: 'boolean', default: false}, - preventWrapping: {type: 'boolean', default: true}, + preventWrapping: {type: 'boolean', default: false}, preventTooltip: {type: 'boolean', default: false}, chronologyKind: {type: 'string'}, @@ -46,6 +40,10 @@ export default { language.encapsulate('misc.artistLink', workingCapsule => { const workingOptions = {}; + if (!html.isBlank(slots.content)) { + relations.artistLink.setSlot('content', slots.content); + } + // Filling slots early is necessary to actually give the tooltip // content. Otherwise, the coming-up html.isBlank() always reports // the tooltip as blank! diff --git a/src/content/dependencies/linkExternal.js b/src/content/dependencies/linkExternal.js index 073c821e..ad8d4f23 100644 --- a/src/content/dependencies/linkExternal.js +++ b/src/content/dependencies/linkExternal.js @@ -1,9 +1,20 @@ import {isExternalLinkContext, isExternalLinkStyle} from '#external-links'; export default { - extraDependencies: ['html', 'language', 'wikiData'], + sprawl: ({wikiInfo}) => ({ + canonicalBase: + wikiInfo.canonicalBase, - data: (url) => ({url}), + canonicalMediaBase: + wikiInfo.canonicalMediaBase, + }), + + data: (sprawl, url) => ({ + url, + + canonicalBase: + sprawl.canonicalBase, + }), slots: { content: { @@ -39,25 +50,44 @@ export default { default: false, }, + disableBrowserTooltip: { + type: 'boolean', + default: false, + }, + tab: { validate: v => v.is('default', 'separate'), default: 'default', }, }, - generate(data, slots, {html, language}) { + generate(data, slots, {html, language, to}) { + const {url} = data; + let urlIsValid; try { - new URL(data.url); + new URL(url); urlIsValid = true; - } catch (error) { + } catch { urlIsValid = false; } + let href; + if (urlIsValid) { + const {canonicalBase, canonicalMediaBase} = data; + if (canonicalMediaBase && url.startsWith(canonicalMediaBase)) { + href = to('media.path', url.slice(canonicalMediaBase.length)); + } else if (canonicalBase && url.startsWith(canonicalBase)) { + href = to('shared.path', url.slice(canonicalBase.length)); + } else { + href = url; + } + } + let formattedLink; if (urlIsValid) { formattedLink = - language.formatExternalLink(data.url, { + language.formatExternalLink(url, { style: slots.style, context: slots.context, }); @@ -65,7 +95,7 @@ export default { // Fall back to platform if nothing matched the desired style. if (html.isBlank(formattedLink) && slots.style !== 'platform') { formattedLink = - language.formatExternalLink(data.url, { + language.formatExternalLink(url, { style: 'platform', context: slots.context, }); @@ -80,7 +110,7 @@ export default { let linkContent; if (urlIsValid) { - linkAttributes.set('href', data.url); + linkAttributes.set('href', href); if (html.isBlank(slots.content)) { linkContent = formattedLink; @@ -111,7 +141,9 @@ export default { linkAttributes.add('class', 'indicate-external'); let titleText; - if (slots.tab === 'separate') { + if (slots.disableBrowserTooltip) { + titleText = null; + } else if (slots.tab === 'separate') { if (html.isBlank(slots.content)) { titleText = language.$('misc.external.opensInNewTab.annotation'); diff --git a/src/content/dependencies/linkFlash.js b/src/content/dependencies/linkFlash.js index 93dd5a28..cfc01079 100644 --- a/src/content/dependencies/linkFlash.js +++ b/src/content/dependencies/linkFlash.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, flash) => ({link: relation('linkThing', 'localized.flash', flash)}), diff --git a/src/content/dependencies/linkFlashAct.js b/src/content/dependencies/linkFlashAct.js index 82c23325..069bedf4 100644 --- a/src/content/dependencies/linkFlashAct.js +++ b/src/content/dependencies/linkFlashAct.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['generateUnsafeMunchy', 'linkThing'], - relations: (relation, flashAct) => ({ unsafeMunchy: relation('generateUnsafeMunchy'), diff --git a/src/content/dependencies/linkFlashIndex.js b/src/content/dependencies/linkFlashIndex.js index 6dd0710e..9c1b076e 100644 --- a/src/content/dependencies/linkFlashIndex.js +++ b/src/content/dependencies/linkFlashIndex.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkStationaryIndex'], - relations: (relation) => ({link: relation( diff --git a/src/content/dependencies/linkFlashSide.js b/src/content/dependencies/linkFlashSide.js index b77ca65a..6407ef25 100644 --- a/src/content/dependencies/linkFlashSide.js +++ b/src/content/dependencies/linkFlashSide.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkFlashAct'], - relations: (relation, flashSide) => ({ link: relation('linkFlashAct', flashSide.acts[0]), diff --git a/src/content/dependencies/linkGroup.js b/src/content/dependencies/linkGroup.js index ebab1b5b..10bec2fb 100644 --- a/src/content/dependencies/linkGroup.js +++ b/src/content/dependencies/linkGroup.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, group) => ({link: relation('linkThing', 'localized.groupInfo', group)}), diff --git a/src/content/dependencies/linkGroupDynamically.js b/src/content/dependencies/linkGroupDynamically.js index 90303ed1..0b5bd85c 100644 --- a/src/content/dependencies/linkGroupDynamically.js +++ b/src/content/dependencies/linkGroupDynamically.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkGroupGallery', 'linkGroup'], - extraDependencies: ['pagePath'], - relations: (relation, group) => ({ galleryLink: relation('linkGroupGallery', group), infoLink: relation('linkGroup', group), diff --git a/src/content/dependencies/linkGroupExtra.js b/src/content/dependencies/linkGroupExtra.js index bc3c0580..1a6161c1 100644 --- a/src/content/dependencies/linkGroupExtra.js +++ b/src/content/dependencies/linkGroupExtra.js @@ -1,13 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'linkGroup', - 'linkGroupGallery', - ], - - extraDependencies: ['html'], - relations(relation, group) { const relations = {}; diff --git a/src/content/dependencies/linkGroupGallery.js b/src/content/dependencies/linkGroupGallery.js index 86c4a0f3..957756d8 100644 --- a/src/content/dependencies/linkGroupGallery.js +++ b/src/content/dependencies/linkGroupGallery.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, group) => ({link: relation('linkThing', 'localized.groupGallery', group)}), diff --git a/src/content/dependencies/linkListing.js b/src/content/dependencies/linkListing.js index ac66919a..4eb2dce6 100644 --- a/src/content/dependencies/linkListing.js +++ b/src/content/dependencies/linkListing.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkThing'], - extraDependencies: ['language'], - relations: (relation, listing) => ({link: relation('linkThing', 'localized.listing', listing)}), diff --git a/src/content/dependencies/linkListingIndex.js b/src/content/dependencies/linkListingIndex.js index 1bfaf46e..209066a9 100644 --- a/src/content/dependencies/linkListingIndex.js +++ b/src/content/dependencies/linkListingIndex.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkStationaryIndex'], - relations: (relation) => ({link: relation( diff --git a/src/content/dependencies/linkNewsEntry.js b/src/content/dependencies/linkNewsEntry.js index 1fb32dd9..9ef7ac0e 100644 --- a/src/content/dependencies/linkNewsEntry.js +++ b/src/content/dependencies/linkNewsEntry.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, newsEntry) => ({link: relation('linkThing', 'localized.newsEntry', newsEntry)}), diff --git a/src/content/dependencies/linkNewsIndex.js b/src/content/dependencies/linkNewsIndex.js index e911a384..4414afc6 100644 --- a/src/content/dependencies/linkNewsIndex.js +++ b/src/content/dependencies/linkNewsIndex.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkStationaryIndex'], - relations: (relation) => ({link: relation( diff --git a/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js b/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js index ec856631..5a16256e 100644 --- a/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js +++ b/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js @@ -3,9 +3,6 @@ import {sortAlbumsTracksChronologically, sortContributionsChronologically} import {chunkArtistTrackContributions} from '#wiki-data'; export default { - contentDependencies: ['generateColorStyleAttribute'], - extraDependencies: ['html', 'language'], - query(track, artist) { const relevantInfoPageChunkingContributions = track.allReleases diff --git a/src/content/dependencies/linkPathFromMedia.js b/src/content/dependencies/linkPathFromMedia.js index d71c69f8..344b7d2c 100644 --- a/src/content/dependencies/linkPathFromMedia.js +++ b/src/content/dependencies/linkPathFromMedia.js @@ -1,17 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: ['linkTemplate'], - - extraDependencies: [ - 'checkIfImagePathHasCachedThumbnails', - 'getDimensionsOfImagePath', - 'getSizeOfMediaFile', - 'getThumbnailsAvailableForDimensions', - 'html', - 'to', - ], - relations: (relation) => ({link: relation('linkTemplate')}), diff --git a/src/content/dependencies/linkPathFromRoot.js b/src/content/dependencies/linkPathFromRoot.js index dab3ac1f..b4a90c07 100644 --- a/src/content/dependencies/linkPathFromRoot.js +++ b/src/content/dependencies/linkPathFromRoot.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkTemplate'], - relations: (relation) => ({link: relation('linkTemplate')}), diff --git a/src/content/dependencies/linkPathFromSite.js b/src/content/dependencies/linkPathFromSite.js index 64676465..67a43059 100644 --- a/src/content/dependencies/linkPathFromSite.js +++ b/src/content/dependencies/linkPathFromSite.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkTemplate'], - relations: (relation) => ({link: relation('linkTemplate')}), diff --git a/src/content/dependencies/linkReferencedArtworks.js b/src/content/dependencies/linkReferencedArtworks.js new file mode 100644 index 00000000..f8b3f3c8 --- /dev/null +++ b/src/content/dependencies/linkReferencedArtworks.js @@ -0,0 +1,12 @@ +export default { + relations: (relation, artwork) => ({ + link: + (artwork.thing.isAlbum + ? relation('linkAlbumReferencedArtworks', artwork.thing) + : artwork.thing.isTrack + ? 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..6b7e4f9a --- /dev/null +++ b/src/content/dependencies/linkReferencingArtworks.js @@ -0,0 +1,12 @@ +export default { + relations: (relation, artwork) => ({ + link: + (artwork.thing.isAlbum + ? relation('linkAlbumReferencingArtworks', artwork.thing) + : artwork.thing.isTrack + ? relation('linkTrackReferencingArtworks', artwork.thing) + : null), + }), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkStaticPage.js b/src/content/dependencies/linkStaticPage.js index 032af6c9..c3ac69fa 100644 --- a/src/content/dependencies/linkStaticPage.js +++ b/src/content/dependencies/linkStaticPage.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, staticPage) => ({link: relation('linkThing', 'localized.staticPage', staticPage)}), diff --git a/src/content/dependencies/linkStationaryIndex.js b/src/content/dependencies/linkStationaryIndex.js index d5506e60..10f8ba44 100644 --- a/src/content/dependencies/linkStationaryIndex.js +++ b/src/content/dependencies/linkStationaryIndex.js @@ -1,9 +1,6 @@ // Not to be confused with "html.Stationery". export default { - contentDependencies: ['linkTemplate'], - extraDependencies: ['language'], - relations(relation) { return { linkTemplate: relation('linkTemplate'), diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index 4f853dc4..10466b43 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -3,13 +3,6 @@ import {empty} from '#sugar'; import striptags from 'striptags'; export default { - extraDependencies: [ - 'appendIndexHTML', - 'html', - 'language', - 'to', - ], - slots: { href: {type: 'string'}, path: {validate: v => v.validateArrayItems(v.isString)}, diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js index 3902f380..166a857d 100644 --- a/src/content/dependencies/linkThing.js +++ b/src/content/dependencies/linkThing.js @@ -1,13 +1,4 @@ export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateTextWithTooltip', - 'generateTooltip', - 'linkTemplate', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, _pathKey, thing) => ({ linkTemplate: relation('linkTemplate'), @@ -20,11 +11,15 @@ export default { tooltip: relation('generateTooltip'), + + name: + relation('generateName', thing), }), data: (pathKey, thing) => ({ name: thing.name, nameShort: thing.nameShort ?? thing.shortName, + nameText: thing.nameText, path: (pathKey @@ -75,22 +70,21 @@ export default { hash: {type: 'string'}, }, - generate(data, relations, slots, {html, language}) { + generate(data, relations, slots, {html}) { const path = slots.path ?? data.path; const linkAttributes = slots.attributes; const wrapperAttributes = html.attributes(); - const showShortName = - (slots.preferShortName - ? data.nameShort && data.nameShort !== data.name - : false); - const name = - (showShortName - ? data.nameShort - : data.name); + relations.name.slot('preferShortName', slots.preferShortName); + + const showShortName = + slots.preferShortName && + !data.nameText && + data.nameShort && + data.nameShort !== data.name; const showWikiTooltip = (slots.tooltipStyle === 'auto' @@ -114,7 +108,7 @@ export default { const content = (html.isBlank(slots.content) - ? language.sanitize(name) + ? name : slots.content); if (slots.color !== false) { diff --git a/src/content/dependencies/linkTrack.js b/src/content/dependencies/linkTrack.js index d5d96726..8ee715f0 100644 --- a/src/content/dependencies/linkTrack.js +++ b/src/content/dependencies/linkTrack.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, track) => ({link: relation('linkThing', 'localized.track', track)}), diff --git a/src/content/dependencies/linkTrackAsRelease.js b/src/content/dependencies/linkTrackAsRelease.js new file mode 100644 index 00000000..7a114ad9 --- /dev/null +++ b/src/content/dependencies/linkTrackAsRelease.js @@ -0,0 +1,20 @@ +export default { + relations: (relation, track) => ({ + trackLink: + relation('linkTrack', track), + }), + + data: (track) => ({ + albumName: + track.album.name, + + albumColor: + track.album.color, + }), + + generate: (data, relations, {language}) => + relations.trackLink.slots({ + content: language.sanitize(data.albumName), + color: data.albumColor, + }), +}; diff --git a/src/content/dependencies/linkTrackDynamically.js b/src/content/dependencies/linkTrackDynamically.js index bbcf1c34..088bbe09 100644 --- a/src/content/dependencies/linkTrackDynamically.js +++ b/src/content/dependencies/linkTrackDynamically.js @@ -1,9 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: ['linkTrack'], - extraDependencies: ['pagePath'], - relations: (relation, track) => ({ infoLink: relation('linkTrack', track), }), diff --git a/src/content/dependencies/linkTrackReferencedArtworks.js b/src/content/dependencies/linkTrackReferencedArtworks.js index b4cb08fe..6da6504e 100644 --- a/src/content/dependencies/linkTrackReferencedArtworks.js +++ b/src/content/dependencies/linkTrackReferencedArtworks.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, track) => ({link: relation('linkThing', 'localized.trackReferencedArtworks', track)}), diff --git a/src/content/dependencies/linkTrackReferencingArtworks.js b/src/content/dependencies/linkTrackReferencingArtworks.js index c9c9f4d1..4d113ba7 100644 --- a/src/content/dependencies/linkTrackReferencingArtworks.js +++ b/src/content/dependencies/linkTrackReferencingArtworks.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, track) => ({link: relation('linkThing', 'localized.trackReferencingArtworks', track)}), diff --git a/src/content/dependencies/linkWikiHomepage.js b/src/content/dependencies/linkWikiHomepage.js index d8d3d0a0..91fbe410 100644 --- a/src/content/dependencies/linkWikiHomepage.js +++ b/src/content/dependencies/linkWikiHomepage.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkTemplate'], - extraDependencies: ['wikiData'], - sprawl({wikiInfo}) { return {wikiShortName: wikiInfo.nameShort}; }, diff --git a/src/content/dependencies/listAlbumsByDate.js b/src/content/dependencies/listAlbumsByDate.js index c83ffc97..eaf9eecf 100644 --- a/src/content/dependencies/listAlbumsByDate.js +++ b/src/content/dependencies/listAlbumsByDate.js @@ -2,9 +2,6 @@ import {sortChronologically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listAlbumsByDateAdded.js b/src/content/dependencies/listAlbumsByDateAdded.js index d462ad46..940da67d 100644 --- a/src/content/dependencies/listAlbumsByDateAdded.js +++ b/src/content/dependencies/listAlbumsByDateAdded.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {chunkByProperties} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listAlbumsByDuration.js b/src/content/dependencies/listAlbumsByDuration.js index c60685ab..8de2bb84 100644 --- a/src/content/dependencies/listAlbumsByDuration.js +++ b/src/content/dependencies/listAlbumsByDuration.js @@ -3,16 +3,17 @@ import {filterByCount, stitchArrays} from '#sugar'; import {getTotalDuration} from '#wiki-data'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, query({albumData}, spec) { - const albums = sortAlphabetically(albumData.slice()); - const durations = albums.map(album => getTotalDuration(album.tracks)); + const albums = + sortAlphabetically( + albumData.filter(album => !album.hideDuration)); + + const durations = + albums.map(album => getTotalDuration(album.tracks)); filterByCount(albums, durations); sortByCount(albums, durations, {greatestFirst: true}); diff --git a/src/content/dependencies/listAlbumsByName.js b/src/content/dependencies/listAlbumsByName.js index 21419537..a7939292 100644 --- a/src/content/dependencies/listAlbumsByName.js +++ b/src/content/dependencies/listAlbumsByName.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listAlbumsByTracks.js b/src/content/dependencies/listAlbumsByTracks.js index 798e6c2e..b1f62a82 100644 --- a/src/content/dependencies/listAlbumsByTracks.js +++ b/src/content/dependencies/listAlbumsByTracks.js @@ -2,21 +2,25 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, query({albumData}, spec) { - const albums = sortAlphabetically(albumData.slice()); - const counts = albums.map(album => album.tracks.length); + const albums = + sortAlphabetically( + albumData.filter(album => !album.hideDuration)); + + const counts = + albums.map(album => album.tracks.length); filterByCount(albums, counts); sortByCount(albums, counts, {greatestFirst: true}); - return {spec, albums, counts}; + const styles = + albums.map(album => album.style); + + return {spec, albums, counts, styles}; }, relations(relation, query) { @@ -32,6 +36,7 @@ export default { data(query) { return { counts: query.counts, + styles: query.styles, }; }, @@ -42,10 +47,19 @@ export default { stitchArrays({ link: relations.albumLinks, count: data.counts, - }).map(({link, count}) => ({ - album: link, - tracks: language.countTracks(count, {unit: true}), - })), + style: data.styles, + }).map(({link, count, style}) => { + const row = { + album: link, + tracks: language.countTracks(count, {unit: true}), + }; + + if (style === 'single') { + row.stringsKey = 'single'; + } + + return row; + }), }); }, }; diff --git a/src/content/dependencies/listAllAdditionalFiles.js b/src/content/dependencies/listAllAdditionalFiles.js index a6e34b9a..2d338916 100644 --- a/src/content/dependencies/listAllAdditionalFiles.js +++ b/src/content/dependencies/listAllAdditionalFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listAllAdditionalFilesTemplate'], - relations: (relation, spec) => ({page: relation('listAllAdditionalFilesTemplate', spec, 'additionalFiles')}), diff --git a/src/content/dependencies/listAllAdditionalFilesTemplate.js b/src/content/dependencies/listAllAdditionalFilesTemplate.js index e33ad7b5..f298233c 100644 --- a/src/content/dependencies/listAllAdditionalFilesTemplate.js +++ b/src/content/dependencies/listAllAdditionalFilesTemplate.js @@ -1,209 +1,37 @@ import {sortChronologically} from '#sort'; -import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateListingPage', - 'generateListAllAdditionalFilesChunk', - 'linkAlbum', - 'linkTrack', - 'linkAlbumAdditionalFile', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({albumData}) => ({albumData}), - query(sprawl, spec, property) { - const albums = - sortChronologically(sprawl.albumData.slice()); - - 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))); + query: (sprawl, spec, property) => ({ + spec, + property, - 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/listAllMidiProjectFiles.js b/src/content/dependencies/listAllMidiProjectFiles.js index 31a70ef0..109cf2e7 100644 --- a/src/content/dependencies/listAllMidiProjectFiles.js +++ b/src/content/dependencies/listAllMidiProjectFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listAllAdditionalFilesTemplate'], - relations: (relation, spec) => ({page: relation('listAllAdditionalFilesTemplate', spec, 'midiProjectFiles')}), diff --git a/src/content/dependencies/listAllSheetMusicFiles.js b/src/content/dependencies/listAllSheetMusicFiles.js index 166b2068..4f3bdb96 100644 --- a/src/content/dependencies/listAllSheetMusicFiles.js +++ b/src/content/dependencies/listAllSheetMusicFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listAllAdditionalFilesTemplate'], - relations: (relation, spec) => ({page: relation('listAllAdditionalFilesTemplate', spec, 'sheetMusicFiles')}), diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js index 5386dcdc..98f81019 100644 --- a/src/content/dependencies/listArtTagNetwork.js +++ b/src/content/dependencies/listArtTagNetwork.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtTagInfo'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({artTagData}) { return {artTagData}; }, @@ -29,21 +26,21 @@ export default { const getStats = (artTag) => ({ directUses: - artTag.directlyTaggedInThings.length, + artTag.directlyFeaturedInArtworks.length, // Not currently displayed directAndIndirectUses: unique([ - ...artTag.indirectlyTaggedInThings, - ...artTag.directlyTaggedInThings, + ...artTag.indirectlyFeaturedInArtworks, + ...artTag.directlyFeaturedInArtworks, ]).length, totalUses: [ - ...artTag.directlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, ... artTag.allDescendantArtTags - .flatMap(artTag => artTag.directlyTaggedInThings), + .flatMap(artTag => artTag.directlyFeaturedInArtworks), ].length, descendants: diff --git a/src/content/dependencies/listArtTagsByName.js b/src/content/dependencies/listArtTagsByName.js index 31856478..10e9e873 100644 --- a/src/content/dependencies/listArtTagsByName.js +++ b/src/content/dependencies/listArtTagsByName.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtTagGallery'], - extraDependencies: ['language', 'wikiData'], - sprawl({artTagData}) { return {artTagData}; }, @@ -35,8 +32,8 @@ export default { counts: query.artTags.map(artTag => unique([ - ...artTag.indirectlyTaggedInThings, - ...artTag.directlyTaggedInThings, + ...artTag.indirectlyFeaturedInArtworks, + ...artTag.directlyFeaturedInArtworks, ]).length), }; }, diff --git a/src/content/dependencies/listArtTagsByUses.js b/src/content/dependencies/listArtTagsByUses.js index fcd324f7..5131580f 100644 --- a/src/content/dependencies/listArtTagsByUses.js +++ b/src/content/dependencies/listArtTagsByUses.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtTagGallery'], - extraDependencies: ['language', 'wikiData'], - sprawl: ({artTagData}) => ({artTagData}), @@ -17,8 +14,8 @@ export default { const counts = artTags.map(artTag => unique([ - ...artTag.directlyTaggedInThings, - ...artTag.indirectlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, ]).length); filterByCount(artTags, counts); diff --git a/src/content/dependencies/listArtistsByCommentaryEntries.js b/src/content/dependencies/listArtistsByCommentaryEntries.js index eff2dba3..ab7bde6c 100644 --- a/src/content/dependencies/listArtistsByCommentaryEntries.js +++ b/src/content/dependencies/listArtistsByCommentaryEntries.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtist'], - extraDependencies: ['language', 'wikiData'], - sprawl({artistData}) { return {artistData}; }, diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js index 41944959..2f8d6391 100644 --- a/src/content/dependencies/listArtistsByContributions.js +++ b/src/content/dependencies/listArtistsByContributions.js @@ -1,18 +1,8 @@ import {sortAlphabetically, sortByCount} from '#sort'; - -import { - accumulateSum, - empty, - filterByCount, - filterMultipleArrays, - stitchArrays, - unique, -} from '#sugar'; +import {empty, filterByCount, filterMultipleArrays, stitchArrays} + from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtist'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({artistData, wikiInfo}) { return { artistData, @@ -41,37 +31,46 @@ export default { query[countsKey] = counts; }; + const countContributions = (artist, keys) => { + const contribs = + keys + .flatMap(key => artist[key]) + .filter(contrib => contrib.countInContributionTotals); + + const things = + new Set(contribs.map(contrib => contrib.thing)); + + return things.size; + }; + queryContributionInfo( 'artistsByTrackContributions', 'countsByTrackContributions', artist => - (unique( - ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - ]).flat() - .map(({thing}) => thing) - )).length); + countContributions(artist, [ + 'trackArtistContributions', + 'trackContributorContributions', + ])); queryContributionInfo( 'artistsByArtworkContributions', 'countsByArtworkContributions', artist => - accumulateSum( - [ - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, - artist.trackCoverArtistContributions, - ], - contribs => contribs.length)); + countContributions(artist, [ + 'albumCoverArtistContributions', + 'albumWallpaperArtistContributions', + 'albumBannerArtistContributions', + 'trackCoverArtistContributions', + ])); if (sprawl.enableFlashesAndGames) { queryContributionInfo( 'artistsByFlashContributions', 'countsByFlashContributions', artist => - artist.flashContributorContributions.length); + countContributions(artist, [ + 'flashContributorContributions', + ])); } return query; diff --git a/src/content/dependencies/listArtistsByDuration.js b/src/content/dependencies/listArtistsByDuration.js index 6b2a18a0..1d550b26 100644 --- a/src/content/dependencies/listArtistsByDuration.js +++ b/src/content/dependencies/listArtistsByDuration.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtist'], - extraDependencies: ['language', 'wikiData'], - sprawl({artistData}) { return {artistData}; }, diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js index 0bf9dd2d..44564b4b 100644 --- a/src/content/dependencies/listArtistsByGroup.js +++ b/src/content/dependencies/listArtistsByGroup.js @@ -10,9 +10,6 @@ import { } from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl({artistData, wikiInfo}) { return {artistData, wikiInfo}; }, @@ -37,20 +34,25 @@ export default { ([ (unique( ([ - artist.albumArtistContributions, - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, + artist.albumArtistContributions + .map(contrib => contrib.thing), + artist.albumCoverArtistContributions + .map(contrib => contrib.thing.thing), + artist.albumWallpaperArtistContributions + .map(contrib => contrib.thing.thing), + artist.albumBannerArtistContributions + .map(contrib => contrib.thing.thing), ]).flat() - .map(({thing}) => thing) )).map(album => album.groups), (unique( ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - artist.trackCoverArtistContributions, + artist.trackArtistContributions + .map(contrib => contrib.thing), + artist.trackContributorContributions + .map(contrib => contrib.thing), + artist.trackCoverArtistContributions + .map(contrib => contrib.thing.thing), ]).flat() - .map(({thing}) => thing) )).map(track => track.album.groups), ]).flat() .map(groups => groups diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js index 27a2faa3..dc7341cf 100644 --- a/src/content/dependencies/listArtistsByLatestContribution.js +++ b/src/content/dependencies/listArtistsByLatestContribution.js @@ -11,15 +11,6 @@ import { const {Album, Flash} = T; export default { - contentDependencies: [ - 'generateListingPage', - 'linkAlbum', - 'linkArtist', - 'linkFlash', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({albumData, artistData, flashData, trackData, wikiInfo}) => ({albumData, artistData, flashData, trackData, enableFlashesAndGames: wikiInfo.enableFlashesAndGames}), @@ -98,13 +89,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/listArtistsByName.js b/src/content/dependencies/listArtistsByName.js index 93218492..8bee4947 100644 --- a/src/content/dependencies/listArtistsByName.js +++ b/src/content/dependencies/listArtistsByName.js @@ -3,9 +3,6 @@ import {stitchArrays} from '#sugar'; import {getArtistNumContributions} from '#wiki-data'; export default { - contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl: ({artistData, wikiInfo}) => ({artistData, wikiInfo}), diff --git a/src/content/dependencies/listGroupsByAlbums.js b/src/content/dependencies/listGroupsByAlbums.js index 4adfb6d9..64814640 100644 --- a/src/content/dependencies/listGroupsByAlbums.js +++ b/src/content/dependencies/listGroupsByAlbums.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listGroupsByCategory.js b/src/content/dependencies/listGroupsByCategory.js index 43919bef..4c10a1e4 100644 --- a/src/content/dependencies/listGroupsByCategory.js +++ b/src/content/dependencies/listGroupsByCategory.js @@ -1,9 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkGroup', 'linkGroupGallery'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupCategoryData}) { return {groupCategoryData}; }, diff --git a/src/content/dependencies/listGroupsByDuration.js b/src/content/dependencies/listGroupsByDuration.js index c79e1bc4..089915c2 100644 --- a/src/content/dependencies/listGroupsByDuration.js +++ b/src/content/dependencies/listGroupsByDuration.js @@ -3,9 +3,6 @@ import {filterByCount, stitchArrays} from '#sugar'; import {getTotalDuration} from '#wiki-data'; export default { - contentDependencies: ['generateListingPage', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listGroupsByLatestAlbum.js b/src/content/dependencies/listGroupsByLatestAlbum.js index 48319314..2d83a354 100644 --- a/src/content/dependencies/listGroupsByLatestAlbum.js +++ b/src/content/dependencies/listGroupsByLatestAlbum.js @@ -2,15 +2,6 @@ import {compareDates, sortChronologically} from '#sort'; import {filterMultipleArrays, sortMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateListingPage', - 'linkAlbum', - 'linkGroup', - 'linkGroupGallery', - ], - - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listGroupsByName.js b/src/content/dependencies/listGroupsByName.js index 696a49bd..e3308158 100644 --- a/src/content/dependencies/listGroupsByName.js +++ b/src/content/dependencies/listGroupsByName.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkGroup', 'linkGroupGallery'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listGroupsByTracks.js b/src/content/dependencies/listGroupsByTracks.js index 0b5e4e97..c9d97614 100644 --- a/src/content/dependencies/listGroupsByTracks.js +++ b/src/content/dependencies/listGroupsByTracks.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {accumulateSum, filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js index 79bba441..81eca274 100644 --- a/src/content/dependencies/listRandomPageLinks.js +++ b/src/content/dependencies/listRandomPageLinks.js @@ -2,14 +2,6 @@ import {sortChronologically} from '#sort'; import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateListingPage', - 'generateListRandomPageLinksAlbumLink', - 'linkGroup', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({albumData, wikiInfo}) => ({albumData, wikiInfo}), query(sprawl, spec) { diff --git a/src/content/dependencies/listTracksByAlbum.js b/src/content/dependencies/listTracksByAlbum.js index b2405034..f6858ada 100644 --- a/src/content/dependencies/listTracksByAlbum.js +++ b/src/content/dependencies/listTracksByAlbum.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js index dcfaeaf0..9d63f19b 100644 --- a/src/content/dependencies/listTracksByDate.js +++ b/src/content/dependencies/listTracksByDate.js @@ -2,9 +2,6 @@ import {sortAlbumsTracksChronologically} from '#sort'; import {chunkByProperties, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl: ({trackData}) => ({trackData}), query({trackData}, spec) { diff --git a/src/content/dependencies/listTracksByDuration.js b/src/content/dependencies/listTracksByDuration.js index 64feb4f1..95fd28b2 100644 --- a/src/content/dependencies/listTracksByDuration.js +++ b/src/content/dependencies/listTracksByDuration.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({trackData}) { return {trackData}; }, diff --git a/src/content/dependencies/listTracksByDurationInAlbum.js b/src/content/dependencies/listTracksByDurationInAlbum.js index c1ea32a1..ad44c7b2 100644 --- a/src/content/dependencies/listTracksByDurationInAlbum.js +++ b/src/content/dependencies/listTracksByDurationInAlbum.js @@ -2,9 +2,6 @@ import {sortByCount, sortChronologically} from '#sort'; import {filterByCount, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listTracksByName.js b/src/content/dependencies/listTracksByName.js index 773b0473..a9c2c504 100644 --- a/src/content/dependencies/listTracksByName.js +++ b/src/content/dependencies/listTracksByName.js @@ -1,9 +1,6 @@ import {sortAlphabetically} from '#sort'; export default { - contentDependencies: ['generateListingPage', 'linkTrack'], - extraDependencies: ['wikiData'], - sprawl({trackData}) { return {trackData}; }, diff --git a/src/content/dependencies/listTracksByTimesReferenced.js b/src/content/dependencies/listTracksByTimesReferenced.js index 5838ded0..8a57e1a6 100644 --- a/src/content/dependencies/listTracksByTimesReferenced.js +++ b/src/content/dependencies/listTracksByTimesReferenced.js @@ -2,9 +2,6 @@ import {sortAlbumsTracksChronologically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({trackData}) { return {trackData}; }, diff --git a/src/content/dependencies/listTracksInFlashesByAlbum.js b/src/content/dependencies/listTracksInFlashesByAlbum.js index 8ca0d993..db5472db 100644 --- a/src/content/dependencies/listTracksInFlashesByAlbum.js +++ b/src/content/dependencies/listTracksInFlashesByAlbum.js @@ -2,9 +2,6 @@ import {sortChronologically} from '#sort'; import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listTracksInFlashesByFlash.js b/src/content/dependencies/listTracksInFlashesByFlash.js index 6ab954ed..325b3cb5 100644 --- a/src/content/dependencies/listTracksInFlashesByFlash.js +++ b/src/content/dependencies/listTracksInFlashesByFlash.js @@ -2,9 +2,6 @@ import {sortFlashesChronologically} from '#sort'; import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'], - extraDependencies: ['wikiData'], - sprawl({flashData}) { return {flashData}; }, diff --git a/src/content/dependencies/listTracksNeedingLyrics.js b/src/content/dependencies/listTracksNeedingLyrics.js new file mode 100644 index 00000000..d21fcd06 --- /dev/null +++ b/src/content/dependencies/listTracksNeedingLyrics.js @@ -0,0 +1,7 @@ +export default { + relations: (relation, spec) => + ({page: relation('listTracksWithExtra', spec, 'needsLyrics', 'truthy')}), + + generate: (relations) => + relations.page, +}; diff --git a/src/content/dependencies/listTracksWithExtra.js b/src/content/dependencies/listTracksWithExtra.js index c7f42f9d..09d8ee21 100644 --- a/src/content/dependencies/listTracksWithExtra.js +++ b/src/content/dependencies/listTracksWithExtra.js @@ -2,9 +2,6 @@ import {sortChronologically} from '#sort'; import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listTracksWithLyrics.js b/src/content/dependencies/listTracksWithLyrics.js index a13a76f0..79d76bf3 100644 --- a/src/content/dependencies/listTracksWithLyrics.js +++ b/src/content/dependencies/listTracksWithLyrics.js @@ -1,8 +1,6 @@ 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/listTracksWithMidiProjectFiles.js b/src/content/dependencies/listTracksWithMidiProjectFiles.js index 418af4c2..9a48f6ae 100644 --- a/src/content/dependencies/listTracksWithMidiProjectFiles.js +++ b/src/content/dependencies/listTracksWithMidiProjectFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listTracksWithExtra'], - relations: (relation, spec) => ({page: relation('listTracksWithExtra', spec, 'midiProjectFiles', 'array')}), diff --git a/src/content/dependencies/listTracksWithSheetMusicFiles.js b/src/content/dependencies/listTracksWithSheetMusicFiles.js index 0c6761eb..f0ba4196 100644 --- a/src/content/dependencies/listTracksWithSheetMusicFiles.js +++ b/src/content/dependencies/listTracksWithSheetMusicFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listTracksWithExtra'], - relations: (relation, spec) => ({page: relation('listTracksWithExtra', spec, 'sheetMusicFiles', 'array')}), diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index f56a1da9..8e902647 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -1,7 +1,11 @@ +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, @@ -25,14 +29,36 @@ const commonMarkedOptions = { const multilineMarked = new Marked({ ...commonMarkedOptions, + + renderer: { + code({text}) { + let lines = text + .replace(/^\n+/, '') + .replace(/\n+$/, '') + .split('\n'); + + lines = lines + .map(line => line + .replace(/^ +/, spaces => ' '.repeat(spaces.length)) + .replaceAll(/ {2,}/g, spaces => ' '.repeat(spaces.length))); + + return ( + `<pre class="content-code"><span><code>` + + (lines.length > 1 ? '\n' : '') + + lines.join('<br>\n') + + (lines.length > 1 ? '\n' : '') + + `</pre></span></code>` + ); + }, + }, }); const inlineMarked = new Marked({ ...commonMarkedOptions, renderer: { - paragraph(text) { - return text; + paragraph({tokens}) { + return this.parser.parseInline(tokens); }, }, }); @@ -45,26 +71,47 @@ function getPlaceholder(node, content) { return {type: 'text', data: content.slice(node.i, node.iEnd)}; } -export default { - contentDependencies: [ - ...( - Object.values(replacerSpec) - .map(description => description.link) - .filter(Boolean)), - 'image', - 'linkExternal', - ], - - extraDependencies: ['html', 'language', 'to', 'wikiData'], +function getArg(node, argKey) { + return ( + node.data.args + ?.find(({key}) => key.data === argKey) + ?.value ?? + null); +} +export default { sprawl(wikiData, content) { - const find = bindFind(wikiData); + const find = + bindFind(wikiData, { + mode: 'quiet', + fuzz: { + capitalization: true, + kebab: true, + }, + }); - const parsedNodes = parseInput(content ?? ''); + const {result: parsedNodes, error} = + parseContentNodes(content ?? '', {errorMode: 'return'}); return { + error, + nodes: parsedNodes .map(node => { + if (node.type === 'tooltip') { + return { + i: node.i, + iEnd: node.iEnd, + type: 'tooltip', + data: { + // No recursion yet. Sorry! + tooltip: node.data.content[0].data, + label: node.data.label[0].data, + link: null, + }, + }; + } + if (node.type !== 'tag') { return node; } @@ -85,7 +132,7 @@ export default { } if (spec.link) { - let data = {link: spec.link}; + let data = {link: spec.link, replacerKey, replacerValue}; determineData: { // No value at all: this is an index link. @@ -124,15 +171,46 @@ export default { data.label = enteredLabel ?? - (transformName && data.thing.name - ? transformName(data.thing.name, node, content) - : null); + + (transformName && data.thing.name && + replacerKeyImplied && replacerValue === data.thing.name + + ? transformName(data.thing.name, node, content) + : null) ?? + + (replacerKeyImplied + ? replacerValue + : null); data.hash = enteredHash ?? null; return {i: node.i, iEnd: node.iEnd, type: 'internal-link', data}; } + if (replacerKey === 'tooltip') { + // TODO: Again, no recursive nodes. Sorry! + // const enteredLabel = node.data.label && transformNode(node.data.label, opts); + const enteredLabel = node.data.label?.data; + + return { + i: node.i, + iEnd: node.iEnd, + type: 'tooltip', + data: { + tooltip: + replacerValue ?? '(empty tooltip...)', + + label: + enteredLabel ?? '(tooltip without label)', + + link: + (getArg(node, 'link') + ? getArg(node, 'link')[0].data + : null), + }, + }; + } + // This will be another {type: 'tag'} node which gets processed in // generate. Extract replacerKey and replacerValue now, since it'd // be a pain to deal with later. @@ -140,8 +218,8 @@ export default { ...node, data: { ...node.data, - replacerKey: node.data.replacerKey.data, - replacerValue: node.data.replacerValue[0].data, + replacerKey, + replacerValue, }, }; }), @@ -152,25 +230,11 @@ export default { return { content, + error: + sprawl.error, + nodes: - sprawl.nodes - .map(node => { - switch (node.type) { - // 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: - return node; - } - }), + sprawl.nodes, }; }, @@ -184,10 +248,18 @@ export default { link: relation(name, arg), label: node.data.label, hash: node.data.hash, + name: arg?.name, + shortName: arg?.shortName ?? arg?.nameShort, } : getPlaceholder(node, content)); return { + textWithTooltip: + relation('generateTextWithTooltip'), + + tooltip: + relation('generateTooltip'), + internalLinks: nodes .filter(({type}) => type === 'internal-link') @@ -206,11 +278,15 @@ export default { externalLinks: nodes .filter(({type}) => type === 'external-link') - .map(node => { - const {href} = node.data; + .map(({data: {href}}) => + relation('linkExternal', href)), - return relation('linkExternal', href); - }), + externalLinksForTooltipNodes: + nodes + .filter(({type}) => type === 'tooltip') + .filter(({data}) => data.link) + .map(({data: {link: href}}) => + relation('linkExternal', href)), images: nodes @@ -241,19 +317,63 @@ export default { default: true, }, + textOnly: { + type: 'boolean', + default: false, + }, + thumb: { validate: v => v.is('small', 'medium', 'large'), default: 'large', }, + + substitute: { + validate: v => + v.strictArrayOf( + v.validateProperties({ + match: v.validateProperties({ + replacerKey: v.isString, + replacerValue: v.isString, + }), + + substitute: v.isHTML, + + apply: v.optional(v.isFunction), + })), + }, }, - generate(data, relations, slots, {html, language, to}) { + generate(data, relations, slots, {html, language, niceShowAggregate, to}) { + if (data.error) { + logWarn`Error in content text.`; + niceShowAggregate(data.error); + } + let imageIndex = 0; let internalLinkIndex = 0; let externalLinkIndex = 0; + let externalLinkForTooltipNodeIndex = 0; let offsetTextNode = 0; + const substitutions = + (slots.substitute + ? slots.substitute.slice() + : []); + + const pickSubstitution = node => { + const index = + substitutions.findIndex(({match}) => + match.replacerKey === node.data.replacerKey && + match.replacerValue === node.data.replacerValue); + + if (index === -1) { + return null; + } + + return substitutions.splice(index, 1).at(0); + }; + const contentFromNodes = data.nodes.map((node, index) => { const nextNode = data.nodes[index + 1]; @@ -272,6 +392,25 @@ export default { } }; + const substitution = pickSubstitution(node); + + if (substitution) { + const source = + substitution.substitute; + + let substitute = source; + + if (substitution.apply) { + const result = substitution.apply(source, node); + + if (result !== undefined) { + substitute = result; + } + } + + return {type: 'substitution', data: substitute}; + } + switch (node.type) { case 'text': { const text = node.data.slice(offsetTextNode); @@ -305,9 +444,8 @@ export default { height && {height}, style && {style}, - align === 'center' && - !link && - {class: 'align-center'}, + align && !link && + {class: 'align-' + align}, pixelate && {class: 'pixelate'}); @@ -318,8 +456,8 @@ export default { {href: link}, {target: '_blank'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, {title: language.encapsulate('misc.external.opensInNewTab', capsule => @@ -369,8 +507,8 @@ export default { inline: false, data: html.tag('div', {class: 'content-image-container'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, image), }; @@ -382,22 +520,31 @@ export default { ? to('media.path', node.src.slice('media/'.length)) : node.src); - const {width, height, align, pixelate} = node; + const {width, height, align, inline, pixelate} = node; - const content = - html.tag('div', {class: 'content-video-container'}, - align === 'center' && - {class: 'align-center'}, + const video = + html.tag('video', + src && {src}, + width && {width}, + height && {height}, - html.tag('video', - src && {src}, - width && {width}, - height && {height}, + {controls: true}, - {controls: true}, + align && inline && + {class: 'align-' + align}, + + pixelate && + {class: 'pixelate'}); + + const content = + (inline + ? video + : html.tag('div', {class: 'content-video-container'}, + align && + {class: 'align-' + align}, + + video)); - pixelate && - {class: 'pixelate'})); return { type: 'processed-video', @@ -411,15 +558,14 @@ export default { ? to('media.path', node.src.slice('media/'.length)) : node.src); - const {align, inline} = node; + const {align, inline, nameless} = node; const audio = html.tag('audio', src && {src}, - align === 'center' && - inline && - {class: 'align-center'}, + align && inline && + {class: 'align-' + align}, {controls: true}); @@ -427,10 +573,17 @@ export default { (inline ? audio : html.tag('div', {class: 'content-audio-container'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, + + [ + !nameless && + html.tag('a', {class: 'filename'}, + src && {href: src}, + language.sanitize(basename(node.src))), - audio)); + audio, + ])); return { type: 'processed-audio', @@ -452,7 +605,17 @@ export default { nodeFromRelations.link, {slots: ['content', 'hash']}); - const {label, hash} = nodeFromRelations; + 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 @@ -466,7 +629,7 @@ export default { try { link.getSlotDescription('preferShortName'); hasPreferShortNameSlot = true; - } catch (error) { + } catch { hasPreferShortNameSlot = false; } @@ -479,7 +642,7 @@ export default { try { link.getSlotDescription('tooltipStyle'); hasTooltipStyleSlot = true; - } catch (error) { + } catch { hasTooltipStyleSlot = false; } @@ -503,9 +666,16 @@ export default { } case 'external-link': { - const {label} = node.data; const externalLink = relations.externalLinks[externalLinkIndex++]; + const label = + node.data.label ?? + node.data.href.replace(/^https?:\/\//, ''); + + if (slots.textOnly) { + return {type: 'text', data: label}; + } + externalLink.setSlots({ content: label, fromContent: true, @@ -526,6 +696,52 @@ export default { return {type: 'processed-external-link', data: externalLink}; } + case 'tooltip': { + const {label, link, tooltip: tooltipContent} = node.data; + + const externalLink = + (link + ? relations.externalLinksForTooltipNodes + .at(externalLinkForTooltipNodeIndex++) + : null); + + if (externalLink) { + externalLink.setSlots({ + content: label, + fromContent: true, + }); + + if (slots.indicateExternalLinks) { + externalLink.setSlots({ + indicateExternal: true, + disableBrowserTooltip: true, + tab: 'separate', + style: 'platform', + }); + } + } + + const textWithTooltip = relations.textWithTooltip.clone(); + const tooltip = relations.tooltip.clone(); + + tooltip.setSlots({ + attributes: {class: 'content-tooltip'}, + content: tooltipContent, // Not sanitized! + }); + + textWithTooltip.setSlots({ + attributes: [ + {class: 'content-tooltip-guy'}, + externalLink && {class: 'has-link'}, + ], + + text: externalLink ?? label, + tooltip, + }); + + return {type: 'processed-tooltip', data: textWithTooltip}; + } + case 'tag': { const {replacerKey, replacerValue} = node.data; @@ -542,12 +758,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: @@ -666,20 +889,37 @@ export default { // This is separated into its own function just since we're gonna reuse // it in a minute if everything goes to heck in lyrics mode. const transformMultiline = () => { - const markedInput = - extractNonTextNodes() - // Compress multiple line breaks into single line breaks, - // except when they're preceding or following indented - // text (by at least two spaces). - .replace(/(?<! .*)\n{2,}(?!^ )/gm, '\n') /* eslint-disable-line no-regex-spaces */ - // Expand line breaks which don't follow a list, quote, - // or <br> / " ", and which don't precede or follow - // indented text (by at least two spaces). - .replace(/(?<!^ *(?:-|\d+\.).*|^>.*|^ .*\n*| $|<br>$)\n+(?! |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */ - // Expand line breaks which are at the end of a list. - .replace(/(?<=^ *(?:-|\d+\.).*)\n+(?!^ *(?:-|\d+\.))/gm, '\n\n') - // Expand line breaks which are at the end of a quote. - .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n'); + let fencedCode = []; + + const fencedCodePlaceholder = + `<span class="INSERT-FENCED-CODE"></span>`; + + let markedInput = extractNonTextNodes(); + + markedInput = markedInput + .replaceAll(/```(?:[\s\S](?!```))*\n```/g, (match) => { + fencedCode.push(match); + return fencedCodePlaceholder; + }); + + markedInput = markedInput + // Compress multiple line breaks into single line breaks, + // except when they're preceding or following indented + // text (by at least two spaces) or blockquotes. + .replace(/(?<!^ .*|^>.*)\n{2,}(?!^ |^>)/gm, '\n') /* eslint-disable-line no-regex-spaces */ + // Expand line breaks which don't follow a list, quote, + // or <br> / " ", and which don't precede or follow + // indented text (by at least two spaces). + .replace(/(?<!^ *(?:-|\d+\.).*|^>.*|^ .*\n*| $|<br>$)\n+(?! |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */ + // Expand line breaks which are at the end of a list. + .replace(/(?<=^ *(?:-|\d+\.).*)\n+(?!^ *(?:-|\d+\.))/gm, '\n\n') + // Expand line breaks which are at the end of a quote. + .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n'); + + fencedCode = fencedCode.reverse(); + + markedInput = markedInput + .replaceAll(fencedCodePlaceholder, () => fencedCode.pop()); const markedOutput = multilineMarked.parse(markedInput); |