From 8a7cb1edff25ba3e612d7c24b07cc776ff8738d6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Jan 2021 00:15:37 -0400 Subject: okay so like, hear me out here this commit isnt QUITE done but its in a working state, and i just had the scariest vision of accidentally discarding all my work via git mishap, yknow? even though im not doing anything funky with git! so yall get this commit early and its goin on line but im not pushing it to the site til its done. no spoilering yourself (even though ive already posted most of the cool things in the discord) <3 --- common/common.js | 20 +- publish.sh | 1 + static/client.js | 95 +++--- static/site.css | 142 ++++++--- upd8-util.js | 143 ++++++++- upd8.js | 946 +++++++++++++++++++++++++++++++++++++++++-------------- 6 files changed, 1014 insertions(+), 333 deletions(-) diff --git a/common/common.js b/common/common.js index 6c21dfc..5db5ad9 100644 --- a/common/common.js +++ b/common/common.js @@ -79,6 +79,7 @@ const C = { CHANGELOG_DIRECTORY: 'changelog', FLASH_DIRECTORY: 'flash', NEWS_DIRECTORY: 'news', + GROUP_DIRECTORY: 'group', JS_DISABLED_DIRECTORY: 'js-disabled', UNRELEASED_TRACKS_DIRECTORY: 'unreleased-tracks', @@ -123,22 +124,9 @@ const C = { // "directories", we just reformat the artist's name. getArtistDirectory: artistName => C.getKebabCase(artistName), - getThingsArtistContributedTo: (artistName, {allTracks, albumData, flashData}) => [ - ...allTracks.filter(track => [ - ...track.artists, - ...track.contributors, - ...track.coverArtists || [] - ].some(({ who }) => who === artistName)), - ...flashData.filter(flash => (flash.contributors || []).some(({ who }) => who === artistName)), - ...albumData.filter(album => - (album.coverArtists || []).some(({ who }) => who === artistName)) - ], - - getArtistNumContributions: (artistName, {allTracks, albumData, flashData}) => ( - C.getThingsArtistContributedTo(artistName, {allTracks, albumData, flashData}).length - ), - - getArtistCommentary: (artistName, {justEverythingMan}) => justEverythingMan.filter(thing => thing.commentary && thing.commentary.replace(/<\/?b>/g, '').includes('' + artistName + ':')) + getArtistNumContributions: artist => (artist.tracks.length + artist.albums.length + artist.flashes.length), + + getArtistCommentary: (artist, {justEverythingMan}) => justEverythingMan.filter(thing => thing.commentary && thing.commentary.replace(/<\/?b>/g, '').includes('' + artist.name + ':')) }; if (typeof module === 'object') { diff --git a/publish.sh b/publish.sh index 4021b9b..5a3cacd 100755 --- a/publish.sh +++ b/publish.sh @@ -4,4 +4,5 @@ # So, don't even try, if you aren't. # 8ut you can tweak it to your own server if that's your vi8e. +node upd8.js --all rsync -avhL site/ --info=progress2 nebula@ed1.club:/home/nebula/hsmusic diff --git a/static/client.js b/static/client.js index 8247a42..549fde2 100644 --- a/static/client.js +++ b/static/client.js @@ -5,10 +5,10 @@ 'use strict'; -const officialAlbumData = albumData.filter(album => !album.isFanon); -const fandomAlbumData = albumData.filter(album => album.isFanon); -const artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name); -const allTracks = C.getAllTracks(albumData); +let albumData, artistData, flashData; +let officialAlbumData, fandomAlbumData, artistNames; + +let ready = false; function rebase(href) { const relative = document.documentElement.dataset.rebase; @@ -27,6 +27,10 @@ function cssProp(el, key) { return getComputedStyle(el).getPropertyValue(key).trim(); } +function getRefDirectory(ref) { + return ref.split(':')[1]; +} + function getAlbum(el) { const directory = cssProp(el, '--album-directory'); return albumData.find(album => album.directory === directory); @@ -37,16 +41,16 @@ function getFlash(el) { return flashData.find(flash => flash.directory === directory); } -function openAlbum(album) { - return rebase(`${C.ALBUM_DIRECTORY}/${album.directory}/`); +function openAlbum(directory) { + return rebase(`${C.ALBUM_DIRECTORY}/${directory}/`); } -function openTrack(track) { - return rebase(`${C.TRACK_DIRECTORY}/${track.directory}/`); +function openTrack(directory) { + return rebase(`${C.TRACK_DIRECTORY}/${directory}/`); } -function openArtist(artist) { - return rebase(`${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist)}/`); +function openArtist(directory) { + return rebase(`${C.ARTIST_DIRECTORY}/${directory}/`); } function openFlash(flash) { @@ -76,20 +80,6 @@ function getTrackListAndIndex() { return {list: album.tracks, index: trackIndex}; } -function openNextTrack() { - const { list, index } = getTrackListAndIndex(); - if (!list) return; - if (index === list.length) return; - return openTrack(list[index + 1]); -} - -function openPreviousTrack() { - const { list, index } = getTrackListAndIndex(); - if (!list) return; - if (index === 0) return; - return openTrack(list[index - 1]); -} - function openRandomTrack() { const { list } = getTrackListAndIndex(); if (!list) return; @@ -104,33 +94,26 @@ function getFlashListAndIndex() { return {list, index: flashIndex}; } -function openNextFlash() { - const { list, index } = getFlashListAndIndex(); - if (index === list.length) return; - return openFlash(list[index + 1]); -} - -function openPreviousFlash() { - const { list, index } = getFlashListAndIndex(); - if (index === 0) return; - return openFlash(list[index - 1]); -} - for (const a of document.body.querySelectorAll('[data-random]')) { a.addEventListener('click', evt => { + if (!ready) { + evt.preventDefault(); + return; + } + setTimeout(() => { a.href = rebase(C.JS_DISABLED_DIRECTORY); }); switch (a.dataset.random) { - case 'album': return a.href = openAlbum(pick(albumData)); - case 'album-in-fandom': return a.href = openAlbum(pick(fandomAlbumData)); - case 'album-in-official': return a.href = openAlbum(pick(officialAlbumData)); - case 'track': return a.href = openTrack(pick(allTracks)); - case 'track-in-album': return a.href = openTrack(pick(getAlbum(a).tracks)); - case 'track-in-fandom': return a.href = openTrack(pick(fandomAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))); - case 'track-in-official': return a.href = openTrack(pick(officialAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))); - case 'artist': return a.href = openArtist(pick(artistNames)); - case 'artist-more-than-one-contrib': return a.href = openArtist(pick(artistNames.filter(name => C.getArtistNumContributions(name, {albumData, allTracks, flashData}) > 1))); + case 'album': return a.href = openAlbum(pick(albumData).directory); + case 'album-in-fandom': return a.href = openAlbum(pick(fandomAlbumData).directory); + case 'album-in-official': return a.href = openAlbum(pick(officialAlbumData).directory); + case 'track': return a.href = openTrack(getRefDirectory(pick(albumData.map(a => a.tracks).reduce((a, b) => a.concat(b), [])))); + case 'track-in-album': return a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks))); + case 'track-in-fandom': return a.href = openTrack(getRefDirectory(pick(fandomAlbumData.reduce((acc, album) => acc.concat(album.tracks), [])))); + case 'track-in-official': return a.href = openTrack(getRefDirectory(pick(officialAlbumData.reduce((acc, album) => acc.concat(album.tracks), [])))); + case 'artist': return a.href = openArtist(pick(artistData).directory); + case 'artist-more-than-one-contrib': return a.href = openArtist(pick(artistData.filter(artist => C.getArtistNumContributions(artist) > 1)).directory); } }); } @@ -159,7 +142,7 @@ document.addEventListener('keypress', event => { } else if (event.charCode === 'P'.charCodeAt(0)) { if (previous) previous.click(); } else if (event.charCode === 'R'.charCodeAt(0)) { - if (random) random.click(); + if (random && ready) random.click(); } } }); @@ -173,3 +156,23 @@ for (const reveal of document.querySelectorAll('.reveal')) { } }); } + +const elements1 = document.getElementsByClassName('js-hide-once-data'); +const elements2 = document.getElementsByClassName('js-show-once-data'); + +for (const element of elements1) element.style.display = 'block'; + +fetch(rebase('data.json')).then(data => data.json()).then(data => { + albumData = data.albumData; + artistData = data.artistData; + flashData = data.flashData; + + officialAlbumData = albumData.filter(album => !album.isFanon); + fandomAlbumData = albumData.filter(album => album.isFanon); + artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name); + + for (const element of elements1) element.style.display = 'none'; + for (const element of elements2) element.style.display = 'block'; + + ready = true; +}); diff --git a/static/site.css b/static/site.css index 016f74c..90f8ed3 100644 --- a/static/site.css +++ b/static/site.css @@ -15,14 +15,37 @@ } body { - background-color: var(--bg-color); - --bg-shade: calc(255 * var(--theme)); --fg-shade: calc(255 * (1 - var(--theme))); + background: black; + margin: 10px; + overflow-y: scroll; +} + +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + + background-image: url("https://www.homestuck.com/images/desktops/johnhouse_1920x1080.jpg"); + background-position: center; + background-size: cover; + opacity: 0.5; +} +#page-container { + background-color: var(--bg-color); color: rgb(var(--fg-shade), var(--fg-shade), var(--fg-shade)); + max-width: 1200px; + margin: 10px auto 50px; padding: 15px; + + box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); } a { @@ -34,14 +57,36 @@ a:hover { text-decoration: underline; } +#skippers { + position: absolute; + left: -100px; + top: auto; + width: 1px; + height: 1px; +} + +#skippers:focus-within { + position: static; + width: unset; + height: unset; +} + +#skippers > .skipper:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; +} + .layout-columns { display: flex; } -#header { +#header, #skippers { margin-bottom: 10px; padding: 5px; font-size: 0.85em; +} + +#header { display: flex; } @@ -122,7 +167,7 @@ a:hover { } @media (max-width: 780px) { - #sidebar:not(.no-hide) { + .sidebar:not(.no-hide) { display: none; } @@ -134,7 +179,7 @@ a:hover { margin-bottom: 10px; } - #sidebar.no-hide { + .sidebar.no-hide { max-width: unset !important; flex-basis: unset !important; margin-right: 0; @@ -147,25 +192,43 @@ a:hover { } } -#sidebar, #content, #header { +.sidebar, #content, #header, #skippers { background-color: rgba(var(--bg-shade), var(--bg-shade), var(--bg-shade), 0.6); border: 1px dotted var(--fg-color); border-radius: 3px; } -#sidebar { +.sidebar-column { flex: 1 1 20%; min-width: 150px; max-width: 250px; flex-basis: 250px; - float: left; + height: 100%; +} + +.sidebar-multiple { + display: flex; + flex-direction: column; +} + +.sidebar-multiple .sidebar:not(:first-child) { + margin-top: 10px; +} + +.sidebar { padding: 5px; - margin-right: 10px; font-size: 0.85em; - height: 100%; } -#sidebar.wide { +#sidebar-left { + margin-right: 10px; +} + +#sidebar-right { + margin-left: 10px; +} + +.sidebar.wide { max-width: 350px; flex-basis: 300px; flex-shrink: 0; @@ -178,23 +241,23 @@ a:hover { flex-shrink: 3; } -#sidebar > h1, -#sidebar > h2, -#sidebar > h3, -#sidebar > p { +.sidebar > h1, +.sidebar > h2, +.sidebar > h3, +.sidebar > p { text-align: center; } -#sidebar h1 { +.sidebar h1 { font-size: 1.25em; } -#sidebar h2 { +.sidebar h2 { font-size: 1.1em; margin: 0; } -#sidebar h3 { +.sidebar h3 { font-size: 1.1em; font-style: oblique; font-variant: small-caps; @@ -202,73 +265,70 @@ a:hover { margin-bottom: 0em; } -#sidebar > p { +.sidebar > p { margin: 0.5em 0; + padding: 0 5px; } -#sidebar p:last-child { - margin-bottom: 0; -} - -#sidebar hr { +.sidebar hr { color: #555; margin: 10px 5px; } -#sidebar > ol, #sidebar > ul { +.sidebar > ol, .sidebar > ul { padding-left: 30px; padding-right: 15px; } -#sidebar > dl { +.sidebar > dl { padding-right: 15px; padding-left: 0; } -#sidebar > dl dt { +.sidebar > dl dt { padding-left: 10px; margin-top: 0.5em; } -#sidebar > dl dt.current { +.sidebar > dl dt.current { font-weight: 800; } -#sidebar > dl dd { +.sidebar > dl dd { margin-left: 0; } -#sidebar > dl dd ul { +.sidebar > dl dd ul { padding-left: 30px; margin-left: 0; } -#sidebar > dl .side { +.sidebar > dl .side { padding-left: 10px; } -#sidebar li.current { +.sidebar li.current { font-weight: 800; } -#sidebar li { +.sidebar li { overflow-wrap: break-word; } -#sidebar article { +.sidebar article { text-align: left; margin: 5px 5px 15px 5px; } -#sidebar article:last-child { +.sidebar article:last-child { margin-bottom: 5px; } -#sidebar article h2 { +.sidebar article h2 { border-bottom: 1px dotted white; } -#sidebar article h2 time { +.sidebar article h2 time { float: right; font-weight: normal; } @@ -320,7 +380,9 @@ img { */ } -.js-hide { +.js-hide, +.js-show-once-data, +.js-hide-once-data { display: none; } @@ -596,6 +658,10 @@ dl ul, dl ol { margin-left: 0; } +.group-chronology-link { + font-style: oblique; +} + hr.split::before { content: "(split)"; color: #808080; diff --git a/upd8-util.js b/upd8-util.js index b24b3b7..28504ea 100644 --- a/upd8-util.js +++ b/upd8-util.js @@ -62,7 +62,11 @@ module.exports.progressPromiseAll = function (msg, array) { }))); }; -module.exports.queue = function (array, max = 10) { +module.exports.queue = function (array, max = 50) { + if (max === 0) { + return array.map(fn => fn()); + } + const begin = []; let current = 0; const ret = array.map(fn => new Promise((resolve, reject) => { @@ -155,3 +159,140 @@ decorateTime.displayTime = function() { }; module.exports.decorateTime = decorateTime; + +// Stolen as #@CK from mtui! +const parseOptions = async function(options, optionDescriptorMap) { + // This function is sorely lacking in comments, but the basic usage is + // as such: + // + // options is the array of options you want to process; + // optionDescriptorMap is a mapping of option names to objects that describe + // the expected value for their corresponding options. + // Returned is a mapping of any specified option names to their values, or + // a process.exit(1) and error message if there were any issues. + // + // Here are examples of optionDescriptorMap to cover all the things you can + // do with it: + // + // optionDescriptorMap: { + // 'telnet-server': {type: 'flag'}, + // 't': {alias: 'telnet-server'} + // } + // + // options: ['t'] -> result: {'telnet-server': true} + // + // optionDescriptorMap: { + // 'directory': { + // type: 'value', + // validate(name) { + // // const whitelistedDirectories = ['apple', 'banana'] + // if (whitelistedDirectories.includes(name)) { + // return true + // } else { + // return 'a whitelisted directory' + // } + // } + // }, + // 'files': {type: 'series'} + // } + // + // ['--directory', 'apple'] -> {'directory': 'apple'} + // ['--directory', 'artichoke'] -> (error) + // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']} + // + // TODO: Be able to validate the values in a series option. + + const handleDashless = optionDescriptorMap[parseOptions.handleDashless]; + const handleUnknown = optionDescriptorMap[parseOptions.handleUnknown]; + const result = Object.create(null); + for (let i = 0; i < options.length; i++) { + const option = options[i]; + if (option.startsWith('--')) { + // --x can be a flag or expect a value or series of values + let name = option.slice(2).split('=')[0]; // '--x'.split('=') = ['--x'] + let descriptor = optionDescriptorMap[name]; + if (!descriptor) { + if (handleUnknown) { + handleUnknown(option); + } else { + console.error(`Unknown option name: ${name}`); + process.exit(1); + } + continue; + } + if (descriptor.alias) { + name = descriptor.alias; + descriptor = optionDescriptorMap[name]; + } + if (descriptor.type === 'flag') { + result[name] = true; + } else if (descriptor.type === 'value') { + let value = option.slice(2).split('=')[1]; + if (!value) { + value = options[++i]; + if (!value || value.startsWith('-')) { + value = null; + } + } + if (!value) { + console.error(`Expected a value for --${name}`); + process.exit(1); + } + result[name] = value; + } else if (descriptor.type === 'series') { + if (!options.slice(i).includes(';')) { + console.error(`Expected a series of values concluding with ; (\\;) for --${name}`); + process.exit(1); + } + const endIndex = i + options.slice(i).indexOf(';'); + result[name] = options.slice(i + 1, endIndex); + i = endIndex; + } + if (descriptor.validate) { + const validation = await descriptor.validate(result[name]); + if (validation !== true) { + console.error(`Expected ${validation} for --${name}`); + process.exit(1); + } + } + } else if (option.startsWith('-')) { + // mtui doesn't use any -x=y or -x y format optionuments + // -x will always just be a flag + let name = option.slice(1); + let descriptor = optionDescriptorMap[name]; + if (!descriptor) { + if (handleUnknown) { + handleUnknown(option); + } else { + console.error(`Unknown option name: ${name}`); + process.exit(1); + } + continue; + } + if (descriptor.alias) { + name = descriptor.alias; + descriptor = optionDescriptorMap[name]; + } + if (descriptor.type === 'flag') { + result[name] = true; + } else { + console.error(`Use --${name} (value) to specify ${name}`); + process.exit(1); + } + } else if (handleDashless) { + handleDashless(option); + } + } + return result; +} + +parseOptions.handleDashless = Symbol(); +parseOptions.handleUnknown = Symbol(); + +module.exports.parseOptions = parseOptions; + +// Cheap FP for a cheap dyke! +// I have no idea if this is what curry actually means. +module.exports.curry = f => x => (...args) => f(x, ...args); + +module.exports.mapInPlace = (array, fn) => array.splice(0, array.length, ...array.map(fn)); diff --git a/upd8.js b/upd8.js index 86aeb53..64a311c 100644 --- a/upd8.js +++ b/upd8.js @@ -51,6 +51,14 @@ // // Use these wisely, which is to say all the time and instead of whatever // terri8le new pseudo structure you're trying to invent!!!!!!!! +// +// Upd8 2021-01-03: Soooooooo we didn't actually really end up using these, +// lol? Well there's still only one anyway. Kinda ended up doing a 8ig refactor +// of all the o8ject structures today. It's not *especially* relevant 8ut feels +// worth mentioning? I'd get rid of this comment 8lock 8ut I like it too much! +// Even though I haven't actually reread it, lol. 8ut yeah, hopefully in the +// spirit of this "make things more consistent" attitude I 8rought up 8ack in +// August, stuff's lookin' 8etter than ever now. W00t! 'use strict'; @@ -94,8 +102,11 @@ const unlink = util.promisify(fs.unlink); const { cacheOneArg, + curry, decorateTime, joinNoOxford, + mapInPlace, + parseOptions, progressPromiseAll, queue, s, @@ -110,9 +121,6 @@ const SITE_TITLE = 'Homestuck Music Wiki'; const SITE_SHORT_TITLE = 'HSMusic'; const SITE_DESCRIPTION = `Expansive resource for anyone interested in fan-made and official Homestuck music alike; an archive for all things related.`; -const SITE_VERSION = 'launch of hsmusic.wiki'; -const SITE_RELEASE = '12 December 2020'; - const SITE_DONATE_LINK = 'https://liberapay.com/nebula'; function readDataFile(file) { @@ -133,6 +141,7 @@ const ARTIST_DATA_FILE = 'artists.txt'; const FLASH_DATA_FILE = 'flashes.txt'; const NEWS_DATA_FILE = 'news.txt'; const TAG_DATA_FILE = 'tags.txt'; +const GROUP_DATA_FILE = 'groups.txt'; const CSS_FILE = 'site.css'; @@ -143,10 +152,11 @@ const CSS_FILE = 'site.css'; // Upd8: Okay yeah these aren't actually any different. Still cleaner than // passing around a data object containing all this, though. let albumData; -let allTracks; +let trackData; let flashData; let newsData; let tagData; +let groupData; let artistNames; let artistData; @@ -155,6 +165,9 @@ let officialAlbumData; let fandomAlbumData; let justEverythingMan; // tracks, albums, flashes -- don't forget to upd8 getHrefOfAnythingMan! let justEverythingSortedByArtDateMan; +let contributionData; + +let queueSize; // Note there isn't a 'find track data files' function. I plan on including the // data for all tracks within an al8um collected in the single metadata file @@ -312,7 +325,7 @@ function transformInline(text) { return ref; } } else if (category === 'tag:') { - const tag = tagData.find(tag => tag.directory === ref); + const tag = getLinkedTag(ref); if (tag) { return fixWS` ${tag.name} @@ -481,7 +494,8 @@ async function processAlbumDataFile(file) { album.trackCoverArtists = getContributionField(albumSection, 'Track Art'); album.artTags = getListField(albumSection, 'Art Tags') || []; album.commentary = getCommentaryField(albumSection); - album.urls = (getListField(albumSection, 'URLs') || []).filter(Boolean); + album.urls = getListField(albumSection, 'URLs') || []; + album.groups = getListField(albumSection, 'Groups') || []; album.directory = getBasicField(albumSection, 'Directory'); const canon = getBasicField(albumSection, 'Canon'); @@ -490,6 +504,8 @@ async function processAlbumDataFile(file) { album.isOfficial = album.isCanon || album.isBeyond; album.isFanon = canon === 'Fanon'; + album.isMajorRelease = getBasicField(albumSection, 'Major Release') === 'yes'; + if (album.artists && album.artists.error) { return {error: `${album.artists.error} (in ${album.name})`}; } @@ -580,7 +596,7 @@ async function processAlbumDataFile(file) { track.aka = getBasicField(section, 'AKA'); if (!track.name) { - return {error: 'A track section is missing the "Track" (name) field (in ${album.name)}.'}; + return {error: `A track section is missing the "Track" (name) field (in ${album.name}, previous: ${album.tracks[album.tracks.length - 1]?.name}).`}; } let durationString = getBasicField(section, 'Duration') || '0:00'; @@ -676,12 +692,21 @@ async function processArtistDataFile(file) { const urls = (getListField(section, 'URLs') || []).filter(Boolean); const alias = getBasicField(section, 'Alias'); const note = getMultilineField(section, 'Note'); + let directory = getBasicField(section, 'Directory'); if (!name) { return {error: 'Expected "Artist" (name) field!'}; } - return {name, urls, alias, note}; + if (!directory) { + directory = C.getArtistDirectory(name); + } + + if (alias) { + return {name, directory, alias}; + } else { + return {name, directory, urls, note}; + } }); } @@ -837,6 +862,51 @@ async function processTagDataFile(file) { }); } +async function processGroupDataFile(file) { + let contents; + try { + contents = await readFile(file, 'utf-8'); + } catch (error) { + return {error: `Could not read ${file} (${error.code}).`}; + } + + const contentLines = contents.split('\n'); + const sections = Array.from(getSections(contentLines)); + + return sections.map(section => { + const name = getBasicField(section, 'Group'); + if (!name) { + return {error: 'Expected "Group" field!'}; + } + + let directory = getBasicField(section, 'Directory'); + if (!directory) { + directory = C.getKebabCase(name); + } + + let description = getMultilineField(section, 'Description'); + if (!description) { + return {error: 'Expected "Description" field!'}; + } + + let descriptionShort = description.split('
')[0]; + + description = transformMultiline(description); + descriptionShort = transformMultiline(descriptionShort); + + const urls = (getListField(section, 'URLs') || []).filter(Boolean); + + return { + name, + directory, + description, + descriptionShort, + urls, + color: '#00ffff' + }; + }); +} + function getDateString({ date }) { /* const pad = val => val.toString().padStart(2, '0'); @@ -885,28 +955,81 @@ function getTotalDuration(tracks) { const stringifyIndent = 0; +const toRefs = (label, array) => array.filter(Boolean).map(x => `${label}:${x.directory}`); + +function stringifyRefs(key, value) { + switch (key) { + case 'albums': return toRefs('album', value); + case 'tracks': + case 'references': + case 'referencedBy': + if (!Array.isArray(value)) console.log(Object.keys(value)); + return toRefs('track', value); + case 'artists': + case 'contributors': + case 'coverArtists': + case 'trackCoverArtists': + return value && value.map(({ who, what }) => ({who: `artist:${who.directory}`, what})); + case 'flashes': return toRefs('flash', value); + case 'groups': return toRefs('group', value); + case 'artTags': return toRefs('tag', value); + case 'aka': return value && `track:${value.directory}`; + default: + return value; + } +} + function stringifyAlbumData() { return JSON.stringify(albumData, (key, value) => { - if (['album', 'commentary'].includes(key)) { - return undefined; + switch (key) { + case 'commentary': + return ''; + default: + return stringifyRefs(key, value); } + }, stringifyIndent); +} - return value; +function stringifyTrackData() { + return JSON.stringify(trackData, (key, value) => { + switch (key) { + case 'album': + case 'commentary': + case 'otherReleases': + return undefined; + default: + return stringifyRefs(key, value); + } }, stringifyIndent); } function stringifyFlashData() { return JSON.stringify(flashData, (key, value) => { - if (['act', 'commentary'].includes(key)) { - return undefined; + switch (key) { + case 'act': + case 'commentary': + return undefined; + default: + return stringifyRefs(key, value); } - - return value; }, stringifyIndent); } function stringifyArtistData() { - return JSON.stringify(artistData, null, stringifyIndent); + return JSON.stringify(artistData, (key, value) => { + switch (key) { + case 'tracks': // skip stringifyRefs handling 'tracks' key as an array + return value; + case 'asAny': + return; + case 'asArtist': + case 'asContributor': + case 'asCoverArtist': + return toRefs('track', value); + default: + return stringifyRefs(key, value); + } + }, stringifyIndent); } function escapeAttributeValue(value) { @@ -996,11 +1119,16 @@ async function writePage(directoryParts, { main = { classes: [], + collapseSidebars: true, content: '' }, sidebar = { - collapse: true, + classes: [], + content: '' + }, + + sidebarRight = { classes: [], content: '' }, @@ -1021,26 +1149,51 @@ async function writePage(directoryParts, { } const canonical = SITE_CANONICAL_BASE + targetPath; + const { + collapseSidebars = true + } = main; + const mainHTML = main.content && fixWS`
${main.content}
`; - const { - collapse = true, + const generateSidebarHTML = (id, { + content, + multiple, + classes: sidebarClasses = [], wide = false - } = sidebar; - - const sidebarHTML = sidebar.content && fixWS` -