diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2023-12-03 17:55:58 -0400 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2023-12-03 17:55:58 -0400 |
commit | 11493b1a70c26d9aa11b98acf93b4d09d89f88bf (patch) | |
tree | fe062bc0b32698c3af6c41f4c7043dc1afb09b92 /src/content | |
parent | 213bddbd9851ee01f256835b1bca0c4bc0cf5fc6 (diff) | |
parent | 7039d7fa471318df40c1905cd5ac52688dc6adcf (diff) |
Merge branch 'preview' into news-tweaks
Diffstat (limited to 'src/content')
55 files changed, 1992 insertions, 1082 deletions
diff --git a/src/content/dependencies/generateAbsoluteDatetimestamp.js b/src/content/dependencies/generateAbsoluteDatetimestamp.js new file mode 100644 index 00000000..63acecf2 --- /dev/null +++ b/src/content/dependencies/generateAbsoluteDatetimestamp.js @@ -0,0 +1,41 @@ +export default { + contentDependencies: ['generateDatetimestampTemplate'], + extraDependencies: ['html', 'language'], + + data: (date) => + ({date}), + + relations: (relation) => + ({template: relation('generateDatetimestampTemplate')}), + + slots: { + style: { + validate: v => v.is('full', 'year'), + 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), + + tooltipContent: + slots.tooltip && + slots.style === 'year' && + language.formatDate(data.date), + + datetime: + data.date.toISOString(), + }), +}; diff --git a/src/content/dependencies/generateAdditionalNamesBox.js b/src/content/dependencies/generateAdditionalNamesBox.js new file mode 100644 index 00000000..63427c58 --- /dev/null +++ b/src/content/dependencies/generateAdditionalNamesBox.js @@ -0,0 +1,20 @@ +export default { + contentDependencies: ['generateAdditionalNamesBoxItem'], + extraDependencies: ['html', 'language'], + + relations: (relation, additionalNames) => ({ + items: + additionalNames + .map(entry => relation('generateAdditionalNamesBoxItem', entry)), + }), + + generate: (relations, {html, language}) => + html.tag('div', {id: 'additional-names-box'}, [ + html.tag('p', + language.$('misc.additionalNames.title')), + + html.tag('ul', + relations.items + .map(item => html.tag('li', item))), + ]), +}; diff --git a/src/content/dependencies/generateAdditionalNamesBoxItem.js b/src/content/dependencies/generateAdditionalNamesBoxItem.js new file mode 100644 index 00000000..bb4c8477 --- /dev/null +++ b/src/content/dependencies/generateAdditionalNamesBoxItem.js @@ -0,0 +1,68 @@ +import {stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['linkTrack', 'transformContent'], + extraDependencies: ['html', 'language'], + + relations: (relation, entry) => ({ + nameContent: + relation('transformContent', entry.name), + + annotationContent: + (entry.annotation + ? relation('transformContent', entry.annotation) + : null), + + trackLinks: + (entry.from + ? entry.from.map(track => relation('linkTrack', track)) + : null), + }), + + data: (entry) => ({ + albumNames: + (entry.from + ? entry.from.map(track => track.album.name) + : null), + }), + + generate: (data, relations, {html, language}) => { + const prefix = 'misc.additionalNames.item'; + + const itemParts = [prefix]; + const itemOptions = {}; + + itemOptions.name = + html.tag('span', {class: 'additional-name'}, + relations.nameContent.slot('mode', 'inline')); + + const accentParts = [prefix, 'accent']; + const accentOptions = {}; + + if (relations.annotationContent) { + accentParts.push('withAnnotation'); + accentOptions.annotation = + relations.annotationContent.slot('mode', 'inline'); + } + + if (relations.trackLinks) { + accentParts.push('withAlbums'); + accentOptions.albums = + language.formatConjunctionList( + stitchArrays({ + trackLink: relations.trackLinks, + albumName: data.albumNames, + }).map(({trackLink, albumName}) => + trackLink.slot('content', albumName))); + } + + if (accentParts.length > 2) { + itemParts.push('withAccent'); + itemOptions.accent = + html.tag('span', {class: 'accent'}, + language.$(...accentParts, accentOptions)); + } + + return language.$(...itemParts, itemOptions); + }, +}; diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 3ad1549e..5a7142e5 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -1,4 +1,4 @@ -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; export default { contentDependencies: [ @@ -6,13 +6,13 @@ export default { 'generateAlbumNavAccent', 'generateAlbumSidebarTrackSection', 'generateAlbumStyleRules', - 'generateColorStyleVariables', + 'generateCommentaryEntry', 'generateContentHeading', 'generateTrackCoverArtwork', 'generatePageLayout', 'linkAlbum', + 'linkExternal', 'linkTrack', - 'transformContent', ], extraDependencies: ['html', 'language'], @@ -33,13 +33,23 @@ export default { relation('generateAlbumNavAccent', album, null); if (album.commentary) { + relations.albumCommentaryHeading = + relation('generateContentHeading'); + + relations.albumCommentaryLink = + relation('linkAlbum', album); + + relations.albumCommentaryListeningLinks = + album.urls.map(url => relation('linkExternal', url)); + if (album.hasCoverArt) { relations.albumCommentaryCover = relation('generateAlbumCoverArtwork', album); } - relations.albumCommentaryContent = - relation('transformContent', album.commentary); + relations.albumCommentaryEntries = + album.commentary + .map(entry => relation('generateCommentaryEntry', entry)); } const tracksWithCommentary = @@ -54,6 +64,11 @@ export default { tracksWithCommentary .map(track => relation('linkTrack', track)); + relations.trackCommentaryListeningLinks = + tracksWithCommentary + .map(track => + track.urls.map(url => relation('linkExternal', url))); + relations.trackCommentaryCovers = tracksWithCommentary .map(track => @@ -61,16 +76,11 @@ export default { ? relation('generateTrackCoverArtwork', track) : null)); - relations.trackCommentaryContent = - tracksWithCommentary - .map(track => relation('transformContent', track.commentary)); - - relations.trackCommentaryColorVariables = + relations.trackCommentaryEntries = tracksWithCommentary .map(track => - (track.color === album.color - ? null - : relation('generateColorStyleVariables'))); + track.commentary + .map(entry => relation('generateCommentaryEntry', entry))); relations.sidebarAlbumLink = relation('linkAlbum', album); @@ -97,11 +107,15 @@ export default { ? [album, ...tracksWithCommentary] : tracksWithCommentary); - data.entryCount = thingsWithCommentary.length; + data.entryCount = + thingsWithCommentary + .flatMap(({commentary}) => commentary) + .length; data.wordCount = thingsWithCommentary - .map(({commentary}) => commentary) + .flatMap(({commentary}) => commentary) + .map(({body}) => body) .join(' ') .split(' ') .length; @@ -146,40 +160,75 @@ export default { language.countCommentaryEntries(data.entryCount, {unit: true})), })), - relations.albumCommentaryContent && [ - html.tag('h3', - {class: ['content-heading']}, - language.$('albumCommentaryPage.entry.title.albumCommentary')), + relations.albumCommentaryEntries && [ + relations.albumCommentaryHeading.slots({ + tag: 'h3', + color: data.color, + + title: + language.$('albumCommentaryPage.entry.title.albumCommentary', { + album: relations.albumCommentaryLink, + }), + + accent: + !empty(relations.albumCommentaryListeningLinks) && + language.$('albumCommentaryPage.entry.title.albumCommentary.accent', { + listeningLinks: + language.formatUnitList( + relations.albumCommentaryListeningLinks + .map(link => link.slots({ + context: 'album', + tab: 'separate', + }))), + }), + }), relations.albumCommentaryCover ?.slots({mode: 'commentary'}), - html.tag('blockquote', - relations.albumCommentaryContent), + relations.albumCommentaryEntries, ], stitchArrays({ heading: relations.trackCommentaryHeadings, link: relations.trackCommentaryLinks, + listeningLinks: relations.trackCommentaryListeningLinks, directory: data.trackCommentaryDirectories, cover: relations.trackCommentaryCovers, - content: relations.trackCommentaryContent, - colorVariables: relations.trackCommentaryColorVariables, + entries: relations.trackCommentaryEntries, color: data.trackCommentaryColors, - }).map(({heading, link, directory, cover, content, colorVariables, color}) => [ + }).map(({ + heading, + link, + listeningLinks, + directory, + cover, + entries, + color, + }) => [ heading.slots({ tag: 'h3', id: directory, - title: link, + color, + + title: + language.$('albumCommentaryPage.entry.title.trackCommentary', { + track: link, + }), + + accent: + !empty(listeningLinks) && + language.$('albumCommentaryPage.entry.title.trackCommentary.accent', { + listeningLinks: + language.formatUnitList( + listeningLinks.map(link => + link.slot('tab', 'separate'))), + }), }), cover?.slots({mode: 'commentary'}), - html.tag('blockquote', - (color - ? {style: colorVariables.slot('color', color).content} - : {}), - content), + entries.map(entry => entry.slot('color', color)), ]), ], @@ -201,6 +250,7 @@ export default { ], leftSidebarStickyMode: 'column', + leftSidebarClass: 'commentary-track-list-sidebar-box', leftSidebarContent: [ html.tag('h1', relations.sidebarAlbumLink), relations.sidebarTrackSections.map(section => diff --git a/src/content/dependencies/generateAlbumCoverArtwork.js b/src/content/dependencies/generateAlbumCoverArtwork.js index cbec930e..ce8cde21 100644 --- a/src/content/dependencies/generateAlbumCoverArtwork.js +++ b/src/content/dependencies/generateAlbumCoverArtwork.js @@ -1,12 +1,22 @@ export default { contentDependencies: ['generateCoverArtwork'], - relations: (relation, album) => - ({coverArtwork: relation('generateCoverArtwork', album.artTags)}), + relations: (relation, album) => ({ + coverArtwork: + relation('generateCoverArtwork', album.artTags), + }), - data: (album) => - ({path: ['media.albumCover', album.directory, album.coverArtFileExtension]}), + data: (album) => ({ + path: + ['media.albumCover', album.directory, album.coverArtFileExtension], + + color: + album.color, + }), generate: (data, relations) => - relations.coverArtwork.slot('path', data.path), + relations.coverArtwork.slots({ + path: data.path, + color: data.color, + }), }; diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index 5fe27caf..90a120ca 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -17,6 +17,7 @@ export default { 'generateAlbumStyleRules', 'generateAlbumTrackList', 'generateChronologyLinks', + 'generateCommentarySection', 'generateContentHeading', 'generatePageLayout', 'linkAlbum', @@ -126,13 +127,8 @@ export default { // Section: Artist commentary if (album.commentary) { - const artistCommentary = sections.artistCommentary = {}; - - artistCommentary.heading = - relation('generateContentHeading'); - - artistCommentary.content = - relation('transformContent', album.commentary); + sections.artistCommentary = + relation('generateCommentarySection', album.commentary); } return relations; @@ -235,17 +231,7 @@ export default { sec.additionalFiles.additionalFilesList, ], - sec.artistCommentary && [ - sec.artistCommentary.heading - .slots({ - id: 'artist-commentary', - title: language.$('releaseInfo.artistCommentary') - }), - - html.tag('blockquote', - sec.artistCommentary.content - .slot('mode', 'multiline')), - ], + sec.artistCommentary, ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index 7eb1dac0..01c88bf7 100644 --- a/src/content/dependencies/generateAlbumNavAccent.js +++ b/src/content/dependencies/generateAlbumNavAccent.js @@ -92,7 +92,7 @@ export default { html.tag('a', { href: '#', - 'data-random': 'track-in-album', + 'data-random': 'track-in-sidebar', id: 'random-button', }, (data.isTrackPage diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js index d6405283..dd5baab9 100644 --- a/src/content/dependencies/generateAlbumReleaseInfo.js +++ b/src/content/dependencies/generateAlbumReleaseInfo.js @@ -94,7 +94,11 @@ export default { links: language.formatDisjunctionList( relations.externalLinks - .map(link => link.slot('mode', 'album'))), + .map(link => + link.slots({ + context: 'album', + style: 'normal', + }))), })), ]); }, diff --git a/src/content/dependencies/generateAlbumSidebar.js b/src/content/dependencies/generateAlbumSidebar.js index a84f4357..5ef4501b 100644 --- a/src/content/dependencies/generateAlbumSidebar.js +++ b/src/content/dependencies/generateAlbumSidebar.js @@ -30,6 +30,7 @@ export default { generate(data, relations, {html}) { const trackListBox = { + class: 'track-list-sidebar-box', content: html.tags([ html.tag('h1', relations.albumLink), @@ -40,8 +41,10 @@ export default { if (data.isAlbumPage) { const groupBoxes = relations.groupBoxes - .map(content => content.slot('mode', 'album')) - .map(content => ({content})); + .map(content => ({ + class: 'individual-group-sidebar-box', + content: content.slot('mode', 'album'), + })); return { leftSidebarMultiple: [ @@ -52,6 +55,7 @@ export default { } const conjoinedGroupBox = { + class: 'conjoined-group-sidebar-box', content: relations.groupBoxes .flatMap((content, i, {length}) => [ diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js index 331ddaba..f3705450 100644 --- a/src/content/dependencies/generateAlbumSidebarGroupBox.js +++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js @@ -66,7 +66,10 @@ export default { !empty(relations.externalLinks) && html.tag('p', language.$('releaseInfo.visitOn', { - links: language.formatDisjunctionList(relations.externalLinks), + links: + language.formatDisjunctionList( + relations.externalLinks + .map(link => link.slot('context', 'group'))), })), slots.mode === 'album' && diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index 03bc0af5..1b85680f 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -161,7 +161,13 @@ export default { sec.visit && html.tag('p', language.$('releaseInfo.visitOn', { - links: language.formatDisjunctionList(sec.visit.externalLinks), + links: + language.formatDisjunctionList( + sec.visit.externalLinks.map(link => + link.slots({ + context: 'artist', + style: 'platform', + }))), })), sec.artworks?.artistGalleryLink && diff --git a/src/content/dependencies/generateColorStyleRules.js b/src/content/dependencies/generateColorStyleRules.js index 1b316a3c..3f1d0130 100644 --- a/src/content/dependencies/generateColorStyleRules.js +++ b/src/content/dependencies/generateColorStyleRules.js @@ -18,9 +18,12 @@ export default { `:root {`, ...( relations.variables - .slot('color', slots.color) + .slots({ + color: slots.color, + context: 'page-root', + mode: 'property-list', + }) .content - .split(';') .map(line => line + ';')), `}`, ].join('\n'); diff --git a/src/content/dependencies/generateColorStyleVariables.js b/src/content/dependencies/generateColorStyleVariables.js index f30d786b..7cd04bd1 100644 --- a/src/content/dependencies/generateColorStyleVariables.js +++ b/src/content/dependencies/generateColorStyleVariables.js @@ -2,7 +2,23 @@ export default { extraDependencies: ['html', 'getColors'], slots: { - color: {validate: v => v.isColor}, + color: { + validate: v => v.isColor, + }, + + context: { + validate: v => v.is( + 'any-content', + 'page-root', + 'primary-only'), + + default: 'any-content', + }, + + mode: { + validate: v => v.is('style', 'property-list'), + default: 'style', + }, }, generate(slots, {getColors}) { @@ -18,7 +34,7 @@ export default { shadow, } = getColors(slots.color); - return [ + let anyContent = [ `--primary-color: ${primary}`, `--dark-color: ${dark}`, `--dim-color: ${dim}`, @@ -26,6 +42,35 @@ export default { `--bg-color: ${bg}`, `--bg-black-color: ${bgBlack}`, `--shadow-color: ${shadow}`, - ].join('; '); + ]; + + let selectedProperties; + + switch (slots.context) { + case 'any-content': + selectedProperties = anyContent; + break; + + case 'page-root': + selectedProperties = [ + ...anyContent, + `--page-primary-color: ${primary}`, + ]; + break; + + case 'primary-only': + selectedProperties = [ + `--primary-color: ${primary}`, + ]; + break; + } + + switch (slots.mode) { + case 'style': + return selectedProperties.join('; '); + + case 'property-list': + return selectedProperties; + } }, }; diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js new file mode 100644 index 00000000..0b2b2558 --- /dev/null +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -0,0 +1,99 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'generateColorStyleVariables', + 'linkArtist', + 'transformContent', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, entry) => ({ + artistLinks: + (!empty(entry.artists) && !entry.artistDisplayText + ? entry.artists + .map(artist => relation('linkArtist', artist)) + : null), + + artistsContent: + (entry.artistDisplayText + ? relation('transformContent', entry.artistDisplayText) + : null), + + annotationContent: + (entry.annotation + ? relation('transformContent', entry.annotation) + : null), + + bodyContent: + (entry.body + ? relation('transformContent', entry.body) + : null), + + colorVariables: + relation('generateColorStyleVariables'), + }), + + data: (entry) => ({ + date: entry.date, + }), + + slots: { + color: {validate: v => v.isColor}, + }, + + generate(data, relations, slots, {html, language}) { + const artistsSpan = + html.tag('span', {class: 'commentary-entry-artists'}, + (relations.artistsContent + ? relations.artistsContent.slot('mode', 'inline') + : relations.artistLinks + ? language.formatConjunctionList(relations.artistLinks) + : language.$('misc.artistCommentary.entry.title.noArtists'))); + + const accentParts = ['misc.artistCommentary.entry.title.accent']; + const accentOptions = {}; + + if (relations.annotationContent) { + accentParts.push('withAnnotation'); + accentOptions.annotation = + relations.annotationContent.slot('mode', 'inline'); + } + + if (data.date) { + accentParts.push('withDate'); + accentOptions.date = + language.formatDate(data.date); + } + + const accent = + (accentParts.length > 1 + ? html.tag('span', {class: 'commentary-entry-accent'}, + language.$(...accentParts, accentOptions)) + : null); + + const titleParts = ['misc.artistCommentary.entry.title']; + const titleOptions = {artists: artistsSpan}; + + if (accent) { + titleParts.push('withAccent'); + titleOptions.accent = accent; + } + + const style = + (slots.color + ? relations.colorVariables + .slot('color', slots.color) + .content + : null); + + return html.tags([ + html.tag('p', {class: 'commentary-entry-heading', style}, + language.$(...titleParts, titleOptions)), + + html.tag('blockquote', {class: 'commentary-entry-body', style}, + relations.bodyContent.slot('mode', 'multiline')), + ]); + }, +}; diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js index 1d381bff..5d38941a 100644 --- a/src/content/dependencies/generateCommentaryIndexPage.js +++ b/src/content/dependencies/generateCommentaryIndexPage.js @@ -19,13 +19,13 @@ export default { query.albums.map(album => [album, ...album.tracks] .filter(({commentary}) => commentary) - .map(({commentary}) => commentary)); + .flatMap(({commentary}) => commentary)); query.wordCounts = entries.map(entries => accumulateSum( entries, - entry => entry.split(' ').length)); + entry => entry.body.split(' ').length)); query.entryCounts = entries.map(entries => entries.length); diff --git a/src/content/dependencies/generateCommentarySection.js b/src/content/dependencies/generateCommentarySection.js new file mode 100644 index 00000000..8ae1b2d0 --- /dev/null +++ b/src/content/dependencies/generateCommentarySection.js @@ -0,0 +1,29 @@ +export default { + contentDependencies: [ + 'transformContent', + 'generateCommentaryEntry', + 'generateContentHeading', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, entries) => ({ + heading: + relation('generateContentHeading'), + + entries: + entries.map(entry => + relation('generateCommentaryEntry', entry)), + }), + + generate: (relations, {html, language}) => + html.tags([ + relations.heading + .slots({ + id: 'artist-commentary', + title: language.$('misc.artistCommentary') + }), + + relations.entries, + ]), +}; diff --git a/src/content/dependencies/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js index ccaf1076..0343409c 100644 --- a/src/content/dependencies/generateContentHeading.js +++ b/src/content/dependencies/generateContentHeading.js @@ -1,19 +1,41 @@ export default { extraDependencies: ['html'], + contentDependencies: ['generateColorStyleVariables'], + + relations: (relation) => ({ + colorVariables: relation('generateColorStyleVariables'), + }), slots: { title: {type: 'html'}, + accent: {type: 'html'}, + + color: {validate: v => v.isColor}, + id: {type: 'string'}, tag: {type: 'string', default: 'p'}, }, - generate(slots, {html}) { + generate(relations, slots, {html}) { return html.tag(slots.tag, { class: 'content-heading', id: slots.id, tabindex: '0', - }, - slots.title); + + style: + slots.color && + relations.colorVariables + .slot('color', slots.color) + .content, + }, [ + html.tag('span', + {[html.onlyIfContent]: true, class: 'content-heading-main-title'}, + slots.title), + + html.tag('span', + {[html.onlyIfContent]: true, class: 'content-heading-accent'}, + slots.accent), + ]); } } diff --git a/src/content/dependencies/generateContributionList.js b/src/content/dependencies/generateContributionList.js index 731cfba5..6401e65e 100644 --- a/src/content/dependencies/generateContributionList.js +++ b/src/content/dependencies/generateContributionList.js @@ -16,5 +16,6 @@ export default { showIcons: true, showContribution: true, preventWrapping: false, + iconMode: 'tooltip', })))), }; diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index aeba97de..e43963fb 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -31,6 +31,10 @@ export default { type: 'string', }, + color: { + validate: v => v.isColor, + }, + mode: { validate: v => v.is('primary', 'thumbnail', 'commentary'), default: 'primary', @@ -45,6 +49,7 @@ export default { .slots({ path: slots.path, alt: slots.alt, + color: slots.color, thumb: 'medium', id: 'cover-art', reveal: true, @@ -67,6 +72,7 @@ export default { .slots({ path: slots.path, alt: slots.alt, + color: slots.color, thumb: 'small', reveal: false, link: false, @@ -78,6 +84,7 @@ export default { .slots({ path: slots.path, alt: slots.alt, + color: slots.color, thumb: 'medium', class: 'commentary-art', reveal: true, diff --git a/src/content/dependencies/generateDatetimestampTemplate.js b/src/content/dependencies/generateDatetimestampTemplate.js new file mode 100644 index 00000000..bfba647f --- /dev/null +++ b/src/content/dependencies/generateDatetimestampTemplate.js @@ -0,0 +1,28 @@ +export default { + extraDependencies: ['html'], + + slots: { + mainContent: {type: 'html'}, + tooltipContent: {type: 'html'}, + datetime: {type: 'string'}, + }, + + generate: (slots, {html}) => + html.tag('span', { + [html.joinChildren]: '', + + class: [ + 'datetimestamp', + slots.tooltipContent && 'has-tooltip', + ], + }, [ + html.tag('time', + {datetime: slots.datetime}, + slots.mainContent), + + slots.tooltipContent && + html.tag('span', {class: 'datetimestamp-tooltip'}, + html.tag('span', {class: 'datetimestamp-tooltip-content'}, + slots.tooltipContent)), + ]), +}; diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js index bd6063c9..29379644 100644 --- a/src/content/dependencies/generateFlashActSidebar.js +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -1,5 +1,6 @@ import find from '#find'; import {stitchArrays} from '#sugar'; +import {filterMultipleArrays} from '#wiki-data'; export default { contentDependencies: ['linkFlash', 'linkFlashAct', 'linkFlashIndex'], @@ -11,10 +12,12 @@ export default { query(sprawl, act, flash) { const findFlashAct = directory => - find.flashAct(directory, sprawl.flashActData, {mode: 'error'}); + find.flashAct(directory, sprawl.flashActData, {mode: 'quiet'}); + + const homestuckSide1 = findFlashAct('flash-act:a1'); const sideFirstActs = [ - findFlashAct('flash-act:a1'), + sprawl.flashActData[0], findFlashAct('flash-act:a6a1'), findFlashAct('flash-act:hiveswap'), findFlashAct('flash-act:cool-and-new-web-comic'), @@ -22,7 +25,9 @@ export default { ]; const sideNames = [ - `Side 1 (Acts 1-5)`, + (homestuckSide1 + ? `Side 1 (Acts 1-5)` + : `All flashes & games`), `Side 2 (Acts 6-7)`, `Additional Canon`, `Fan Adventures`, @@ -30,13 +35,18 @@ export default { ]; const sideColors = [ - '#4ac925', + (homestuckSide1 + ? '#4ac925' + : null), '#3796c6', '#f2a400', '#c466ff', '#32c7fe', ]; + filterMultipleArrays(sideFirstActs, sideNames, sideColors, + firstAct => firstAct); + const sideFirstActIndexes = sideFirstActs .map(act => sprawl.flashActData.indexOf(act)); @@ -127,7 +137,7 @@ export default { }), generate(data, relations, {getColors, html, language}) { - const currentActBox = html.tags([ + const currentActBoxContent = html.tags([ html.tag('h1', relations.currentActLink), html.tag('details', @@ -150,7 +160,7 @@ export default { ]), ]); - const sideMapBox = html.tags([ + const sideMapBoxContent = html.tags([ html.tag('h1', relations.flashIndexLink), stitchArrays({ @@ -178,17 +188,21 @@ export default { ])), ]); + const sideMapBox = { + class: 'flash-act-map-sidebar-box', + content: sideMapBoxContent, + }; + + const currentActBox = { + class: 'flash-current-act-sidebar-box', + content: currentActBoxContent, + }; + return { leftSidebarMultiple: (data.isFlashActPage - ? [ - {content: sideMapBox}, - {content: currentActBox}, - ] - : [ - {content: currentActBox}, - {content: sideMapBox}, - ]), + ? [sideMapBox, currentActBox] + : [currentActBox, sideMapBox]), }; }, }; diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js index ad1dab94..5fc62ab3 100644 --- a/src/content/dependencies/generateFlashIndexPage.js +++ b/src/content/dependencies/generateFlashIndexPage.js @@ -1,4 +1,4 @@ -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; export default { contentDependencies: [ @@ -95,23 +95,25 @@ export default { mainClasses: ['flash-index'], mainContent: [ - html.tag('p', - {class: 'quick-info'}, - language.$('misc.jumpTo')), - - html.tag('ul', - {class: 'quick-info'}, - stitchArrays({ - colorVariables: relations.jumpLinkColorVariables, - anchor: data.jumpLinkAnchors, - color: data.jumpLinkColors, - label: data.jumpLinkLabels, - }).map(({colorVariables, anchor, color, label}) => - html.tag('li', - html.tag('a', { - href: '#' + anchor, - style: colorVariables.slot('color', color).content, - }, label)))), + !empty(data.jumpLinkLabels) && [ + html.tag('p', + {class: 'quick-info'}, + language.$('misc.jumpTo')), + + html.tag('ul', + {class: 'quick-info'}, + stitchArrays({ + colorVariables: relations.jumpLinkColorVariables, + anchor: data.jumpLinkAnchors, + color: data.jumpLinkColors, + label: data.jumpLinkLabels, + }).map(({colorVariables, anchor, color, label}) => + html.tag('li', + html.tag('a', { + href: '#' + anchor, + style: colorVariables.slot('color', color).content, + }, label)))), + ], stitchArrays({ colorVariables: relations.actColorVariables, diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js index 09c6b37c..c60f9696 100644 --- a/src/content/dependencies/generateFlashInfoPage.js +++ b/src/content/dependencies/generateFlashInfoPage.js @@ -133,7 +133,7 @@ export default { links: language.formatDisjunctionList( relations.externalLinks - .map(link => link.slot('mode', 'flash'))), + .map(link => link.slot('context', 'flash'))), })), sec.featuredTracks && [ diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js index 5df83566..86e6c61a 100644 --- a/src/content/dependencies/generateFooterLocalizationLinks.js +++ b/src/content/dependencies/generateFooterLocalizationLinks.js @@ -1,3 +1,6 @@ +import {stitchArrays} from '#sugar'; +import {sortByName} from '#wiki-data'; + export default { extraDependencies: [ 'defaultLanguage', @@ -16,25 +19,37 @@ export default { pagePath, to, }) { - const links = Object.entries(languages) - .filter(([code, language]) => code !== 'default' && !language.hidden) - .map(([code, language]) => language) - .sort(({name: a}, {name: b}) => (a < b ? -1 : a > b ? 1 : 0)) - .map((language) => - html.tag('span', - html.tag('a', - { - href: - language === defaultLanguage - ? to( - 'localizedDefaultLanguage.' + pagePath[0], - ...pagePath.slice(1)) - : to( - 'localizedWithBaseDirectory.' + pagePath[0], - language.code, - ...pagePath.slice(1)), - }, - language.name))); + const switchableLanguages = + Object.entries(languages) + .filter(([code, language]) => code !== 'default' && !language.hidden) + .map(([code, language]) => language); + + if (switchableLanguages.length <= 1) { + return html.blank(); + } + + sortByName(switchableLanguages); + + const [pagePathSubkey, ...pagePathArgs] = pagePath; + + const linkPaths = + switchableLanguages.map(language => + (language === defaultLanguage + ? (['localizedDefaultLanguage.' + pagePathSubkey, + ...pagePathArgs]) + : (['localizedWithBaseDirectory.' + pagePathSubkey, + language.code, + ...pagePathArgs]))); + + const links = + stitchArrays({ + language: switchableLanguages, + linkPath: linkPaths, + }).map(({language, linkPath}) => + html.tag('span', + html.tag('a', + {href: to(...linkPath)}, + language.name))); return html.tag('div', {class: 'footer-localization-links'}, language.$('misc.uiLanguage', { diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js index 0583755e..0e5d645b 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -1,7 +1,9 @@ -import {empty} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; export default { contentDependencies: [ + 'generateAbsoluteDatetimestamp', + 'generateColorStyleVariables', 'generateContentHeading', 'generateGroupNavLinks', 'generateGroupSecondaryNav', @@ -62,18 +64,27 @@ export default { sec.albums.galleryLink = relation('linkGroupGallery', group); - sec.albums.entries = - group.albums.map(album => { - const links = {}; - links.albumLink = relation('linkAlbum', album); + sec.albums.colorVariables = + group.albums + .map(() => relation('generateColorStyleVariables')); - const otherGroup = album.groups.find(g => g !== group); - if (otherGroup) { - links.groupLink = relation('linkGroup', otherGroup); - } + sec.albums.albumLinks = + group.albums + .map(album => relation('linkAlbum', album)); - return links; - }); + sec.albums.groupLinks = + group.albums + .map(album => album.groups.find(g => g !== group)) + .map(group => + (group + ? relation('linkGroup', group) + : null)); + + sec.albums.datetimestamps = + group.albums.map(album => + (album.date + ? relation('generateAbsoluteDatetimestamp', album.date) + : null)); } return relations; @@ -85,11 +96,8 @@ export default { data.name = group.name; data.color = group.color; - if (!empty(group.albums)) { - data.albumYears = - group.albums - .map(album => album.date?.getFullYear()); - } + data.albumColors = + group.albums.map(album => album.color); return data; }, @@ -107,7 +115,10 @@ export default { sec.info.visitLinks && html.tag('p', language.$('releaseInfo.visitOn', { - links: language.formatDisjunctionList(sec.info.visitLinks), + links: + language.formatDisjunctionList( + sec.info.visitLinks + .map(link => link.slot('context', 'group'))), })), html.tag('blockquote', @@ -130,34 +141,50 @@ export default { })), html.tag('ul', - sec.albums.entries.map(({albumLink, groupLink}, index) => { - // All these strings are really jank, and should probably - // be implemented with the same 'const parts = [], opts = {}' - // form used elsewhere... - const year = data.albumYears[index]; - const item = - (year - ? language.$('groupInfoPage.albumList.item', { - year, - album: albumLink, - }) - : language.$('groupInfoPage.albumList.item.withoutYear', { - album: albumLink, - })); - - return html.tag('li', - (groupLink - ? language.$('groupInfoPage.albumList.item.withAccent', { - item, - accent: - html.tag('span', {class: 'other-group-accent'}, - language.$('groupInfoPage.albumList.item.otherGroupAccent', { - group: - groupLink.slot('color', false), - })), - }) - : item)); - })), + stitchArrays({ + albumLink: sec.albums.albumLinks, + groupLink: sec.albums.groupLinks, + datetimestamp: sec.albums.datetimestamps, + colorVariables: sec.albums.colorVariables, + albumColor: data.albumColors, + }).map(({ + albumLink, + groupLink, + datetimestamp, + colorVariables, + albumColor, + }) => { + const prefix = 'groupInfoPage.albumList.item'; + const parts = [prefix]; + const options = {}; + + options.album = + albumLink.slot('color', false); + + if (datetimestamp) { + parts.push('withYear'); + options.yearAccent = + language.$(prefix, 'yearAccent', { + year: + datetimestamp.slots({style: 'year', tooltip: true}), + }); + } + + if (groupLink) { + parts.push('withOtherGroup'); + options.otherGroupAccent = + html.tag('span', {class: 'other-group-accent'}, + language.$(prefix, 'otherGroupAccent', { + group: + groupLink.slot('color', false), + })); + } + + return ( + html.tag('li', + {style: colorVariables.slot('color', albumColor).content}, + language.$(...parts, options))); + })), ], ], diff --git a/src/content/dependencies/generateGroupSidebar.js b/src/content/dependencies/generateGroupSidebar.js index 6baf37f4..98b288fa 100644 --- a/src/content/dependencies/generateGroupSidebar.js +++ b/src/content/dependencies/generateGroupSidebar.js @@ -22,6 +22,7 @@ export default { generate(relations, slots, {html, language}) { return { + leftSidebarClass: 'category-map-sidebar-box', leftSidebarContent: [ html.tag('h1', language.$('groupSidebar.title')), diff --git a/src/content/dependencies/generateListRandomPageLinksAlbumLink.js b/src/content/dependencies/generateListRandomPageLinksAlbumLink.js new file mode 100644 index 00000000..b3560aca --- /dev/null +++ b/src/content/dependencies/generateListRandomPageLinksAlbumLink.js @@ -0,0 +1,18 @@ +export default { + contentDependencies: ['linkAlbum'], + + data: (album) => + ({directory: album.directory}), + + relations: (relation, album) => + ({albumLink: relation('linkAlbum', album)}), + + generate: (data, relations) => + relations.albumLink.slots({ + anchor: true, + attributes: { + 'data-random': 'track-in-album', + 'style': `--album-directory: ${data.directory}`, + }, + }), +}; diff --git a/src/content/dependencies/generateListRandomPageLinksGroupSection.js b/src/content/dependencies/generateListRandomPageLinksGroupSection.js deleted file mode 100644 index 2a684b19..00000000 --- a/src/content/dependencies/generateListRandomPageLinksGroupSection.js +++ /dev/null @@ -1,81 +0,0 @@ -import {stitchArrays} from '#sugar'; -import {sortChronologically} from '#wiki-data'; - -export default { - contentDependencies: ['generateColorStyleVariables', 'linkGroup'], - extraDependencies: ['html', 'language', 'wikiData'], - - sprawl: ({albumData}) => ({albumData}), - - query: (sprawl, group) => ({ - albums: - sortChronologically(sprawl.albumData.slice()) - .filter(album => album.groups.includes(group)) - .filter(album => album.tracks.length > 1), - }), - - relations: (relation, query, sprawl, group) => ({ - groupLink: - relation('linkGroup', group), - - albumColorVariables: - query.albums - .map(() => relation('generateColorStyleVariables')), - }), - - data: (query, sprawl, group) => ({ - groupDirectory: - group.directory, - - albumColors: - query.albums - .map(album => album.color), - - albumDirectories: - query.albums - .map(album => album.directory), - - albumNames: - query.albums - .map(album => album.name), - }), - - generate: (data, relations, {html, language}) => - html.tags([ - html.tag('dt', - language.$('listingPage.other.randomPages.group', { - group: relations.groupLink, - - randomAlbum: - html.tag('a', - {href: '#', 'data-random': 'album-in-' + data.groupDirectory}, - language.$('listingPage.other.randomPages.group.randomAlbum')), - - randomTrack: - html.tag('a', - {href: '#', 'data-random': 'track-in-' + data.groupDirectory}, - language.$('listingPage.other.randomPages.group.randomTrack')), - })), - - html.tag('dd', - html.tag('ul', - stitchArrays({ - colorVariables: relations.albumColorVariables, - color: data.albumColors, - directory: data.albumDirectories, - name: data.albumNames, - }).map(({colorVariables, color, directory, name}) => - html.tag('li', - language.$('listingPage.other.randomPages.album', { - album: - html.tag('a', { - href: '#', - 'data-random': 'track-in-album', - style: - colorVariables.slot('color', color).content + - '; ' + - `--album-directory: ${directory}`, - }, name), - }))))), - ]), -}; diff --git a/src/content/dependencies/generateListingPage.js b/src/content/dependencies/generateListingPage.js index 08eb40c6..2050d62d 100644 --- a/src/content/dependencies/generateListingPage.js +++ b/src/content/dependencies/generateListingPage.js @@ -1,4 +1,4 @@ -import {empty, stitchArrays} from '#sugar'; +import {bindOpts, empty, stitchArrays} from '#sugar'; export default { contentDependencies: [ @@ -7,6 +7,7 @@ export default { 'generatePageLayout', 'linkListing', 'linkListingIndex', + 'linkTemplate', ], extraDependencies: ['html', 'language', 'wikiData'], @@ -26,6 +27,9 @@ export default { relations.chunkHeading = relation('generateContentHeading'); + relations.showSkipToSectionLinkTemplate = + relation('linkTemplate'); + if (listing.target.listings.length > 1) { relations.sameTargetListingLinks = listing.target.listings @@ -58,12 +62,42 @@ export default { }, slots: { - type: {validate: v => v.is('rows', 'chunks', 'custom')}, + type: { + validate: v => v.is('rows', 'chunks', 'custom'), + }, + + rows: { + validate: v => v.strictArrayOf(v.isObject), + }, - rows: {validate: v => v.strictArrayOf(v.isObject)}, + rowAttributes: { + validate: v => v.strictArrayOf(v.optional(v.isObject)) + }, + + chunkTitles: { + validate: v => v.strictArrayOf(v.isObject), + }, - chunkTitles: {validate: v => v.strictArrayOf(v.isObject)}, - chunkRows: {validate: v => v.strictArrayOf(v.isObject)}, + chunkTitleAccents: { + validate: v => v.strictArrayOf(v.optional(v.isObject)), + }, + + chunkRows: { + validate: v => v.strictArrayOf(v.isObject), + }, + + chunkRowAttributes: { + validate: v => v.strictArrayOf(v.optional(v.isObject)), + }, + + showSkipToSection: { + type: 'boolean', + default: false, + }, + + chunkIDs: { + validate: v => v.strictArrayOf(v.optional(v.isString)), + }, listStyle: { validate: v => v.is('ordered', 'unordered'), @@ -74,26 +108,59 @@ export default { }, generate(data, relations, slots, {html, language}) { - const listTag = - (slots.listStyle === 'ordered' - ? 'ol' - : 'ul'); + function formatListingString({ + context, + provided = {}, + }) { + const parts = ['listingPage', data.stringsKey]; + + if (Array.isArray(context)) { + parts.push(...context); + } else { + parts.push(context); + } - const formatListingString = (contextStringsKey, options = {}) => { - const baseStringsKey = `listingPage.${data.stringsKey}`; + if (provided.stringsKey) { + parts.push(provided.stringsKey); + } - const parts = [baseStringsKey, contextStringsKey]; + const options = {...provided}; + delete options.stringsKey; - if (options.stringsKey) { - parts.push(options.stringsKey); - delete options.stringsKey; - } + return language.formatString(...parts, options); + } - return language.formatString(parts.join('.'), options); - }; + const formatRow = ({context, row, attributes}) => + (attributes?.href + ? html.tag('li', + html.tag('a', + attributes, + formatListingString({ + context, + provided: row, + }))) + : html.tag('li', + attributes, + formatListingString({ + context, + provided: row, + }))); + + const formatRowList = ({context, rows, rowAttributes}) => + html.tag( + (slots.listStyle === 'ordered' ? 'ol' : 'ul'), + stitchArrays({ + row: rows, + attributes: rowAttributes ?? rows.map(() => null), + }).map( + bindOpts(formatRow, { + [bindOpts.bindIndex]: 0, + context, + }))); return relations.layout.slots({ - title: formatListingString('title'), + title: formatListingString({context: 'title'}), + headingMode: 'sticky', mainContent: [ @@ -121,35 +188,78 @@ export default { listings: language.formatUnitList(relations.seeAlsoLinks), })), + slots.content, + slots.type === 'rows' && - html.tag(listTag, - slots.rows.map(row => - html.tag('li', - formatListingString('item', row)))), + formatRowList({ + context: 'item', + rows: slots.rows, + rowAttributes: slots.rowAttributes, + }), slots.type === 'chunks' && - html.tag('dl', + html.tag('dl', [ + slots.showSkipToSection && [ + html.tag('dt', + language.$('listingPage.skipToSection')), + + html.tag('dd', + html.tag('ul', + stitchArrays({ + title: slots.chunkTitles, + id: slots.chunkIDs, + }).filter(({id}) => id) + .map(({title, id}) => + html.tag('li', + relations.showSkipToSectionLinkTemplate + .clone() + .slots({ + hash: id, + content: + html.normalize( + formatListingString({ + context: 'chunk.title', + provided: title, + }).toString() + .replace(/:$/, '')), + }))))), + ], + stitchArrays({ title: slots.chunkTitles, + titleAccent: slots.chunkTitleAccents, + id: slots.chunkIDs, rows: slots.chunkRows, - }).map(({title, rows}) => [ + rowAttributes: slots.chunkRowAttributes, + }).map(({title, titleAccent, id, rows, rowAttributes}) => [ relations.chunkHeading .clone() .slots({ tag: 'dt', - title: formatListingString('chunk.title', title), + id, + + title: + formatListingString({ + context: 'chunk.title', + provided: title, + }), + + accent: + titleAccent && + formatListingString({ + context: ['chunk.title', title.stringsKey, 'accent'], + provided: titleAccent, + }), }), html.tag('dd', - html.tag(listTag, - rows.map(row => - html.tag('li', - {class: row.stringsKey === 'rerelease' && 'rerelease'}, - formatListingString('chunk.item', row))))), - ])), - - slots.type === 'custom' && - slots.content, + formatRowList({ + context: 'chunk.item', + rows, + rowAttributes, + })), + ]), + ]), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateListingSidebar.js b/src/content/dependencies/generateListingSidebar.js index fe2a08fa..1cdd236b 100644 --- a/src/content/dependencies/generateListingSidebar.js +++ b/src/content/dependencies/generateListingSidebar.js @@ -11,6 +11,7 @@ export default { generate(relations, {html}) { return { + leftSidebarClass: 'listing-map-sidebar-box', leftSidebarContent: [ html.tag('h1', relations.listingIndexLink), relations.listingIndexList.slot('mode', 'sidebar'), diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 72dfbae5..1591223a 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -6,12 +6,19 @@ function sidebarSlots(side) { // if specified. [side + 'Content']: {type: 'html'}, - // Multiple is an array of {content: (HTML)} objects. Each of these - // will generate one sidebar section. + // A single class to apply to the whole sidebar. If specifying multiple + // sections, this be added to the containing sidebar-column - specify a + // class on each section if that's more suitable. + [side + 'Class']: {type: 'string'}, + + // Multiple is an array of objects, each specifying content (HTML) and + // optionally class (a string). Each of these will generate one sidebar + // section. [side + 'Multiple']: { validate: v => v.sparseArrayOf( v.validateProperties({ + class: v.optional(v.isString), content: v.isHTML, })), }, @@ -27,6 +34,7 @@ function sidebarSlots(side) { // the whole section's containing box (or the sidebar column as a whole). [side + 'StickyMode']: { validate: v => v.is('last', 'column', 'static'), + default: 'static', }, // Collapsing sidebars disappear when the viewport is sufficiently @@ -85,8 +93,10 @@ export default { relations.stickyHeadingContainer = relation('generateStickyHeadingContainer'); - relations.defaultFooterContent = - relation('transformContent', sprawl.footerContent); + if (sprawl.footerContent) { + relations.defaultFooterContent = + relation('transformContent', sprawl.footerContent); + } relations.colorStyleRules = relation('generateColorStyleRules'); @@ -98,6 +108,8 @@ export default { title: {type: 'html'}, showWikiNameInTitle: {type: 'boolean', default: true}, + additionalNames: {type: 'html'}, + cover: {type: 'html'}, socialEmbed: {type: 'html'}, @@ -212,26 +224,29 @@ export default { const colors = getColors(slots.color ?? data.wikiColor); const hasSocialEmbed = !html.isBlank(slots.socialEmbed); - let titleHTML = null; - - if (!html.isBlank(slots.title)) { - switch (slots.headingMode) { - case 'sticky': - titleHTML = - relations.stickyHeadingContainer.slots({ - title: slots.title, - cover: slots.cover, - }); - break; - case 'static': - titleHTML = html.tag('h1', slots.title); - break; - } - } + const titleContentsHTML = + (html.isBlank(slots.title) + ? null + : html.isBlank(slots.additionalNames) + ? language.sanitize(slots.title) + : html.tag('a', { + href: '#additional-names-box', + title: language.$('misc.additionalNames.tooltip').toString(), + }, language.sanitize(slots.title))); + + const titleHTML = + (html.isBlank(slots.title) + ? null + : slots.headingMode === 'sticky' + ? relations.stickyHeadingContainer.slots({ + title: titleContentsHTML, + cover: slots.cover, + }) + : html.tag('h1', titleContentsHTML)); let footerContent = slots.footerContent; - if (html.isBlank(footerContent)) { + if (html.isBlank(footerContent) && relations.defaultFooterContent) { footerContent = relations.defaultFooterContent .slot('mode', 'multiline'); } @@ -244,6 +259,7 @@ export default { titleHTML, slots.cover, + slots.additionalNames, html.tag('div', { @@ -352,6 +368,7 @@ export default { const generateSidebarHTML = (side, id) => { const content = slots[side + 'Content']; + const topClass = slots[side + 'Class']; const multiple = slots[side + 'Multiple']; const stickyMode = slots[side + 'StickyMode']; const wide = slots[side + 'Wide']; @@ -361,20 +378,18 @@ export default { let sidebarContent = html.blank(); if (!html.isBlank(content)) { - sidebarClasses = ['sidebar']; + sidebarClasses = ['sidebar', topClass]; sidebarContent = content; } else if (multiple) { - sidebarClasses = ['sidebar-multiple']; + sidebarClasses = ['sidebar-multiple', topClass]; sidebarContent = multiple .filter(Boolean) - .map(({content}) => - html.tag('div', - { - [html.onlyIfContent]: true, - class: 'sidebar', - }, - content)); + .map(box => + html.tag('div', { + [html.onlyIfContent]: true, + class: ['sidebar', box.class], + }, box.content)); } if (html.isBlank(sidebarContent)) { @@ -609,7 +624,7 @@ export default { html.tag('link', { rel: 'stylesheet', - href: to('shared.staticFile', 'site5.css', cachebust), + href: to('shared.staticFile', 'site6.css', cachebust), }), html.tag('style', [ @@ -646,7 +661,7 @@ export default { html.tag('script', { type: 'module', - src: to('shared.staticFile', 'client2.js', cachebust), + src: to('shared.staticFile', 'client3.js', cachebust), }), ]), ]) diff --git a/src/content/dependencies/generateRelativeDatetimestamp.js b/src/content/dependencies/generateRelativeDatetimestamp.js new file mode 100644 index 00000000..bbe33188 --- /dev/null +++ b/src/content/dependencies/generateRelativeDatetimestamp.js @@ -0,0 +1,58 @@ +export default { + contentDependencies: [ + 'generateAbsoluteDatetimestamp', + 'generateDatetimestampTemplate', + ], + + extraDependencies: ['html', 'language'], + + data: (currentDate, referenceDate) => + (currentDate.getTime() === referenceDate.getTime() + ? {equal: true, date: currentDate} + : {equal: false, currentDate, referenceDate}), + + relations: (relation, currentDate) => + ({template: relation('generateDatetimestampTemplate'), + fallback: relation('generateAbsoluteDatetimestamp', currentDate)}), + + slots: { + style: { + validate: v => v.is('full', 'year'), + default: 'full', + }, + + tooltip: { + type: 'boolean', + default: false, + }, + }, + + generate(data, relations, slots, {language}) { + if (data.comparison === 'equal') { + return relations.fallback.slots({ + style: slots.style, + tooltip: slots.tooltip, + }); + } + + return relations.template.slots({ + mainContent: + (slots.style === 'full' + ? language.formatDate(data.currentDate) + : slots.style === 'year' + ? data.currentDate.getFullYear().toString() + : null), + + tooltipContent: + slots.tooltip && + 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 1fa8dcca..2e6c4709 100644 --- a/src/content/dependencies/generateReleaseInfoContributionsLine.js +++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js @@ -35,6 +35,7 @@ export default { link.slots({ showContribution: slots.showContribution, showIcons: slots.showIcons, + iconMode: 'tooltip', }))), }); }, diff --git a/src/content/dependencies/generateStaticPage.js b/src/content/dependencies/generateStaticPage.js index 3e27fd43..226152c7 100644 --- a/src/content/dependencies/generateStaticPage.js +++ b/src/content/dependencies/generateStaticPage.js @@ -1,5 +1,6 @@ export default { contentDependencies: ['generatePageLayout', 'transformContent'], + extraDependencies: ['html'], relations(relation, staticPage) { return { @@ -12,10 +13,11 @@ export default { return { name: staticPage.name, stylesheet: staticPage.stylesheet, + script: staticPage.script, }; }, - generate(data, relations) { + generate(data, relations, {html}) { return relations.layout .slots({ title: data.name, @@ -27,7 +29,12 @@ export default { : []), mainClasses: ['long-content'], - mainContent: relations.content, + mainContent: [ + relations.content, + + data.script && + html.tag('script', data.script), + ], navLinkStyle: 'hierarchical', navLinks: [ diff --git a/src/content/dependencies/generateTrackAdditionalNamesBox.js b/src/content/dependencies/generateTrackAdditionalNamesBox.js new file mode 100644 index 00000000..bad04b74 --- /dev/null +++ b/src/content/dependencies/generateTrackAdditionalNamesBox.js @@ -0,0 +1,53 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: ['generateAdditionalNamesBox'], + extraDependencies: ['html'], + + query: (track) => { + const { + additionalNames: own, + sharedAdditionalNames: shared, + inferredAdditionalNames: inferred, + } = track; + + if (empty(own) && empty(shared) && empty(inferred)) { + return {combinedList: []}; + } + + const firstFilter = + (empty(own) + ? new Set() + : new Set(own.map(({name}) => name))); + + const sharedFiltered = + shared.filter(({name}) => !firstFilter.has(name)) + + const secondFilter = + new Set([ + ...firstFilter, + ...sharedFiltered.map(({name}) => name), + ]); + + const inferredFiltered = + inferred.filter(({name}) => !secondFilter.has(name)); + + return { + combinedList: [ + ...own, + ...sharedFiltered, + ...inferredFiltered, + ], + }; + }, + + relations: (relation, query) => ({ + box: + (empty(query.combinedList) + ? null + : relation('generateAdditionalNamesBox', query.combinedList)), + }), + + generate: (relations, {html}) => + relations.box ?? html.blank(), +}; diff --git a/src/content/dependencies/generateTrackCoverArtwork.js b/src/content/dependencies/generateTrackCoverArtwork.js index ec0488e2..6c056c9a 100644 --- a/src/content/dependencies/generateTrackCoverArtwork.js +++ b/src/content/dependencies/generateTrackCoverArtwork.js @@ -1,20 +1,28 @@ export default { contentDependencies: ['generateCoverArtwork'], - relations: (relation, track) => - ({coverArtwork: - relation('generateCoverArtwork', - (track.hasUniqueCoverArt - ? track.artTags - : track.album.artTags))}), - - data: (track) => - ({path: + relations: (relation, track) => ({ + coverArtwork: + relation('generateCoverArtwork', (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : ['media.albumCover', track.album.directory, track.album.coverArtFileExtension])}), + ? track.artTags + : track.album.artTags)), + }), + + data: (track) => ({ + path: + (track.hasUniqueCoverArt + ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] + : ['media.albumCover', track.album.directory, track.album.coverArtFileExtension]), + + color: + track.color, + }), generate: (data, relations) => - relations.coverArtwork.slot('path', data.path), + relations.coverArtwork.slots({ + path: data.path, + color: data.color, + }), }; diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 93334948..041f6bbc 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -1,19 +1,24 @@ -import {empty} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; import {sortAlbumsTracksChronologically, sortFlashesChronologically} from '#wiki-data'; import getChronologyRelations from '../util/getChronologyRelations.js'; export default { contentDependencies: [ + 'generateAbsoluteDatetimestamp', 'generateAdditionalFilesShortcut', 'generateAlbumAdditionalFilesList', 'generateAlbumNavAccent', 'generateAlbumSidebar', 'generateAlbumStyleRules', 'generateChronologyLinks', + 'generateColorStyleVariables', + 'generateCommentarySection', 'generateContentHeading', 'generateContributionList', 'generatePageLayout', + 'generateRelativeDatetimestamp', + 'generateTrackAdditionalNamesBox', 'generateTrackCoverArtwork', 'generateTrackList', 'generateTrackListDividedByGroups', @@ -106,6 +111,10 @@ export default { list: relation('generateAlbumAdditionalFilesList', album, additionalFiles), }); + // This'll take care of itself being blank if there's nothing to show here. + relations.additionalNamesBox = + relation('generateTrackAdditionalNamesBox', track); + if (track.hasUniqueCoverArt || album.hasCoverArt) { relations.cover = relation('generateTrackCoverArtwork', track); @@ -133,6 +142,29 @@ export default { otherReleases.heading = relation('generateContentHeading'); + otherReleases.colorVariables = + track.otherReleases + .map(() => relation('generateColorStyleVariables')); + + otherReleases.trackLinks = + track.otherReleases + .map(track => relation('linkTrack', track)); + + otherReleases.albumLinks = + track.otherReleases + .map(track => relation('linkAlbum', track.album)); + + otherReleases.datetimestamps = + track.otherReleases.map(track2 => + (track2.date + ? (track.date + ? relation('generateRelativeDatetimestamp', + track2.date, + track.date) + : relation('generateAbsoluteDatetimestamp', + track2.date)) + : null)); + otherReleases.items = track.otherReleases.map(track => ({ trackLink: relation('linkTrack', track), @@ -268,13 +300,8 @@ export default { // Section: Artist commentary if (track.commentary) { - const artistCommentary = sections.artistCommentary = {}; - - artistCommentary.heading = - relation('generateContentHeading'); - - artistCommentary.content = - relation('transformContent', track.commentary); + sections.artistCommentary = + relation('generateCommentarySection', track.commentary); } return relations; @@ -288,6 +315,9 @@ export default { hasTrackNumbers: track.album.hasTrackNumbers, trackNumber: track.album.tracks.indexOf(track) + 1, + otherReleaseColors: + track.otherReleases.map(track => track.color), + numAdditionalFiles: track.additionalFiles.length, }; }, @@ -300,6 +330,8 @@ export default { title: language.$('trackPage.title', {track: data.name}), headingMode: 'sticky', + additionalNames: relations.additionalNamesBox, + color: data.color, styleRules: [relations.albumStyleRules], @@ -352,12 +384,39 @@ export default { }), html.tag('ul', - sec.otherReleases.items.map(({trackLink, albumLink}) => - html.tag('li', - language.$('releaseInfo.alsoReleasedAs.item', { - track: trackLink, - album: albumLink, - })))), + stitchArrays({ + trackLink: sec.otherReleases.trackLinks, + albumLink: sec.otherReleases.albumLinks, + datetimestamp: sec.otherReleases.datetimestamps, + colorVariables: sec.otherReleases.colorVariables, + color: data.otherReleaseColors, + }).map(({ + trackLink, + albumLink, + datetimestamp, + colorVariables, + color, + }) => { + const parts = ['releaseInfo.alsoReleasedAs.item']; + const options = {}; + + options.track = trackLink.slot('color', false); + options.album = albumLink; + + if (datetimestamp) { + parts.push('withYear'); + options.year = + datetimestamp.slots({ + style: 'year', + tooltip: true, + }); + } + + return ( + html.tag('li', + {style: colorVariables.slot('color', color).content}, + language.$(...parts, options))); + })), ], sec.contributors && [ @@ -491,17 +550,7 @@ export default { sec.additionalFiles.list, ], - sec.artistCommentary && [ - sec.artistCommentary.heading - .slots({ - id: 'artist-commentary', - title: language.$('releaseInfo.artistCommentary') - }), - - html.tag('blockquote', - sec.artistCommentary.content - .slot('mode', 'multiline')), - ], + sec.artistCommentary, ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js index 9a7478ca..c347dbce 100644 --- a/src/content/dependencies/generateTrackReleaseInfo.js +++ b/src/content/dependencies/generateTrackReleaseInfo.js @@ -77,7 +77,10 @@ export default { html.tag('p', (relations.externalLinks ? language.$('releaseInfo.listenOn', { - links: language.formatDisjunctionList(relations.externalLinks), + links: + language.formatDisjunctionList( + relations.externalLinks + .map(link => link.slot('context', 'track'))), }) : language.$('releaseInfo.listenOn.noLinks', { name: html.tag('i', data.name), diff --git a/src/content/dependencies/generateWikiHomeAlbumsRow.js b/src/content/dependencies/generateWikiHomeAlbumsRow.js index cb0860f5..a19f104c 100644 --- a/src/content/dependencies/generateWikiHomeAlbumsRow.js +++ b/src/content/dependencies/generateWikiHomeAlbumsRow.js @@ -11,7 +11,7 @@ export default { 'transformContent', ], - extraDependencies: ['wikiData'], + extraDependencies: ['language', 'wikiData'], sprawl({albumData}, row) { const sprawl = {}; @@ -90,12 +90,14 @@ export default { data.paths = sprawl.albums .map(album => - ['media.albumCover', album.directory, album.coverArtFileExtension]); + (album.hasCoverArt + ? ['media.albumCover', album.directory, album.coverArtFileExtension] + : null)); return data; }, - generate(data, relations) { + generate(data, relations, {language}) { // Grids and carousels share some slots! Very convenient. const commonSlots = {}; @@ -106,8 +108,16 @@ export default { stitchArrays({ image: relations.images, path: data.paths, - }).map(({image, path}) => - image.slot('path', path)); + name: data.names ?? data.paths.slice().fill(null), + }).map(({image, path, name}) => + image.slots({ + path, + missingSourceContent: + name && + language.$('misc.albumGrid.noCoverArt', { + album: name, + }), + })); commonSlots.actionLinks = (relations.actionLinks diff --git a/src/content/dependencies/generateWikiHomeNewsBox.js b/src/content/dependencies/generateWikiHomeNewsBox.js index 8acd426c..0d8303f1 100644 --- a/src/content/dependencies/generateWikiHomeNewsBox.js +++ b/src/content/dependencies/generateWikiHomeNewsBox.js @@ -42,6 +42,7 @@ export default { } return { + class: 'latest-news-sidebar-box', content: [ html.tag('h1', language.$('homepage.news.title')), diff --git a/src/content/dependencies/generateWikiHomePage.js b/src/content/dependencies/generateWikiHomePage.js index 40a6b1c5..36fcc6f2 100644 --- a/src/content/dependencies/generateWikiHomePage.js +++ b/src/content/dependencies/generateWikiHomePage.js @@ -75,6 +75,7 @@ export default { leftSidebarMultiple: [ (relations.customSidebarContent ? { + class: 'custom-content-sidebar-box', content: relations.customSidebarContent .slot('mode', 'multiline'), diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 8aa9753b..3c78abe3 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -14,6 +14,12 @@ export default { 'to', ], + contentDependencies: ['generateColorStyleVariables'], + + relations: (relation) => ({ + colorVariables: relation('generateColorStyleVariables'), + }), + data(artTags) { const data = {}; @@ -43,6 +49,10 @@ export default { default: false, }, + color: { + validate: v => v.isColor, + }, + reveal: {type: 'boolean', default: true}, lazy: {type: 'boolean', default: false}, square: {type: 'boolean', default: false}, @@ -56,7 +66,7 @@ export default { missingSourceContent: {type: 'html'}, }, - generate(data, slots, { + generate(data, relations, slots, { checkIfImagePathHasCachedThumbnails, getDimensionsOfImagePath, getSizeOfImagePath, @@ -110,6 +120,12 @@ export default { !isMissingImageFile && !empty(data.contentWarnings); + const colorStyle = + slots.color && + relations.colorVariables + .slot('color', slots.color) + .content; + const willSquare = slots.square; const idOnImg = willLink ? null : slots.id; @@ -118,6 +134,9 @@ export default { const classOnImg = willLink ? null : slots.class; const classOnLink = willLink ? slots.class : null; + const styleOnContainer = willLink ? null : colorStyle; + const styleOnLink = willLink ? colorStyle : null; + if (!originalSrc || isMissingImageFile) { return prepare( html.tag('div', {class: 'image-text-area'}, @@ -191,7 +210,7 @@ export default { imgAttributes['data-no-image-preview'] = true; } - // These attributes are only relevant when a thumbnail are available *and* + // These attributes are only relevant when a thumbnail is available *and* // being used. if (hasThumbnails && slots.thumb) { if (fileSize) { @@ -238,9 +257,13 @@ export default { let wrapped = content; wrapped = - html.tag('div', {class: ['image-container', !originalSrc && 'placeholder-image']}, + html.tag('div', { + class: ['image-container', !originalSrc && 'placeholder-image'], + style: styleOnContainer, + }, [ html.tag('div', {class: 'image-inner-area'}, - wrapped)); + wrapped), + ]); if (willReveal) { wrapped = @@ -270,6 +293,7 @@ export default { wrapped = html.tag('a', { id: idOnLink, + class: [ 'box', 'image-link', @@ -277,6 +301,8 @@ export default { classOnLink, ], + style: styleOnLink, + href: (typeof slots.link === 'string' ? slots.link diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js index 8e42f247..790afa4f 100644 --- a/src/content/dependencies/linkContribution.js +++ b/src/content/dependencies/linkContribution.js @@ -1,15 +1,8 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'linkArtist', - 'linkExternalAsIcon', - ], - - extraDependencies: [ - 'html', - 'language', - ], + contentDependencies: ['linkArtist', 'linkExternalAsIcon'], + extraDependencies: ['html', 'language'], relations(relation, contribution) { const relations = {}; @@ -20,7 +13,6 @@ export default { if (!empty(contribution.who.urls)) { relations.artistIcons = contribution.who.urls - .slice(0, 4) .map(url => relation('linkExternalAsIcon', url)); } @@ -37,37 +29,81 @@ export default { showContribution: {type: 'boolean', default: false}, showIcons: {type: 'boolean', default: false}, preventWrapping: {type: 'boolean', default: true}, + + iconMode: { + validate: v => v.is('inline', 'tooltip'), + default: 'inline' + }, }, generate(data, relations, slots, {html, language}) { - const hasContributionPart = !!(slots.showContribution && data.what); - const hasExternalPart = !!(slots.showIcons && relations.artistIcons); - - const externalLinks = hasExternalPart && - html.tag('span', - {[html.noEdgeWhitespace]: true, class: 'icons'}, - language.formatUnitList(relations.artistIcons)); + const hasContribution = !!(slots.showContribution && data.what); + const hasExternalIcons = !!(slots.showIcons && relations.artistIcons); const parts = ['misc.artistLink']; const options = {artist: relations.artistLink}; - if (hasContributionPart) { + if (hasContribution) { parts.push('withContribution'); options.contrib = data.what; } - if (hasExternalPart) { + if (hasExternalIcons && slots.iconMode === 'inline') { parts.push('withExternalLinks'); - options.links = externalLinks; + options.links = + html.tag('span', + { + [html.noEdgeWhitespace]: true, + class: ['icons', 'icons-inline'], + }, + language.formatUnitList( + relations.artistIcons + .slice(0, 4) + .map(icon => icon.slot('context', 'artist')))); } - const content = language.formatString(parts.join('.'), options); + let content = language.formatString(parts.join('.'), options); - return ( - (parts.length > 1 && slots.preventWrapping - ? html.tag('span', - {[html.noEdgeWhitespace]: true, class: 'nowrap'}, - content) - : content)); - }, + if (hasExternalIcons && slots.iconMode === 'tooltip') { + content = [ + content, + html.tag('span', + { + [html.noEdgeWhitespace]: true, + class: ['icons', 'icons-tooltip'], + inert: true, + }, + html.tag('span', + { + [html.noEdgeWhitespace]: true, + [html.joinChildren]: '', + class: 'icons-tooltip-content', + }, + relations.artistIcons + .map(icon => icon.slots({context: 'artist', withText: true})))), + ]; + } + + if (hasContribution || hasExternalIcons) { + content = + html.tag('span', { + [html.noEdgeWhitespace]: true, + [html.joinChildren]: '', + + class: [ + 'contribution', + + hasExternalIcons && + slots.iconMode === 'tooltip' && + 'has-tooltip', + + parts.length > 1 && + slots.preventWrapping && + 'nowrap', + ], + }, content); + } + + return content; + } }; diff --git a/src/content/dependencies/linkExternal.js b/src/content/dependencies/linkExternal.js index 5de612e2..70e1ccff 100644 --- a/src/content/dependencies/linkExternal.js +++ b/src/content/dependencies/linkExternal.js @@ -1,140 +1,42 @@ -// TODO: Define these as extra dependencies and pass them somewhere -const BANDCAMP_DOMAINS = ['bc.s3m.us', 'music.solatrux.com']; -const MASTODON_DOMAINS = ['types.pl']; +import {isExternalLinkContext, isExternalLinkStyle} from '#external-links'; export default { extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({wikiInfo}), - - data(sprawl, url) { - const data = {url}; - - const {canonicalBase} = sprawl.wikiInfo; - if (canonicalBase) { - const {hostname: canonicalDomain} = new URL(canonicalBase); - Object.assign(data, {canonicalDomain}); - } - - return data; - }, + data: (url) => ({url}), slots: { - mode: { - validate: v => v.is('generic', 'album', 'flash'), - default: 'generic', + style: { + // This awkward syntax is because the slot descriptor validator can't + // differentiate between a function that returns a validator (the usual + // syntax) and a function that is itself a validator. + validate: () => isExternalLinkStyle, + default: 'normal', }, - }, - generate(data, slots, {html, language}) { - let isLocal; - let domain; - let pathname; - - try { - const url = new URL(data.url); - domain = url.hostname; - pathname = url.pathname; - } catch (error) { - // No support for relative local URLs yet, sorry! (I.e, local URLs must - // be absolute relative to the domain name in order to work.) - isLocal = true; - domain = null; - pathname = null; - } + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, - // isLocal also applies for URLs which match the 'Canonical Base' under - // wiki-info.yaml, if present. - if (data.canonicalDomain && domain === data.canonicalDomain) { - isLocal = true; - } + tab: { + validate: v => v.is('default', 'separate'), + default: 'default', + }, + }, - const link = html.tag('a', + generate: (data, slots, {html, language}) => + html.tag('a', { href: data.url, class: 'nowrap', + target: + (slots.tab === 'separate' + ? '_blank' + : null), }, - - // truly unhinged indentation here - isLocal - ? language.$('misc.external.local') - - : domain.includes('bandcamp.com') - ? language.$('misc.external.bandcamp') - - : BANDCAMP_DOMAINS.includes(domain) - ? language.$('misc.external.bandcamp.domain', {domain}) - - : MASTODON_DOMAINS.includes(domain) - ? language.$('misc.external.mastodon.domain', {domain}) - - : domain.includes('youtu') - ? slots.mode === 'album' - ? data.url.includes('list=') - ? language.$('misc.external.youtube.playlist') - : language.$('misc.external.youtube.fullAlbum') - : language.$('misc.external.youtube') - - : domain.includes('soundcloud') - ? language.$('misc.external.soundcloud') - - : domain.includes('tumblr.com') - ? language.$('misc.external.tumblr') - - : domain.includes('twitter.com') - ? language.$('misc.external.twitter') - - : domain.includes('deviantart.com') - ? language.$('misc.external.deviantart') - - : domain.includes('wikipedia.org') - ? language.$('misc.external.wikipedia') - - : domain.includes('poetryfoundation.org') - ? language.$('misc.external.poetryFoundation') - - : domain.includes('instagram.com') - ? language.$('misc.external.instagram') - - : domain.includes('patreon.com') - ? language.$('misc.external.patreon') - - : domain.includes('spotify.com') - ? language.$('misc.external.spotify') - - : domain.includes('newgrounds.com') - ? language.$('misc.external.newgrounds') - - : domain); - - switch (slots.mode) { - case 'flash': { - const wrap = content => - html.tag('span', {class: 'nowrap'}, content); - - if (domain.includes('homestuck.com')) { - const match = pathname.match(/\/story\/(.*)\/?/); - if (match) { - if (isNaN(Number(match[1]))) { - return wrap(language.$('misc.external.flash.homestuck.secret', {link})); - } else { - return wrap(language.$('misc.external.flash.homestuck.page', { - link, - page: match[1], - })); - } - } - } else if (domain.includes('bgreco.net')) { - return wrap(language.$('misc.external.flash.bgreco', {link})); - } else if (domain.includes('youtu')) { - return wrap(language.$('misc.external.flash.youtube', {link})); - } - - return link; - } - - default: - return link; - } - } + language.formatExternalLink(data.url, { + style: slots.style, + context: slots.context, + })), }; diff --git a/src/content/dependencies/linkExternalAsIcon.js b/src/content/dependencies/linkExternalAsIcon.js index cd168992..357c835c 100644 --- a/src/content/dependencies/linkExternalAsIcon.js +++ b/src/content/dependencies/linkExternalAsIcon.js @@ -1,46 +1,45 @@ -// TODO: Define these as extra dependencies and pass them somewhere -const BANDCAMP_DOMAINS = ['bc.s3m.us', 'music.solatrux.com']; -const MASTODON_DOMAINS = ['types.pl']; +import {isExternalLinkContext} from '#external-links'; export default { extraDependencies: ['html', 'language', 'to'], - data(url) { - return {url}; + data: (url) => ({url}), + + slots: { + context: { + // This awkward syntax is because the slot descriptor validator can't + // differentiate between a function that returns a validator (the usual + // syntax) and a function that is itself a validator. + validate: () => isExternalLinkContext, + default: 'generic', + }, + + withText: {type: 'boolean'}, }, - generate(data, {html, language, to}) { - const domain = new URL(data.url).hostname; - const [id, msg] = ( - domain.includes('bandcamp.com') - ? ['bandcamp', language.$('misc.external.bandcamp')] - : BANDCAMP_DOMAINS.includes(domain) - ? ['bandcamp', language.$('misc.external.bandcamp.domain', {domain})] - : MASTODON_DOMAINS.includes(domain) - ? ['mastodon', language.$('misc.external.mastodon.domain', {domain})] - : domain.includes('youtu') - ? ['youtube', language.$('misc.external.youtube')] - : domain.includes('soundcloud') - ? ['soundcloud', language.$('misc.external.soundcloud')] - : domain.includes('tumblr.com') - ? ['tumblr', language.$('misc.external.tumblr')] - : domain.includes('twitter.com') - ? ['twitter', language.$('misc.external.twitter')] - : domain.includes('deviantart.com') - ? ['deviantart', language.$('misc.external.deviantart')] - : domain.includes('instagram.com') - ? ['instagram', language.$('misc.external.bandcamp')] - : domain.includes('newgrounds.com') - ? ['newgrounds', language.$('misc.external.newgrounds')] - : ['globe', language.$('misc.external.domain', {domain})]); + generate(data, slots, {html, language, to}) { + const format = style => + language.formatExternalLink(data.url, {style, context: slots.context}); + + const normalText = format('normal'); + const compactText = format('compact'); + const iconId = format('icon-id'); return html.tag('a', - {href: data.url, class: 'icon'}, - html.tag('svg', [ - html.tag('title', msg), - html.tag('use', { - href: to('shared.staticIcon', id), - }), - ])); + {href: data.url, class: ['icon', slots.withText && 'has-text']}, + [ + html.tag('svg', [ + !slots.withText && + html.tag('title', normalText), + + html.tag('use', { + href: to('shared.staticIcon', iconId), + }), + ]), + + slots.withText && + html.tag('span', {class: 'icon-text'}, + compactText ?? normalText), + ]); }, }; diff --git a/src/content/dependencies/linkExternalFlash.js b/src/content/dependencies/linkExternalFlash.js deleted file mode 100644 index 65158ff8..00000000 --- a/src/content/dependencies/linkExternalFlash.js +++ /dev/null @@ -1,41 +0,0 @@ -// Note: This function is seriously hard-coded for HSMusic, with custom -// presentation of links to Homestuck flashes hosted various places. - -export default { - contentDependencies: ['linkExternal'], - extraDependencies: ['html', 'language'], - - relations(relation, url) { - return { - link: relation('linkExternal', url), - }; - }, - - data(url, flash) { - return { - url, - page: flash.page, - }; - }, - - generate(data, relations, {html, language}) { - const {link} = relations; - const {url, page} = data; - - return html.tag('span', - {class: 'nowrap'}, - - url.includes('homestuck.com') - ? isNaN(Number(page)) - ? language.$('misc.external.flash.homestuck.secret', {link}) - : language.$('misc.external.flash.homestuck.page', {link, page}) - - : url.includes('bgreco.net') - ? language.$('misc.external.flash.bgreco', {link}) - - : url.includes('youtu') - ? language.$('misc.external.flash.youtube', {link}) - - : link); - }, -}; diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index d9af726c..a361a4e7 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -64,6 +64,14 @@ export default { style = `--primary-color: ${primary}; --dim-color: ${dim}`; } + if (slots.attributes?.style) { + if (style) { + style += '; ' + slots.attributes.style; + } else { + style = slots.attributes.style; + } + } + if (slots.tooltip) { title = slots.tooltip; } diff --git a/src/content/dependencies/linkTrackDynamically.js b/src/content/dependencies/linkTrackDynamically.js new file mode 100644 index 00000000..242cd4cb --- /dev/null +++ b/src/content/dependencies/linkTrackDynamically.js @@ -0,0 +1,34 @@ +export default { + contentDependencies: ['linkTrack'], + extraDependencies: ['pagePath'], + + relations: (relation, track) => ({ + infoLink: relation('linkTrack', track), + }), + + data: (track) => ({ + trackDirectory: + track.directory, + + albumDirectory: + track.album.directory, + + trackHasCommentary: + !!track.commentary, + }), + + generate(data, relations, {pagePath}) { + if ( + pagePath[0] === 'albumCommentary' && + pagePath[1] === data.albumDirectory && + data.trackHasCommentary + ) { + relations.infoLink.setSlots({ + anchor: true, + hash: data.trackDirectory, + }); + } + + return relations.infoLink; + }, +}; diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js index 86c8cfa2..58c51a40 100644 --- a/src/content/dependencies/listArtistsByContributions.js +++ b/src/content/dependencies/listArtistsByContributions.js @@ -1,5 +1,11 @@ -import {stitchArrays, unique} from '#sugar'; -import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data'; +import {empty, stitchArrays, unique} from '#sugar'; + +import { + filterByCount, + filterMultipleArrays, + sortAlphabetically, + sortByCount, +} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], @@ -96,68 +102,54 @@ export default { return data; }, - generate(data, relations, {html, language}) { - const lists = Object.fromEntries( - ([ - ['tracks', [ - relations.artistLinksByTrackContributions, - data.countsByTrackContributions, - 'countTracks', - ]], - - ['artworks', [ - relations.artistLinksByArtworkContributions, - data.countsByArtworkContributions, - 'countArtworks', - ]], - - data.enableFlashesAndGames && - ['flashes', [ - relations.artistLinksByFlashContributions, - data.countsByFlashContributions, - 'countFlashes', - ]], - ]).filter(Boolean) - .map(([key, [artistLinks, counts, countFunction]]) => [ - key, - html.tag('ul', - stitchArrays({ - artistLink: artistLinks, - count: counts, - }).map(({artistLink, count}) => - html.tag('li', - language.$('listingPage.listArtists.byContribs.item', { - artist: artistLink, - contributions: language[countFunction](count, {unit: true}), - })))), - ])); + generate(data, relations, {language}) { + const listChunkIDs = ['tracks', 'artworks', 'flashes']; + const listTitleStringsKeys = ['trackContributors', 'artContributors', 'flashContributors']; + const listCountFunctions = ['countTracks', 'countArtworks', 'countFlashes']; + + const listArtistLinks = [ + relations.artistLinksByTrackContributions, + relations.artistLinksByArtworkContributions, + relations.artistLinksByFlashContributions, + ]; + + const listArtistCounts = [ + data.countsByTrackContributions, + data.countsByArtworkContributions, + data.countsByFlashContributions, + ]; + + filterMultipleArrays( + listChunkIDs, + listTitleStringsKeys, + listCountFunctions, + listArtistLinks, + listArtistCounts, + (_chunkID, _titleStringsKey, _countFunction, artistLinks, _artistCounts) => + !empty(artistLinks)); return relations.page.slots({ - type: 'custom', - content: - html.tag('div', {class: 'content-columns'}, [ - html.tag('div', {class: 'column'}, [ - html.tag('h2', - language.$('listingPage.misc.trackContributors')), - - lists.tracks, - ]), - - html.tag('div', {class: 'column'}, [ - html.tag('h2', - language.$( - 'listingPage.misc.artContributors')), - - lists.artworks, - - lists.flashes && [ - html.tag('h2', - language.$('listingPage.misc.flashContributors')), - - lists.flashes, - ], - ]), - ]), + type: 'chunks', + + showSkipToSection: true, + chunkIDs: listChunkIDs, + + chunkTitles: + listTitleStringsKeys.map(stringsKey => ({stringsKey})), + + chunkRows: + stitchArrays({ + artistLinks: listArtistLinks, + artistCounts: listArtistCounts, + countFunction: listCountFunctions, + }).map(({artistLinks, artistCounts, countFunction}) => + stitchArrays({ + artistLink: artistLinks, + artistCount: artistCounts, + }).map(({artistLink, artistCount}) => ({ + artist: artistLink, + contributions: language[countFunction](artistCount, {unit: true}), + }))), }); }, }; diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js new file mode 100644 index 00000000..3778b9e3 --- /dev/null +++ b/src/content/dependencies/listArtistsByGroup.js @@ -0,0 +1,133 @@ +import {empty, stitchArrays, unique} from '#sugar'; + +import { + filterMultipleArrays, + getArtistNumContributions, + sortAlphabetically, +} from '#wiki-data'; + +export default { + contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], + extraDependencies: ['language', 'wikiData'], + + sprawl({artistData, wikiInfo}) { + return {artistData, wikiInfo}; + }, + + query(sprawl, spec) { + const artists = sortAlphabetically(sprawl.artistData.slice()); + const groups = sprawl.wikiInfo.divideTrackListsByGroups; + + if (empty(groups)) { + return {spec, artists}; + } + + const artistGroups = + artists.map(artist => + unique( + unique([ + ...artist.albumsAsAny, + ...artist.tracksAsAny.map(track => track.album), + ]).flatMap(album => album.groups))) + + const artistsByGroup = + groups.map(group => + artists.filter((artist, index) => artistGroups[index].includes(group))); + + filterMultipleArrays(groups, artistsByGroup, + (group, artists) => !empty(artists)); + + return {spec, groups, artistsByGroup}; + }, + + relations(relation, query) { + const relations = {}; + + relations.page = + relation('generateListingPage', query.spec); + + if (query.artists) { + relations.artistLinks = + query.artists + .map(artist => relation('linkArtist', artist)); + } + + if (query.artistsByGroup) { + relations.groupLinks = + query.groups + .map(group => relation('linkGroup', group)); + + relations.artistLinksByGroup = + query.artistsByGroup + .map(artists => artists + .map(artist => relation('linkArtist', artist))); + } + + return relations; + }, + + data(query) { + const data = {}; + + if (query.artists) { + data.counts = + query.artists + .map(artist => getArtistNumContributions(artist)); + } + + if (query.artistsByGroup) { + data.groupDirectories = + query.groups + .map(group => group.directory); + + data.countsByGroup = + query.artistsByGroup + .map(artists => artists + .map(artist => getArtistNumContributions(artist))); + } + + return data; + }, + + generate(data, relations, {language}) { + return ( + (relations.artistLinksByGroup + ? relations.page.slots({ + type: 'chunks', + + showSkipToSection: true, + chunkIDs: + data.groupDirectories + .map(directory => `contributed-to-${directory}`), + + chunkTitles: + relations.groupLinks.map(groupLink => ({ + group: groupLink, + })), + + chunkRows: + stitchArrays({ + artistLinks: relations.artistLinksByGroup, + counts: data.countsByGroup, + }).map(({artistLinks, counts}) => + stitchArrays({ + link: artistLinks, + count: counts, + }).map(({link, count}) => ({ + artist: link, + contributions: language.countContributions(count, {unit: true}), + }))), + }) + : relations.page.slots({ + type: 'rows', + rows: + stitchArrays({ + link: relations.artistLinks, + count: data.counts, + }).map(({link, count}) => ({ + artist: link, + contributions: language.countContributions(count, {unit: true}), + })), + }))); + }, +}; diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js index 3870afde..45f8390f 100644 --- a/src/content/dependencies/listArtistsByLatestContribution.js +++ b/src/content/dependencies/listArtistsByLatestContribution.js @@ -1,15 +1,16 @@ -import {transposeArrays, empty, stitchArrays} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; +import T from '#things'; import { chunkMultipleArrays, - compareCaseLessSensitive, - compareDates, - filterMultipleArrays, - reduceMultipleArrays, sortAlphabetically, + sortAlbumsTracksChronologically, + sortFlashesChronologically, sortMultipleArrays, } from '#wiki-data'; +const {Album, Flash} = T; + export default { contentDependencies: [ 'generateListingPage', @@ -20,348 +21,299 @@ export default { extraDependencies: ['html', 'language', 'wikiData'], - sprawl({artistData, wikiInfo}) { - return { - artistData, - enableFlashesAndGames: wikiInfo.enableFlashesAndGames, - }; - }, + sprawl: ({albumData, artistData, flashData, trackData, wikiInfo}) => + ({albumData, artistData, flashData, trackData, + enableFlashesAndGames: wikiInfo.enableFlashesAndGames}), query(sprawl, spec) { - const query = { - spec, - enableFlashesAndGames: sprawl.enableFlashesAndGames, - }; - - const queryContributionInfo = ( - artistsKey, - chunkThingsKey, - datesKey, - datelessArtistsKey, - fn, - ) => { - const artists = sortAlphabetically(sprawl.artistData.slice()); - - // Each value stored in dateLists, corresponding to each artist, - // is going to be a list of dates and nulls. Any nulls represent - // a contribution which isn't associated with a particular date. - const [chunkThingLists, dateLists] = - transposeArrays(artists.map(artist => fn(artist))); - - // Scrap artists who don't even have any relevant contributions. - // These artists may still have other contributions across the wiki, but - // they weren't returned by the callback and so aren't relevant to this - // list. - filterMultipleArrays( - artists, - chunkThingLists, - dateLists, - (artists, chunkThings, dates) => !empty(dates)); - - // Also exclude artists whose remaining contributions are all dateless. - // But keep track of the artists removed here, since they'll be displayed - // in an additional list in the final listing page. - const {removed: [datelessArtists]} = - filterMultipleArrays( - artists, - chunkThingLists, - dateLists, - (artist, chunkThings, dates) => !empty(dates.filter(Boolean))); - - // Cut out dateless contributions. They're not relevant to finding the - // latest date. - for (const [chunkThings, dates] of transposeArrays([chunkThingLists, dateLists])) { - filterMultipleArrays(chunkThings, dates, (chunkThing, date) => date); + // + // First main step is to get the latest thing each artist has contributed + // to, and the date associated with that contribution! Some notes: + // + // * Album and track contributions are considered before flashes, so + // they'll take priority if an artist happens to have multiple contribs + // landing on the same date to both an album and a flash. + // + // * The final (album) contribution list is chunked by album, but also by + // date, because an individual album can cover a variety of dates. + // + // * If an artist has contributed both artworks and tracks to the album + // containing their latest contribution, then that will be indicated + // in an annotation, but *only if* those contributions were also on + // the same date. + // + // * If an artist made contributions to multiple albums on the same date, + // then the first of the *albums* sorted chronologically (latest first) + // is the one that will count. + // + // * Same for artists who've contributed to multiple flashes which were + // released on the same date. + // + // * The map may exclude artists none of whose contributions were dated. + // + + const artistLatestContribMap = new Map(); + + const considerDate = (artist, date, thing, contribution) => { + if (!date) { + return; } - const [chunkThings, dates] = - transposeArrays( - transposeArrays([chunkThingLists, dateLists]) - .map(([chunkThings, dates]) => - reduceMultipleArrays( - chunkThings, dates, - (accChunkThing, accDate, chunkThing, date) => - (date && date > accDate - ? [chunkThing, date] - : [accChunkThing, accDate])))); - - sortMultipleArrays(artists, dates, chunkThings, - (artistA, artistB, dateA, dateB, chunkThingA, chunkThingB) => { - const dateComparison = compareDates(dateA, dateB, {latestFirst: true}); - if (dateComparison !== 0) { - return dateComparison; - } - - // TODO: Compare alphabetically, not just by directory. - return compareCaseLessSensitive(chunkThingA.directory, chunkThingB.directory); - }); - - const chunks = - chunkMultipleArrays(artists, dates, chunkThings, - (artist, lastArtist, date, lastDate, chunkThing, lastChunkThing) => - +date !== +lastDate || chunkThing !== lastChunkThing); + if (artistLatestContribMap.has(artist)) { + const latest = artistLatestContribMap.get(artist); + if (latest.date > date) { + return; + } - query[chunkThingsKey] = - chunks.map(([artists, dates, chunkThings]) => chunkThings[0]); - - query[datesKey] = - chunks.map(([artists, dates, chunkThings]) => dates[0]); + if (latest.date === date) { + if (latest.thing === thing) { + // May combine differnt contributions to the same thing and date. + latest.contribution.add(contribution); + } - query[artistsKey] = - chunks.map(([artists, dates, chunkThings]) => artists); + // Earlier-processed things of same date take priority. + return; + } + } - query[datelessArtistsKey] = datelessArtists; + // First entry for artist or more recent contribution than latest date. + artistLatestContribMap.set(artist, { + date, + thing, + contribution: new Set([contribution]), + }); }; - queryContributionInfo( - 'artistsByTrackContributions', - 'albumsByTrackContributions', - 'datesByTrackContributions', - 'datelessArtistsByTrackContributions', - artist => { - const tracks = - [...artist.tracksAsArtist, ...artist.tracksAsContributor] - .filter(track => !track.originalReleaseTrack); - - const albums = tracks.map(track => track.album); - const dates = tracks.map(track => track.date); + const getArtists = (thing, key) => thing[key].map(({who}) => who); - return [albums, dates]; - }); + const albumsLatestFirst = sortAlbumsTracksChronologically(sprawl.albumData.slice()); + const tracksLatestFirst = sortAlbumsTracksChronologically(sprawl.trackData.slice()); + const flashesLatestFirst = sortFlashesChronologically(sprawl.flashData.slice()); - queryContributionInfo( - 'artistsByArtworkContributions', - 'albumsByArtworkContributions', - 'datesByArtworkContributions', - 'datelessArtistsByArtworkContributions', - artist => [ - [ - ...artist.tracksAsCoverArtist.map(track => track.album), - ...artist.albumsAsCoverArtist, - ...artist.albumsAsWallpaperArtist, - ...artist.albumsAsBannerArtist, - ], - [ - // TODO: Per-artwork dates, see #90. - ...artist.tracksAsCoverArtist.map(track => track.coverArtDate ?? track.date), - ...artist.albumsAsCoverArtist.map(album => album.coverArtDate ?? album.date), - ...artist.albumsAsWallpaperArtist.map(album => album.coverArtDate ?? album.date), - ...artist.albumsAsBannerArtist.map(album => album.coverArtDate ?? album.date), - ], - ]); - - if (sprawl.enableFlashesAndGames) { - queryContributionInfo( - 'artistsByFlashContributions', - 'flashesByFlashContributions', - 'datesByFlashContributions', - 'datelessArtistsByFlashContributions', - artist => [ - [ - ...artist.flashesAsContributor, - ], - [ - ...artist.flashesAsContributor.map(flash => flash.date), - ], - ]); + for (const album of albumsLatestFirst) { + for (const artist of new Set([ + ...getArtists(album, 'coverArtistContribs'), + ...getArtists(album, 'wallpaperArtistContribs'), + ...getArtists(album, 'bannerArtistContribs'), + ])) { + // Might combine later with 'track' of the same album and date. + considerDate(artist, album.coverArtDate ?? album.date, album, 'artwork'); + } } - return query; - }, - - relations(relation, query) { - const relations = {}; - - relations.page = - relation('generateListingPage', query.spec); - - // Track contributors - - relations.albumLinksByTrackContributions = - query.albumsByTrackContributions - .map(album => relation('linkAlbum', album)); - - relations.artistLinksByTrackContributions = - query.artistsByTrackContributions - .map(artists => - artists.map(artist => relation('linkArtist', artist))); - - relations.datelessArtistLinksByTrackContributions = - query.datelessArtistsByTrackContributions - .map(artist => relation('linkArtist', artist)); - - // Artwork contributors - - relations.albumLinksByArtworkContributions = - query.albumsByArtworkContributions - .map(album => relation('linkAlbum', album)); - - relations.artistLinksByArtworkContributions = - query.artistsByArtworkContributions - .map(artists => - artists.map(artist => relation('linkArtist', artist))); + 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'); + } - relations.datelessArtistLinksByArtworkContributions = - query.datelessArtistsByArtworkContributions - .map(artist => relation('linkArtist', artist)); + for (const artist of new Set([ + ...getArtists(track, 'artistContribs'), + ...getArtists(track, 'contributorContribs'), + ])) { + // Might be combining with 'artwork' of the same album and date. + considerDate(artist, track.date, track.album, 'track'); + } + } - // Flash contributors + for (const flash of flashesLatestFirst) { + for (const artist of getArtists(flash, 'contributorContribs')) { + // Won't take priority above album contributions of the same date. + considerDate(artist, flash.date, flash, 'flash'); + } + } - if (query.enableFlashesAndGames) { - relations.flashLinksByFlashContributions = - query.flashesByFlashContributions - .map(flash => relation('linkFlash', flash)); + // + // Next up is to sort all the processed artist information! + // + // Entries with the same album/flash and the same date go together first, + // with the following rules for sorting artists therein: + // + // * If the contributions are different, which can only happen for albums, + // then it's tracks-only first, tracks + artworks next, and artworks-only + // last. + // + // * If the contributions are the same, then sort alphabetically. + // + // Entries with different albums/flashes follow another set of rules: + // + // * Later dates come before earlier dates. + // + // * On the same date, albums come before flashes. + // + // * Things of the same type *and* date are sorted alphabetically. + // + + const artistsAlphabetically = + sortAlphabetically(sprawl.artistData.slice()); + + const artists = + Array.from(artistLatestContribMap.keys()); + + const artistContribEntries = + Array.from(artistLatestContribMap.values()); + + const artistThings = + artistContribEntries.map(({thing}) => thing); + + const artistDates = + artistContribEntries.map(({date}) => date); + + const artistContributions = + artistContribEntries.map(({contribution}) => contribution); + + sortMultipleArrays(artistThings, artistDates, artistContributions, artists, + (thing1, thing2, date1, date2, contrib1, contrib2, artist1, artist2) => { + if (date1 === date2 && thing1 === thing2) { + // Move artwork-only contribs after contribs with tracks. + if (!contrib1.has('track') && contrib2.has('track')) return 1; + if (!contrib2.has('track') && contrib1.has('track')) return -1; + + // Move track-only contribs before tracks with tracks and artwork. + if (!contrib1.has('artwork') && contrib2.has('artwork')) return -1; + if (!contrib2.has('artwork') && contrib1.has('artwork')) return 1; + + // Sort artists of the same type of contribution alphabetically, + // referring to a previous sort. + const index1 = artistsAlphabetically.indexOf(artist1); + const index2 = artistsAlphabetically.indexOf(artist2); + return index1 - index2; + } else { + // Move later dates before earlier ones. + if (date1 !== date2) return date2 - date1; + + // Move albums before flashes. + if (thing1 instanceof Album && thing2 instanceof Flash) return -1; + if (thing1 instanceof Flash && thing2 instanceof Album) return 1; + + // Sort two albums or two flashes alphabetically, referring to a + // previous sort (which was chronological but includes the correct + // ordering for things released on the same date). + const thingsLatestFirst = + (thing1 instanceof Album + ? albumsLatestFirst + : flashesLatestFirst); + const index1 = thingsLatestFirst.indexOf(thing1); + const index2 = thingsLatestFirst.indexOf(thing2); + return index2 - index1; + } + }); - relations.artistLinksByFlashContributions = - query.artistsByFlashContributions - .map(artists => - artists.map(artist => relation('linkArtist', artist))); + const chunks = + chunkMultipleArrays(artistThings, artistDates, artistContributions, artists, + (thing, lastThing, date, lastDate) => + thing !== lastThing || + +date !== +lastDate); - relations.datelessArtistLinksByFlashContributions = - query.datelessArtistsByFlashContributions - .map(artist => relation('linkArtist', artist)); - } + const chunkThings = + chunks.map(([artistThings, , , ]) => artistThings[0]); - return relations; - }, + const chunkDates = + chunks.map(([, artistDates, , ]) => artistDates[0]); - data(query) { - const data = {}; + const chunkArtistContributions = + chunks.map(([, , artistContributions, ]) => artistContributions); - data.enableFlashesAndGames = query.enableFlashesAndGames; + const chunkArtists = + chunks.map(([, , , artists]) => artists); - data.datesByTrackContributions = query.datesByTrackContributions; - data.datesByArtworkContributions = query.datesByArtworkContributions; + // And one bonus step - keep track of all the artists whose contributions + // were all without date. - if (query.enableFlashesAndGames) { - data.datesByFlashContributions = query.datesByFlashContributions; - } + const datelessArtists = + artistsAlphabetically + .filter(artist => !artists.includes(artist)); - return data; + return { + spec, + chunkThings, + chunkDates, + chunkArtistContributions, + chunkArtists, + datelessArtists, + }; }, - generate(data, relations, {html, language}) { - const chunkTitles = Object.fromEntries( - ([ - ['tracks', [ - 'album', - relations.albumLinksByTrackContributions, - data.datesByTrackContributions, - ]], - - ['artworks', [ - 'album', - relations.albumLinksByArtworkContributions, - data.datesByArtworkContributions, - ]], - - data.enableFlashesAndGames && - ['flashes', [ - 'flash', - relations.flashLinksByFlashContributions, - data.datesByFlashContributions, - ]], - ]).filter(Boolean) - .map(([key, [stringsKey, links, dates]]) => [ - key, - stitchArrays({link: links, date: dates}) - .map(({link, date}) => - html.tag('dt', - language.$(`listingPage.listArtists.byLatest.chunk.title.${stringsKey}`, { - [stringsKey]: link, - date: language.formatDate(date), - }))), - ])); - - const chunkItems = Object.fromEntries( - ([ - ['tracks', relations.artistLinksByTrackContributions], - ['artworks', relations.artistLinksByArtworkContributions], - data.enableFlashesAndGames && - ['flashes', relations.artistLinksByFlashContributions], - ]).filter(Boolean) - .map(([key, artistLinkLists]) => [ - key, - artistLinkLists.map(artistLinks => - html.tag('dd', - html.tag('ul', - artistLinks.map(artistLink => - html.tag('li', - language.$('listingPage.listArtists.byLatest.chunk.item', { - artist: artistLink, - })))))), - ])); - - const lists = Object.fromEntries( - ([ - ['tracks', [ - chunkTitles.tracks, - chunkItems.tracks, - relations.datelessArtistLinksByTrackContributions, - ]], - - ['artworks', [ - chunkTitles.artworks, - chunkItems.artworks, - relations.datelessArtistLinksByArtworkContributions, - ]], - - data.enableFlashesAndGames && - ['flashes', [ - chunkTitles.flashes, - chunkItems.flashes, - relations.datelessArtistLinksByFlashContributions, - ]], - ]).filter(Boolean) - .map(([key, [titles, items, datelessArtistLinks]]) => [ - key, - html.tags([ - html.tag('dl', - stitchArrays({ - title: titles, - items: items, - }).map(({title, items}) => [title, items])), - - !empty(datelessArtistLinks) && [ - html.tag('p', - language.$('listingPage.listArtists.byLatest.dateless.title')), - - html.tag('ul', - datelessArtistLinks.map(artistLink => - html.tag('li', - language.$('listingPage.listArtists.byLatest.dateless.item', { - artist: artistLink, - })))), - ], - ]), - ])); - + relations: (relation, query) => ({ + page: + relation('generateListingPage', query.spec), + + chunkAlbumLinks: + query.chunkThings + .map(thing => + (thing instanceof Album + ? relation('linkAlbum', thing) + : null)), + + chunkFlashLinks: + query.chunkThings + .map(thing => + (thing instanceof Flash + ? relation('linkFlash', thing) + : null)), + + chunkArtistLinks: + query.chunkArtists + .map(artists => artists + .map(artist => relation('linkArtist', artist))), + + datelessArtistLinks: + query.datelessArtists + .map(artist => relation('linkArtist', artist)), + }), + + data: (query) => ({ + chunkDates: query.chunkDates, + chunkArtistContributions: query.chunkArtistContributions, + }), + + generate(data, relations, {language}) { return relations.page.slots({ - type: 'custom', - content: - html.tag('div', {class: 'content-columns'}, [ - html.tag('div', {class: 'column'}, [ - html.tag('h2', - language.$('listingPage.misc.trackContributors')), - - lists.tracks, - ]), - - html.tag('div', {class: 'column'}, [ - html.tag('h2', - language.$( - 'listingPage.misc.artContributors')), - - lists.artworks, - - lists.flashes && [ - html.tag('h2', - language.$('listingPage.misc.flashContributors')), - - lists.flashes, - ], - ]), - ]), + type: 'chunks', + + chunkTitles: + stitchArrays({ + albumLink: relations.chunkAlbumLinks, + flashLink: relations.chunkFlashLinks, + date: data.chunkDates, + }).map(({albumLink, flashLink, date}) => ({ + date: language.formatDate(date), + ...(albumLink + ? {stringsKey: 'album', album: albumLink} + : {stringsKey: 'flash', flash: flashLink}), + })) + .concat( + (empty(relations.datelessArtistLinks) + ? [] + : [{stringsKey: 'dateless'}])), + + chunkRows: + stitchArrays({ + artistLinks: relations.chunkArtistLinks, + contributions: data.chunkArtistContributions, + }).map(({artistLinks, contributions}) => + stitchArrays({ + artistLink: artistLinks, + contribution: contributions, + }).map(({artistLink, contribution}) => ({ + artist: artistLink, + stringsKey: + (contribution.has('track') && contribution.has('artwork') + ? 'tracksAndArt' + : contribution.has('track') + ? 'tracks' + : contribution.has('artwork') + ? 'art' + : null), + }))) + .concat( + (empty(relations.datelessArtistLinks) + ? [] + : [ + relations.datelessArtistLinks.map(artistLink => ({ + artist: artistLink, + })), + ])), }); }, }; diff --git a/src/content/dependencies/listArtistsByName.js b/src/content/dependencies/listArtistsByName.js index 6c0ad836..554b4587 100644 --- a/src/content/dependencies/listArtistsByName.js +++ b/src/content/dependencies/listArtistsByName.js @@ -2,38 +2,33 @@ import {stitchArrays} from '#sugar'; import {getArtistNumContributions, sortAlphabetically} from '#wiki-data'; export default { - contentDependencies: ['generateListingPage', 'linkArtist'], + contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], extraDependencies: ['language', 'wikiData'], - sprawl({artistData}) { - return {artistData}; - }, + sprawl: ({artistData, wikiInfo}) => + ({artistData, wikiInfo}), - query({artistData}, spec) { - return { - spec, + query: (sprawl, spec) => ({ + spec, - artists: sortAlphabetically(artistData.slice()), - }; - }, + artists: + sortAlphabetically(sprawl.artistData.slice()), + }), - relations(relation, query) { - return { - page: relation('generateListingPage', query.spec), + relations: (relation, query) => ({ + page: + relation('generateListingPage', query.spec), - artistLinks: - query.artists - .map(artist => relation('linkArtist', artist)), - }; - }, + artistLinks: + query.artists + .map(artist => relation('linkArtist', artist)), + }), - data(query) { - return { - counts: - query.artists - .map(artist => getArtistNumContributions(artist)), - }; - }, + data: (query) => ({ + counts: + query.artists + .map(artist => getArtistNumContributions(artist)), + }), generate(data, relations, {language}) { return relations.page.slots({ diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js index 43bf7dd5..18585696 100644 --- a/src/content/dependencies/listRandomPageLinks.js +++ b/src/content/dependencies/listRandomPageLinks.js @@ -1,90 +1,192 @@ +import {empty} from '#sugar'; +import {sortChronologically} from '#wiki-data'; + export default { contentDependencies: [ 'generateListingPage', - 'generateListRandomPageLinksGroupSection', + 'generateListRandomPageLinksAlbumLink', + 'linkGroup', ], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({groupData}) { - return {groupData}; - }, + sprawl: ({albumData, wikiInfo}) => ({albumData, wikiInfo}), query(sprawl, spec) { - const group = directory => - sprawl.groupData.find(group => group.directory === directory); - - return { - spec, - officialGroup: group('official'), - fandomGroup: group('fandom'), - beyondGroup: group('beyond'), - }; + const query = {spec}; + + const groups = sprawl.wikiInfo.divideTrackListsByGroups; + + query.divideByGroups = !empty(groups); + + if (query.divideByGroups) { + query.groups = groups; + + query.groupAlbums = + groups + .map(group => + group.albums.filter(album => album.tracks.length > 1)); + } else { + query.undividedAlbums = + sortChronologically(sprawl.albumData.slice()) + .filter(album => album.tracks.length > 1); + } + + return query; }, relations(relation, query) { - return { - page: relation('generateListingPage', query.spec), + const relations = {}; + + relations.page = + relation('generateListingPage', query.spec); + + if (query.divideByGroups) { + relations.groupLinks = + query.groups + .map(group => relation('linkGroup', group)); + + relations.groupAlbumLinks = + query.groupAlbums + .map(albums => albums + .map(album => + relation('generateListRandomPageLinksAlbumLink', album))); + } else { + relations.undividedAlbumLinks = + query.undividedAlbums + .map(album => + relation('generateListRandomPageLinksAlbumLink', album)); + } + + return relations; + }, - officialSection: - relation('generateListRandomPageLinksGroupSection', query.officialGroup), + data(query) { + const data = {}; - fandomSection: - relation('generateListRandomPageLinksGroupSection', query.fandomGroup), + if (query.divideByGroups) { + data.groupDirectories = + query.groups + .map(group => group.directory); + } - beyondSection: - relation('generateListRandomPageLinksGroupSection', query.beyondGroup), - }; + return data; }, - generate(relations, {html, language}) { + generate(data, relations, {html, language}) { + const miscellaneousChunkRows = [ + { + stringsKey: 'randomArtist', + + mainLink: + html.tag('a', + {href: '#', 'data-random': 'artist'}, + language.$('listingPage.other.randomPages.chunk.item.randomArtist.mainLink')), + + atLeastTwoContributions: + html.tag('a', + {href: '#', 'data-random': 'artist-more-than-one-contrib'}, + language.$('listingPage.other.randomPages.chunk.item.randomArtist.atLeastTwoContributions')), + }, + + {stringsKey: 'randomAlbumWholeSite'}, + {stringsKey: 'randomTrackWholeSite'}, + ]; + + const miscellaneousChunkRowAttributes = [ + null, + {href: '#', 'data-random': 'album'}, + {href: '#','data-random': 'track'}, + ]; + return relations.page.slots({ - type: 'custom', + type: 'chunks', + content: [ html.tag('p', - language.$('listingPage.other.randomPages.chooseLinkLine')), + language.$('listingPage.other.randomPages.chooseLinkLine', { + fromPart: + (relations.groupLinks + ? language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.dividedByGroups') + : language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.notDividedByGroups')), - html.tag('p', - {class: 'js-hide-once-data'}, + browserSupportPart: + language.$('listingPage.other.randomPages.chooseLinkLine.browserSupportPart'), + })), + + html.tag('p', {id: 'data-loading-line'}, language.$('listingPage.other.randomPages.dataLoadingLine')), - html.tag('p', - {class: 'js-show-once-data'}, + html.tag('p', {id: 'data-loaded-line'}, language.$('listingPage.other.randomPages.dataLoadedLine')), - html.tag('dl', [ - html.tag('dt', - language.$('listingPage.other.randomPages.misc')), - - html.tag('dd', - html.tag('ul', [ - html.tag('li', [ - html.tag('a', - {href: '#', 'data-random': 'artist'}, - language.$('listingPage.other.randomPages.misc.randomArtist')), - - '(' + - html.tag('a', - {href: '#', 'data-random': 'artist-more-than-one-contrib'}, - language.$('listingPage.other.randomPages.misc.atLeastTwoContributions')) + - ')', + html.tag('p', {id: 'data-error-line'}, + language.$('listingPage.other.randomPages.dataErrorLine')), + ], + + showSkipToSection: true, + + chunkIDs: + (data.groupDirectories + ? [null, ...data.groupDirectories] + : null), + + chunkTitles: [ + {stringsKey: 'misc'}, + + ... + (relations.groupLinks + ? relations.groupLinks.map(groupLink => ({ + stringsKey: 'fromGroup', + group: groupLink, + })) + : [{stringsKey: 'fromAlbum'}]), + ], + + chunkTitleAccents: [ + null, + + ... + (relations.groupLinks + ? relations.groupLinks.map(() => ({ + randomAlbum: + html.tag('a', + {href: '#', 'data-random': 'album-in-group-dl'}, + language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomAlbum')), + + randomTrack: + html.tag('a', + {href: '#', 'data-random': 'track-in-group-dl'}, + language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomTrack')), + })) + : [null]), + ], + + chunkRows: [ + miscellaneousChunkRows, + + ... + (relations.groupAlbumLinks + ? relations.groupAlbumLinks.map(albumLinks => + albumLinks.map(albumLink => ({ + stringsKey: 'album', + album: albumLink, + }))) + : [ + relations.undividedAlbumLinks.map(albumLink => ({ + stringsKey: 'album', + album: albumLink, + })), ]), + ], - html.tag('li', - html.tag('a', - {href: '#', 'data-random': 'album'}, - language.$('listingPage.other.randomPages.misc.randomAlbumWholeSite'))), - - html.tag('li', - html.tag('a', - {href: '#', 'data-random': 'track'}, - language.$('listingPage.other.randomPages.misc.randomTrackWholeSite'))), - ])), - - relations.officialSection, - relations.fandomSection, - relations.beyondSection, - ]), + chunkRowAttributes: [ + miscellaneousChunkRowAttributes, + ... + (relations.groupAlbumLinks + ? relations.groupAlbumLinks.map(albumLinks => + albumLinks.map(() => null)) + : [relations.undividedAlbumLinks.map(() => null)]), ], }); }, diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js index d6546e67..25beb739 100644 --- a/src/content/dependencies/listTracksByDate.js +++ b/src/content/dependencies/listTracksByDate.js @@ -71,8 +71,15 @@ export default { rerelease: rereleases, }).map(({trackLink, rerelease}) => (rerelease - ? {track: trackLink, stringsKey: 'rerelease'} + ? {stringsKey: 'rerelease', track: trackLink} : {track: trackLink}))), + + chunkRowAttributes: + data.rereleases.map(rereleases => + rereleases.map(rerelease => + (rerelease + ? {class: 'rerelease'} + : null))), }); }, }; diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 3c2c3521..2002ebee 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -1,7 +1,7 @@ import {bindFind} from '#find'; import {parseInput} from '#replacer'; -import {marked} from 'marked'; +import {Marked} from 'marked'; export const replacerSpec = { album: { @@ -130,7 +130,7 @@ const linkThingRelationMap = { newsEntry: 'linkNewsEntry', staticPage: 'linkStaticPage', tag: 'linkArtTag', - track: 'linkTrack', + track: 'linkTrackDynamically', }; const linkValueRelationMap = { @@ -147,6 +147,29 @@ const linkIndexRelationMap = { newsIndex: 'linkNewsIndex', }; +const commonMarkedOptions = { + headerIds: false, + mangle: false, +}; + +const multilineMarked = new Marked({ + ...commonMarkedOptions, +}); + +const inlineMarked = new Marked({ + ...commonMarkedOptions, + + renderer: { + paragraph(text) { + return text; + }, + }, +}); + +const lyricsMarked = new Marked({ + ...commonMarkedOptions, +}); + function getPlaceholder(node, content) { return {type: 'text', data: content.slice(node.i, node.iEnd)}; } @@ -447,19 +470,9 @@ export default { return link.data; } - // In inline mode, no further processing is needed! - - if (slots.mode === 'inline') { - return html.tags(contentFromNodes.map(node => node.data)); - } - - // Multiline mode has a secondary processing stage where it's passed... - // through marked! Rolling your own Markdown only gets you so far :D - - const markedOptions = { - headerIds: false, - mangle: false, - }; + // Content always goes through marked (i.e. parsing as Markdown). + // This does require some attention to detail, mostly to do with line + // breaks (in multiline mode) and extracting/re-inserting non-text nodes. // The content of non-text nodes can end up getting mangled by marked. // To avoid this, we replace them with mundane placeholders, then @@ -534,23 +547,36 @@ export default { return html.tags(tags, {[html.joinChildren]: ''}); }; + if (slots.mode === 'inline') { + const markedInput = + extractNonTextNodes(); + + const markedOutput = + inlineMarked.parse(markedInput); + + return reinsertNonTextNodes(markedOutput); + } + // 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. - .replace(/\n{2,}/g, '\n') + // 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> / " ". - .replace(/(?<!^ *-.*|^>.*| $|<br>$)\n+/gm, '\n\n') /* eslint-disable-line no-regex-spaces */ + // or <br> / " ", and which don't precede or follow + // indented text (by at least two spaces). + .replace(/(?<!^ *-.*|^>.*|^ .*\n*| $|<br>$)\n+(?! |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */ // Expand line breaks which are at the end of a list. .replace(/(?<=^ *-.*)\n+(?!^ *-)/gm, '\n\n') // Expand line breaks which are at the end of a quote. .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n'); const markedOutput = - marked.parse(markedInput, markedOptions); + multilineMarked.parse(markedInput); return reinsertNonTextNodes(markedOutput); } @@ -600,7 +626,7 @@ export default { }); const markedOutput = - marked.parse(markedInput, markedOptions); + lyricsMarked.parse(markedInput); return reinsertNonTextNodes(markedOutput); } |