From a1d39a16ab3ed60a9af4ef277fbf5c4a98a84b2d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 1 May 2026 18:37:35 -0300 Subject: content: generateListenLineOrList, generateReleaseInfoBlock --- .../dependencies/generateAlbumReleaseInfo.js | 88 ++++++------ src/content/dependencies/generateArtistInfoPage.js | 21 +-- .../generateExternalLinksLineOrList.js | 99 +++++++++++++ .../dependencies/generateListenLineOrList.js | 145 +++++++++++++++++++ .../dependencies/generateReleaseInfoBlock.js | 64 +++++++++ .../dependencies/generateReleaseInfoListenLine.js | 157 --------------------- .../dependencies/generateTrackReleaseInfo.js | 92 ++++++------ 7 files changed, 398 insertions(+), 268 deletions(-) create mode 100644 src/content/dependencies/generateExternalLinksLineOrList.js create mode 100644 src/content/dependencies/generateListenLineOrList.js create mode 100644 src/content/dependencies/generateReleaseInfoBlock.js delete mode 100644 src/content/dependencies/generateReleaseInfoListenLine.js (limited to 'src/content/dependencies') diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js index 4cec4120..38cd1c05 100644 --- a/src/content/dependencies/generateAlbumReleaseInfo.js +++ b/src/content/dependencies/generateAlbumReleaseInfo.js @@ -1,17 +1,16 @@ import {accumulateSum, empty} from '#sugar'; export default { - relations(relation, album) { - const relations = {}; + relations: (relation, album) => ({ + block: + relation('generateReleaseInfoBlock'), - relations.artistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.artistContribs); + artistContributionsLine: + relation('generateReleaseInfoContributionsLine', album.artistContribs), - relations.listenLine = - relation('generateReleaseInfoListenLine', album); - - return relations; - }, + listenLineOrList: + relation('generateListenLineOrList', album), + }), data(album) { const data = {}; @@ -45,44 +44,37 @@ export default { generate: (data, relations, {html, language}) => language.encapsulate('releaseInfo', capsule => html.tags([ - html.tag('p', - {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, - - [ - relations.artistContributionsLine.slots({ - stringKey: capsule + '.by', - featuringStringKey: capsule + '.by.featuring', - chronologyKind: 'album', - }), - - language.$(capsule, 'released', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.date), - }), - - language.$(capsule, 'duration', { - [language.onlyIfOptions]: ['duration'], - duration: - language.formatDuration(data.duration, { - approximate: data.durationApproximate, - }), - }), - ]), - - html.tag('p', - {[html.onlyIfContent]: true}, - - relations.listenLine.slots({ - context: [ - 'album', - - (data.numTracks === 0 - ? 'albumNoTracks' - : data.numTracks === 1 - ? 'albumOneTrack' - : 'albumMultipleTracks'), - ], - })), + relations.block.slot('items', [ + relations.artistContributionsLine.slots({ + stringKey: capsule + '.by', + featuringStringKey: capsule + '.by.featuring', + chronologyKind: 'album', + }), + + language.$(capsule, 'released', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.date), + }), + + language.$(capsule, 'duration', { + [language.onlyIfOptions]: ['duration'], + duration: + language.formatDuration(data.duration, { + approximate: data.durationApproximate, + }), + }), + ]), + + relations.listenLineOrList.slots({ + context: [ + 'album', + + (data.numTracks === 0 + ? 'albumNoTracks' + : data.numTracks === 1 + ? 'albumOneTrack' + : 'albumMultipleTracks'), + ], + }), ])), }; diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index 45b111ed..ae21b361 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -60,9 +60,8 @@ export default { query.aliasLinkedGroups .map(({group}) => relation('linkGroup', group)), - visitLinks: - artist.urls - .map(entry => relation('linkExternal', entry)), + externalLinksLineOrList: + relation('generateExternalLinksLineOrList', artist.urls), tracksChunkedList: relation('generateArtistInfoPageTracksChunkedList', artist), @@ -182,17 +181,11 @@ export default { }), ])), - html.tag('p', - {[html.onlyIfContent]: true}, - - language.$('releaseInfo.visitOn', { - [language.onlyIfOptions]: ['links'], - - links: - language.formatDisjunctionList( - relations.visitLinks - .map(link => link.slot('context', 'artist'))), - })), + relations.externalLinksLineOrList.slots({ + string: 'releaseInfo.visitOn', + maximumTotalEntriesInLine: 5, + maximumAnnotatedEntriesInLine: 3, + }), html.tag('p', {[html.onlyIfContent]: true}, diff --git a/src/content/dependencies/generateExternalLinksLineOrList.js b/src/content/dependencies/generateExternalLinksLineOrList.js new file mode 100644 index 00000000..18922db6 --- /dev/null +++ b/src/content/dependencies/generateExternalLinksLineOrList.js @@ -0,0 +1,99 @@ +import {isExternalLinkContext} from '#external-links'; +import {empty, stitchArrays} from '#sugar'; + +export default { + relations: (relation, urlEntries) => ({ + externalLinks: + urlEntries.map(entry => relation('linkExternal', entry)), + }), + + data: (urlEntries) => ({ + totalEntries: + urlEntries.length, + + annotatedEntries: + urlEntries.filter(entry => entry.annotation).length, + }), + + slots: { + string: {type: 'string'}, + + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + + contexts: { + validate: v => v.strictArrayOf(isExternalLinkContext), + default: [], + }, + + maximumTotalEntriesInLine: {type: 'number', default: 4}, + maximumAnnotatedEntriesInLine: {type: 'number', default: 1}, + maximumParenthesizedEntriesInLine: {type: 'number', default: 3}, + }, + + generate(data, relations, slots, {html, language}) { + const {externalLinks} = relations; + + if (empty(externalLinks)) { + return html.blank(); + } + + if (!html.isBlank(slots.contexts)) { + stitchArrays({ + link: externalLinks, + linkContext: slots.contexts, + }).forEach(({link, linkContext}) => { + link.setSlot('context', [slots.context, linkContext].flat(Infinity)); + }); + } else { + externalLinks.forEach(link => { + link.setSlot('context', slots.context); + }); + } + + let style = 'line'; + + if (data.totalEntries > slots.maximumTotalEntriesInLine) { + style = 'list'; + } + + if (data.annotatedEntries > slots.maximumAnnotatedEntriesInLine) { + style = 'list'; + } + + let parenthesizedEntries = 0; + for (const item of externalLinks) { + const plainText = html.resolve(item, {normalize: 'plain'}); + if (plainText.endsWith(')')) { + parenthesizedEntries++; + } + } + + if (parenthesizedEntries > slots.maximumParenthesizedEntriesInLine) { + style = 'list'; + } + + switch (style) { + case 'line': return ( + html.tag('p', + language.$(slots.string, { + links: + language.formatDisjunctionList(externalLinks), + })) + ); + + case 'list': return ( + html.tags([ + html.tag('p', language.$(slots.string, 'title')), + html.tag('ul', + externalLinks.map(link => html.tag('li', link))), + ]) + ); + + default: + return html.blank(); + } + }, +}; diff --git a/src/content/dependencies/generateListenLineOrList.js b/src/content/dependencies/generateListenLineOrList.js new file mode 100644 index 00000000..f0d8dae9 --- /dev/null +++ b/src/content/dependencies/generateListenLineOrList.js @@ -0,0 +1,145 @@ +import {isExternalLinkContext} from '#external-links'; +import {empty, unique} from '#sugar'; + +function getReleaseContext(urlString, { + _artistURLs, + albumArtistURLs, +}) { + const composerBandcampDomains = + albumArtistURLs + .filter(url => url.hostname.endsWith('.bandcamp.com')) + .map(url => url.hostname); + + const url = new URL(urlString); + + if (url.hostname === 'homestuck.bandcamp.com') { + return ['officialRelease']; + } + + if (composerBandcampDomains.includes(url.hostname)) { + return ['composerRelease']; + } + + return []; +} + +export default { + query(thing) { + const query = {}; + + query.album = + (thing.album + ? thing.album + : thing); + + query.urls = + (!empty(thing.urls) + ? thing.urls + : thing.album && + thing.album.style === 'single' && + thing.album.tracks[0] === thing + ? thing.album.urls + : []); + + query.artists = + thing.artistContribs + .map(contrib => contrib.artist); + + query.artistGroups = + query.artists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + query.albumArtists = + query.album.artistContribs + .map(contrib => contrib.artist); + + query.albumArtistGroups = + query.albumArtists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + return query; + }, + + relations: (relation, query, _thing) => ({ + externalLinksLineOrList: + relation('generateExternalLinksLineOrList', query.urls), + }), + + data(query, thing) { + const data = {}; + + data.name = thing.name; + + data.noLinks = empty(query.urls); + + const artistURLs = + unique([ + ...query.artists.flatMap(artist => artist.urls), + ...query.artistGroups.flatMap(group => group.urls), + ]).map(entry => new URL(entry.url)); + + const albumArtistURLs = + unique([ + ...query.albumArtists.flatMap(artist => artist.urls), + ...query.albumArtistGroups.flatMap(group => group.urls), + ]).map(entry => new URL(entry.url)); + + const boundGetReleaseContext = urlString => + getReleaseContext(urlString, { + artistURLs, + albumArtistURLs, + }); + + let releaseContexts = + query.urls.map(({url}) => boundGetReleaseContext(url)); + + const albumReleaseContexts = + query.album.urls.map(({url}) => boundGetReleaseContext(url)); + + const presentReleaseContexts = + unique(releaseContexts.filter(Boolean)); + + const presentAlbumReleaseContexts = + unique(albumReleaseContexts.filter(Boolean)); + + if ( + presentReleaseContexts.length <= 1 && + presentAlbumReleaseContexts.length <= 1 + ) { + releaseContexts = + query.urls.map(() => []); + } + + data.releaseContexts = releaseContexts; + + return data; + }, + + slots: { + visibleWithoutLinks: { + type: 'boolean', + default: false, + }, + + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('releaseInfo.listenOn', capsule => + (data.noLinks && slots.visibleWithoutLinks + ? language.$(capsule, 'noLinks', { + name: + html.tag('i', data.name), + }) + + : relations.externalLinksLineOrList.slots({ + string: capsule, + context: slots.context, + contexts: data.releaseContexts, + }))), +}; diff --git a/src/content/dependencies/generateReleaseInfoBlock.js b/src/content/dependencies/generateReleaseInfoBlock.js new file mode 100644 index 00000000..93d889ab --- /dev/null +++ b/src/content/dependencies/generateReleaseInfoBlock.js @@ -0,0 +1,64 @@ +import {empty} from '#sugar'; + +export default { + slots: { + // This isn't mutable, but we will be inspecting items' contents. + items: {validate: v => v.looseArrayOf(v.isHTML)}, + }, + + generate(slots, {html}) { + const tags = []; + + let paragraphLines = []; + const closeParagraph = () => { + if (empty(paragraphLines)) return; + + const paragraph = + html.tag('p', + {[html.joinChildren]: html.tag('br')}, + {[html.onlyIfContent]: true}, + paragraphLines); + + tags.push(paragraph); + paragraphLines = []; + }; + + for (let item of slots.items) { + item = html.Template.resolve(item); + + if (typeof item === 'string' && item.length) { + paragraphLines.push(item); + continue; + } + + if (html.isBlank(item)) { + continue; + } + + if (item.contentOnly) { + paragraphLines.push(item); + continue; + } + + if (item.tagName === 'br') { + continue; + } + + if (item.tagName === 'p') { + paragraphLines.push(item.content); + continue; + } + + closeParagraph(); + tags.push(item); + } + + closeParagraph(); + + if (empty(tags)) { + return html.blank(); + } else { + return html.tags(tags); + } + } +}; diff --git a/src/content/dependencies/generateReleaseInfoListenLine.js b/src/content/dependencies/generateReleaseInfoListenLine.js deleted file mode 100644 index 54e7985f..00000000 --- a/src/content/dependencies/generateReleaseInfoListenLine.js +++ /dev/null @@ -1,157 +0,0 @@ -import {isExternalLinkContext} from '#external-links'; -import {empty, stitchArrays, unique} from '#sugar'; - -function getReleaseContext(urlString, { - _artistURLs, - albumArtistURLs, -}) { - const composerBandcampDomains = - albumArtistURLs - .filter(url => url.hostname.endsWith('.bandcamp.com')) - .map(url => url.hostname); - - const url = new URL(urlString); - - if (url.hostname === 'homestuck.bandcamp.com') { - return 'officialRelease'; - } - - if (composerBandcampDomains.includes(url.hostname)) { - return 'composerRelease'; - } - - return null; -} - -export default { - query(thing) { - const query = {}; - - query.album = - (thing.album - ? thing.album - : thing); - - query.urls = - (!empty(thing.urls) - ? thing.urls - : thing.album && - thing.album.style === 'single' && - thing.album.tracks[0] === thing - ? thing.album.urls - : []); - - query.artists = - thing.artistContribs - .map(contrib => contrib.artist); - - query.artistGroups = - query.artists - .flatMap(artist => artist.closelyLinkedGroups) - .map(({group}) => group); - - query.albumArtists = - query.album.artistContribs - .map(contrib => contrib.artist); - - query.albumArtistGroups = - query.albumArtists - .flatMap(artist => artist.closelyLinkedGroups) - .map(({group}) => group); - - return query; - }, - - relations: (relation, query, _thing) => ({ - links: - query.urls - .map(entry => relation('linkExternal', entry)), - }), - - data(query, thing) { - const data = {}; - - data.name = thing.name; - - const artistURLs = - unique([ - ...query.artists.flatMap(artist => artist.urls), - ...query.artistGroups.flatMap(group => group.urls), - ]).map(entry => new URL(entry.url)); - - const albumArtistURLs = - unique([ - ...query.albumArtists.flatMap(artist => artist.urls), - ...query.albumArtistGroups.flatMap(group => group.urls), - ]).map(entry => new URL(entry.url)); - - const boundGetReleaseContext = urlString => - getReleaseContext(urlString, { - artistURLs, - albumArtistURLs, - }); - - let releaseContexts = - query.urls.map(({url}) => boundGetReleaseContext(url)); - - const albumReleaseContexts = - query.album.urls.map(({url}) => boundGetReleaseContext(url)); - - const presentReleaseContexts = - unique(releaseContexts.filter(Boolean)); - - const presentAlbumReleaseContexts = - unique(albumReleaseContexts.filter(Boolean)); - - if ( - presentReleaseContexts.length <= 1 && - presentAlbumReleaseContexts.length <= 1 - ) { - releaseContexts = - query.urls.map(() => null); - } - - data.releaseContexts = releaseContexts; - - return data; - }, - - slots: { - visibleWithoutLinks: { - type: 'boolean', - default: false, - }, - - context: { - validate: () => isExternalLinkContext, - default: 'generic', - }, - }, - - generate: (data, relations, slots, {html, language}) => - language.encapsulate('releaseInfo.listenOn', capsule => - (empty(relations.links) && slots.visibleWithoutLinks - ? language.$(capsule, 'noLinks', { - name: - html.tag('i', data.name), - }) - - : language.$('releaseInfo.listenOn', { - [language.onlyIfOptions]: ['links'], - - links: - language.formatDisjunctionList( - stitchArrays({ - link: relations.links, - releaseContext: data.releaseContexts, - }).map(({link, releaseContext}) => - link.slot('context', [ - ... - (Array.isArray(slots.context) - ? slots.context - : [slots.context]), - - releaseContext, - ]))), - }))), -}; diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js index 0207e574..001f5a54 100644 --- a/src/content/dependencies/generateTrackReleaseInfo.js +++ b/src/content/dependencies/generateTrackReleaseInfo.js @@ -1,22 +1,21 @@ import {compareArrays} from '#sugar'; export default { - relations(relation, track) { - const relations = {}; + relations: (relation, track) => ({ + block: + relation('generateReleaseInfoBlock'), - relations.artistContributionsLine = + artistContributionsLine: relation('generateReleaseInfoContributionsLine', track.artistContribs, - track.artistText); + track.artistText), - relations.listenLine = - relation('generateReleaseInfoListenLine', track); + listenLineOrList: + relation('generateListenLineOrList', track), - relations.albumLink = - relation('linkAlbum', track.album); - - return relations; - }, + albumLink: + relation('linkAlbum', track.album), + }), data(track) { const data = {}; @@ -48,43 +47,38 @@ export default { generate: (data, relations, {html, language}) => language.encapsulate('releaseInfo', capsule => html.tags([ - html.tag('p', - {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, - - [ - language.encapsulate(capsule, 'by', capsule => { - const withAlbum = - (data.showAlbum ? '.withAlbum' : ''); - - const albumOptions = - (data.showAlbum ? {album: relations.albumLink} : {}); - - return relations.artistContributionsLine.slots({ - stringKey: capsule + withAlbum, - featuringStringKey: capsule + '.featuring' + withAlbum, - - additionalStringOptions: albumOptions, - - chronologyKind: 'track', - }); - }), - - language.$(capsule, 'released', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.date), - }), - - language.$(capsule, 'duration', { - [language.onlyIfOptions]: ['duration'], - duration: language.formatDuration(data.duration), - }), - ]), - - html.tag('p', - relations.listenLine.slots({ - visibleWithoutLinks: true, - context: ['track'], - })), + relations.block.slot('items', [ + language.encapsulate(capsule, 'by', capsule => { + const withAlbum = + (data.showAlbum ? '.withAlbum' : ''); + + const albumOptions = + (data.showAlbum ? {album: relations.albumLink} : {}); + + return relations.artistContributionsLine.slots({ + stringKey: capsule + withAlbum, + featuringStringKey: capsule + '.featuring' + withAlbum, + + additionalStringOptions: albumOptions, + + chronologyKind: 'track', + }); + }), + + language.$(capsule, 'released', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.date), + }), + + language.$(capsule, 'duration', { + [language.onlyIfOptions]: ['duration'], + duration: language.formatDuration(data.duration), + }), + ]), + + relations.listenLineOrList.slots({ + visibleWithoutLinks: true, + context: 'track', + }), ])), }; -- cgit 1.3.0-6-gf8a5