summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--package-lock.json2
-rw-r--r--src/misc-templates.js58
-rw-r--r--src/page/album.js3
-rw-r--r--src/page/artist.js10
-rw-r--r--src/page/homepage.js3
-rw-r--r--src/page/tag.js6
-rw-r--r--src/page/track.js2
-rw-r--r--src/static/site.css3
-rw-r--r--src/strings-default.json1
-rwxr-xr-xsrc/upd8.js285
-rw-r--r--src/url-spec.js9
-rw-r--r--src/util/find.js83
-rw-r--r--src/util/link.js31
-rw-r--r--src/util/replacer.js9
-rw-r--r--src/util/urls.js46
-rw-r--r--src/util/wiki-data.js8
17 files changed, 384 insertions, 179 deletions
diff --git a/README.md b/README.md
index a463f10..2a67c00 100644
--- a/README.md
+++ b/README.md
@@ -9,10 +9,10 @@ HSMusic, short for the *Homestuck Music Wiki*, is a revitalization and reimagini
* `src/upd8.js`: "Build" code for the site. Everything specific to generating the structure and HTML content of the website is conatined in this file. As expected, it's pretty massive, and is currently undergoing some much-belated restructuring.
* `src/static`: Static code and supporting files. Everything here is wholly client-side and referenced by the generated HTML files.
* `src/common`: Code which is depended upon by both client- and server-side code. For the most part, this is constants such as directory paths, though there are a few handy algorithms here too.
-* In the not quite so far past, we used to have `data` and `media` folders too. Today, for portability and convenience in project structure, those are saved in separate repositories, and you can pass hsmusic paths to them through the `--data` and `--media` options, or the `HSMUSIC_DATA` and `HSMUSIC_MEDIA` environment variables.
+* In the not quite so far past, we used to have `data` and `media` folders too. Today, for portability and convenience in project structure, those are saved in separate repositories, and you can pass hsmusic paths to them through the `--data-path` and `--media-path` options, or the `HSMUSIC_DATA` and `HSMUSIC_MEDIA` environment variables.
* Data directory: The majority of data files belonging to the wiki are here. If you were to, say, create a fork of hsmusic for some other music archival project, you'd want to change the files here. Data files are all a custom text format designed to be easy to edit, process, and maintain; they should be self-descriptive.
* Media directory: Images and other static files referenced by generated and static content across the site. Many of the files here are cover art, and their names match the automatically generated "kebab case" identifiers for tracks and albums (or a manually overridden one).
-* Same for the output root: previously it was in a `site` folder; today, use `--out` or `HSMUSIC_OUT`!
+* Same for the output root: previously it was in a `site` folder; today, use `--out-path` or `HSMUSIC_OUT`!
The upd8 code process was politely introduced by 2019!us back when we were beginning the site, and it's essentially the same structure followed today. In summary:
diff --git a/package-lock.json b/package-lock.json
index 155caeb..f97e7f0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,7 @@
"he": "^1.2.0"
},
"bin": {
- "hsmusic": "upd8/main.js"
+ "hsmusic": "src/upd8.js"
}
},
"node_modules/fix-whitespace": {
diff --git a/src/misc-templates.js b/src/misc-templates.js
index d4e4af3..578c4e5 100644
--- a/src/misc-templates.js
+++ b/src/misc-templates.js
@@ -45,6 +45,7 @@ export function getArtistString(artists, {
// Chronology links
export function generateChronologyLinks(currentThing, {
+ dateKey = 'date',
contribKey,
getThings,
headingString,
@@ -65,7 +66,7 @@ export function generateChronologyLinks(currentThing, {
}
return contributions.map(({ who: artist }) => {
- const things = sortByDate(unique(getThings(artist)));
+ const things = sortByDate(unique(getThings(artist)), dateKey);
const releasedThings = things.filter(thing => {
const album = albumData.includes(thing) ? thing : thing.album;
return !(album && album.directory === UNRELEASED_TRACKS_DIRECTORY);
@@ -183,7 +184,7 @@ export function getAlbumStylesheet(album, {to}) {
return [
album.wallpaperArtists && fixWS`
body::before {
- background-image: url("${to('media.albumWallpaper', album.directory)}");
+ background-image: url("${to('media.albumWallpaper', album.directory, album.wallpaperFileExtension)}");
${album.wallpaperStyle}
}
`,
@@ -198,8 +199,17 @@ export function getAlbumStylesheet(album, {to}) {
// Fancy lookin' links
export function fancifyURL(url, {strings, album = false} = {}) {
- const domain = new URL(url).hostname;
+ let local = Symbol();
+ let domain;
+ try {
+ domain = new URL(url).hostname;
+ } 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.)
+ domain = local;
+ }
return fixWS`<a href="${url}" class="nowrap">${
+ domain === local ? strings('misc.external.local') :
domain.includes('bandcamp.com') ? strings('misc.external.bandcamp') :
[
'music.solatrux.com'
@@ -260,45 +270,42 @@ export function iconifyURL(url, {strings, to}) {
// Grids
export function getGridHTML({
- getLinkThemeString,
img,
strings,
entries,
srcFn,
- hrefFn,
+ linkFn,
altFn = () => '',
detailsFn = null,
lazy = true
}) {
- return entries.map(({ large, item }, i) => html.tag('a',
+ return entries.map(({ large, item }, i) => linkFn(item,
{
class: ['grid-item', 'box', large && 'large-grid-item'],
- href: hrefFn(item),
- style: getLinkThemeString(item.color)
- },
- fixWS`
- ${img({
- src: srcFn(item),
- alt: altFn(item),
- thumb: 'small',
- lazy: (typeof lazy === 'number' ? i >= lazy : lazy),
- square: true,
- reveal: getRevealStringFromTags(item.artTags, {strings})
- })}
- <span>${item.name}</span>
- ${detailsFn && `<span>${detailsFn(item)}</span>`}
- `)).join('\n');
+ text: fixWS`
+ ${img({
+ src: srcFn(item),
+ alt: altFn(item),
+ thumb: 'small',
+ lazy: (typeof lazy === 'number' ? i >= lazy : lazy),
+ square: true,
+ reveal: getRevealStringFromTags(item.artTags, {strings})
+ })}
+ <span>${item.name}</span>
+ ${detailsFn && `<span>${detailsFn(item)}</span>`}
+ `
+ })).join('\n');
}
export function getAlbumGridHTML({
- getAlbumCover, getGridHTML, strings, to,
+ getAlbumCover, getGridHTML, link, strings,
details = false,
...props
}) {
return getGridHTML({
srcFn: getAlbumCover,
- hrefFn: album => to('localized.album', album.directory),
+ linkFn: link.album,
detailsFn: details && (album => strings('misc.albumGridDetails', {
tracks: strings.count.tracks(album.tracks.length, {unit: true}),
time: strings.count.duration(getTotalDuration(album.tracks))
@@ -308,15 +315,16 @@ export function getAlbumGridHTML({
}
export function getFlashGridHTML({
- getFlashCover, getGridHTML, to,
+ getFlashCover, getGridHTML, link,
...props
}) {
return getGridHTML({
srcFn: getFlashCover,
- hrefFn: flash => to('localized.flash', flash.directory),
+ linkFn: link.flash,
...props
});
}
+
// Nav-bar links
export function generateInfoGalleryLinks(currentThing, isGallery, {
diff --git a/src/page/album.js b/src/page/album.js
index adcc058..6e8d6db 100644
--- a/src/page/album.js
+++ b/src/page/album.js
@@ -122,7 +122,7 @@ export function write(album, {wikiData}) {
banner: album.bannerArtists && {
dimensions: album.bannerDimensions,
- path: ['media.albumBanner', album.directory],
+ path: ['media.albumBanner', album.directory, album.bannerFileExtension],
alt: strings('misc.alt.albumBanner'),
position: 'top'
},
@@ -394,6 +394,7 @@ export function generateAlbumChronologyLinks(album, currentTrack, {generateChron
}),
generateChronologyLinks(currentTrack || album, {
contribKey: 'coverArtists',
+ dateKey: 'coverArtDate',
getThings: artist => [...artist.albums.asCoverArtist, ...artist.tracks.asCoverArtist],
headingString: 'misc.chronology.heading.coverArt'
})
diff --git a/src/page/artist.js b/src/page/artist.js
index 695fddf..e6160be 100644
--- a/src/page/artist.js
+++ b/src/page/artist.js
@@ -51,7 +51,7 @@ export function write(artist, {wikiData}) {
key
});
- const artListChunks = chunkByProperties(artThingsAll.flatMap(thing =>
+ const artListChunks = chunkByProperties(sortByDate(artThingsAll.flatMap(thing =>
(['coverArtists', 'wallpaperArtists', 'bannerArtists']
.map(key => getArtistsAndContrib(thing, key))
.filter(({ contrib }) => contrib)
@@ -61,7 +61,7 @@ export function write(artist, {wikiData}) {
date: +(thing.coverArtDate || thing.date),
...props
})))
- ), ['date', 'album']);
+ )), ['date', 'album']);
const commentaryListChunks = chunkByProperties(commentaryThings.map(thing => ({
album: thing.album || thing,
@@ -452,9 +452,9 @@ export function write(artist, {wikiData}) {
srcFn: thing => (thing.album
? getTrackCover(thing)
: getAlbumCover(thing)),
- hrefFn: thing => (thing.album
- ? to('localized.track', thing.directory)
- : to('localized.album', thing.directory))
+ linkFn: (thing, opts) => (thing.album
+ ? link.track(thing, opts)
+ : link.album(thing, opts))
})}
</div>
`
diff --git a/src/page/homepage.js b/src/page/homepage.js
index 37ec442..e60256d 100644
--- a/src/page/homepage.js
+++ b/src/page/homepage.js
@@ -112,7 +112,8 @@ export function writeTargetless({wikiData}) {
link.newsIndex('', {text: strings('newsIndex.title'), to}),
wikiInfo.features.flashesAndGames &&
link.flashIndex('', {text: strings('flashIndex.title'), to}),
- ...staticPageData.filter(page => page.listed).map(link.staticPage)
+ ...staticPageData.filter(page => page.listed).map(page =>
+ link.staticPage(page, {text: page.shortName}))
].filter(Boolean).map(link => `<span>${link}</span>`).join('\n')}
</h2>
`
diff --git a/src/page/tag.js b/src/page/tag.js
index 4253120..791c713 100644
--- a/src/page/tag.js
+++ b/src/page/tag.js
@@ -53,9 +53,9 @@ export function write(tag, {wikiData}) {
srcFn: thing => (thing.album
? getTrackCover(thing)
: getAlbumCover(thing)),
- hrefFn: thing => (thing.album
- ? to('localized.track', thing.directory)
- : to('localized.album', thing.directory))
+ linkFn: (thing, opts) => (thing.album
+ ? link.track(thing, opts)
+ : link.album(thing, opts))
})}
</div>
`
diff --git a/src/page/track.js b/src/page/track.js
index 0941ee8..c2f2c59 100644
--- a/src/page/track.js
+++ b/src/page/track.js
@@ -155,7 +155,7 @@ export function write(track, {wikiData}) {
banner: album.bannerArtists && {
classes: ['dim'],
dimensions: album.bannerDimensions,
- path: ['media.albumBanner', album.directory],
+ path: ['media.albumBanner', album.directory, album.bannerFileExtension],
alt: strings('misc.alt.albumBanner'),
position: 'bottom'
},
diff --git a/src/static/site.css b/src/static/site.css
index 4ccbdc1..65d4d34 100644
--- a/src/static/site.css
+++ b/src/static/site.css
@@ -201,6 +201,9 @@ footer > :last-child {
.other-group-accent {
opacity: 0.7;
font-style: oblique;
+}
+
+.other-group-accent {
white-space: nowrap;
}
diff --git a/src/strings-default.json b/src/strings-default.json
index c5132c0..b80c99f 100644
--- a/src/strings-default.json
+++ b/src/strings-default.json
@@ -114,6 +114,7 @@
"misc.chronology.heading.flash": "{INDEX} flash/game by {ARTIST}",
"misc.chronology.heading.track": "{INDEX} track by {ARTIST}",
"misc.external.domain": "External ({DOMAIN})",
+ "misc.external.local": "Wiki Archive (local upload)",
"misc.external.bandcamp": "Bandcamp",
"misc.external.bandcamp.domain": "Bandcamp ({DOMAIN})",
"misc.external.deviantart": "DeviantArt",
diff --git a/src/upd8.js b/src/upd8.js
index e9b3d4e..6f538d1 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -186,7 +186,6 @@ import {
bindOpts,
call,
filterEmptyLines,
- mapInPlace,
queue,
splitArray,
unique,
@@ -225,14 +224,14 @@ const DEFAULT_STRINGS_FILE = 'strings-default.json';
//
// Rather than hard code it, anything in this directory can 8e shared across
// 8oth ends of the code8ase.
-// (This gets symlinked into the --data directory.)
+// (This gets symlinked into the --data-path directory.)
const UTILITY_DIRECTORY = 'util';
// Code that's used only in the static site! CSS, cilent JS, etc.
-// (This gets symlinked into the --data directory.)
+// (This gets symlinked into the --data-path directory.)
const STATIC_DIRECTORY = 'static';
-// Su8directory under provided --data directory for al8um files, which are
+// Su8directory under provided --data-path directory for al8um files, which are
// read from and processed to compose the majority of album and track data.
const DATA_ALBUM_DIRECTORY = 'album';
@@ -271,9 +270,13 @@ async function findFiles(dataPath, filter = f => true) {
.filter(file => filter(file));
}
+function splitLines(text) {
+ return text.split(/\r\n|\r|\n/);
+}
+
function* getSections(lines) {
// ::::)
- const isSeparatorLine = line => /^-{8,}$/.test(line);
+ const isSeparatorLine = line => /^-{8,}/.test(line);
yield* splitArray(lines, isSeparatorLine);
}
@@ -594,7 +597,7 @@ function transformMultiline(text, {
// interested in doing lol. sorry!!!
let inBlockquote = false;
- for (let line of text.split(/\r|\n|\r\n/)) {
+ for (let line of splitLines(text)) {
const imageLine = line.startsWith('<img');
line = line.replace(/<img (.*?)>/g, (match, attributes) => img({
lazy: true,
@@ -768,7 +771,7 @@ async function processAlbumDataFile(file) {
// We'll just return more specific errors if it's missing necessary data
// fields.
- const contentLines = contents.split('\n');
+ const contentLines = contents.split(/\r\n|\r|\n/);
// In this line of code I defeat the purpose of using a generator in the
// first place. Sorry!!!!!!!!
@@ -778,11 +781,18 @@ async function processAlbumDataFile(file) {
const album = {};
album.name = getBasicField(albumSection, 'Album');
+
+ if (!album.name) {
+ return {error: `The file "${path.relative(dataPath, file)}" is missing the "Album" field - maybe this is a misplaced file instead of album data?`};
+ }
+
album.artists = getContributionField(albumSection, 'Artists') || getContributionField(albumSection, 'Artist');
album.wallpaperArtists = getContributionField(albumSection, 'Wallpaper Art');
album.wallpaperStyle = getMultilineField(albumSection, 'Wallpaper Style');
+ album.wallpaperFileExtension = getBasicField(albumSection, 'Wallpaper File Extension') || 'jpg';
album.bannerArtists = getContributionField(albumSection, 'Banner Art');
album.bannerStyle = getMultilineField(albumSection, 'Banner Style');
+ album.bannerFileExtension = getBasicField(albumSection, 'Banner File Extension') || 'jpg';
album.bannerDimensions = getDimensionsField(albumSection, 'Banner Dimensions');
album.date = getBasicField(albumSection, 'Date');
album.trackArtDate = getBasicField(albumSection, 'Track Art Date') || album.date;
@@ -886,9 +896,19 @@ async function processAlbumDataFile(file) {
getBasicField(section, 'FG') ||
album.color
),
+ originalDate: getBasicField(section, 'Original Date'),
startIndex: trackIndex,
tracks: []
};
+ if (group.originalDate) {
+ if (isNaN(Date.parse(group.originalDate))) {
+ return {error: `The track group "${group.name}" has an invalid "Original Date" field: "${group.originalDate}"`};
+ }
+ group.originalDate = new Date(group.originalDate);
+ group.date = group.originalDate;
+ } else {
+ group.date = album.date;
+ }
if (album.trackGroups) {
album.trackGroups.push(group);
} else {
@@ -962,7 +982,11 @@ async function processAlbumDataFile(file) {
if (isNaN(Date.parse(track.originalDate))) {
return {error: `The track "${track.name}"'s has an invalid "Original Date" field: "${track.originalDate}"`};
}
+ track.originalDate = new Date(track.originalDate);
track.date = new Date(track.originalDate);
+ } else if (group && group.originalDate) {
+ track.originalDate = group.originalDate;
+ track.date = group.originalDate;
} else {
track.date = album.date;
}
@@ -1002,7 +1026,7 @@ async function processArtistDataFile(file) {
return {error: `Could not read ${file} (${error.code}).`};
}
- const contentLines = contents.split('\n');
+ const contentLines = splitLines(contents);
const sections = Array.from(getSections(contentLines));
return sections.filter(s => s.filter(Boolean).length).map(section => {
@@ -1037,7 +1061,7 @@ async function processFlashDataFile(file) {
return {error: `Could not read ${file} (${error.code}).`};
}
- const contentLines = contents.split('\n');
+ const contentLines = splitLines(contents);
const sections = Array.from(getSections(contentLines));
let act, color;
@@ -1097,7 +1121,7 @@ async function processNewsDataFile(file) {
return {error: `Could not read ${file} (${error.code}).`};
}
- const contentLines = contents.split('\n');
+ const contentLines = splitLines(contents);
const sections = Array.from(getSections(contentLines));
return sections.map(section => {
@@ -1151,7 +1175,7 @@ async function processTagDataFile(file) {
}
}
- const contentLines = contents.split('\n');
+ const contentLines = splitLines(contents);
const sections = Array.from(getSections(contentLines));
return sections.map(section => {
@@ -1197,7 +1221,7 @@ async function processGroupDataFile(file) {
}
}
- const contentLines = contents.split('\n');
+ const contentLines = splitLines(contents);
const sections = Array.from(getSections(contentLines));
let category, color;
@@ -1252,7 +1276,7 @@ async function processStaticPageDataFile(file) {
}
}
- const contentLines = contents.split('\n');
+ const contentLines = splitLines(contents);
const sections = Array.from(getSections(contentLines));
return sections.map(section => {
@@ -1299,7 +1323,7 @@ async function processWikiInfoFile(file) {
// Unlike other data files, the site info data file isn't 8roken up into
// more than one entry. So we operate on the plain old contentLines array,
// rather than dividing into sections like we usually do!
- const contentLines = contents.split('\n');
+ const contentLines = splitLines(contents);
const name = getBasicField(contentLines, 'Name');
if (!name) {
@@ -1362,7 +1386,7 @@ async function processHomepageInfoFile(file) {
return {error: `Could not read ${file} (${error.code}).`};
}
- const contentLines = contents.split('\n');
+ const contentLines = splitLines(contents);
const sections = Array.from(getSections(contentLines));
const [ firstSection, ...rowSections ] = sections;
@@ -1769,7 +1793,7 @@ writePage.html = (pageFn, {
footer.content ??= (wikiInfo.footer ? transformMultiline(wikiInfo.footer) : '');
const canonical = (wikiInfo.canonicalBase
- ? wikiInfo.canonicalBase + paths.pathname
+ ? wikiInfo.canonicalBase + (paths.pathname === '/' ? '' : paths.pathanme)
: '');
const collapseSidebars = (sidebarLeft.collapse !== false) && (sidebarRight.collapse !== false);
@@ -2000,8 +2024,8 @@ writePage.paths = (baseDirectory, fullKey, directory = '', {
const [ groupKey, subKey ] = fullKey.split('.');
const pathname = (groupKey === 'localized' && baseDirectory
- ? urls.from('shared.root').to('localizedWithBaseDirectory.' + subKey, baseDirectory, directory)
- : urls.from('shared.root').to(fullKey, directory));
+ ? urls.from('shared.root').toDevice('localizedWithBaseDirectory.' + subKey, baseDirectory, directory)
+ : urls.from('shared.root').toDevice(fullKey, directory));
// Needed for the rare directory which itself contains a slash, e.g. for
// listings, with directories like 'albums/by-name'.
@@ -2025,7 +2049,7 @@ function writeSymlinks() {
]);
async function link(directory, urlKey) {
- const pathname = urls.from('shared.root').to(urlKey);
+ const pathname = urls.from('shared.root').toDevice(urlKey);
const file = path.join(outputPath, pathname);
try {
await unlink(file);
@@ -2034,7 +2058,13 @@ function writeSymlinks() {
throw error;
}
}
- await symlink(path.resolve(directory), file);
+ try {
+ await symlink(path.resolve(directory), file);
+ } catch (error) {
+ if (error.code === 'EPERM') {
+ await symlink(path.resolve(directory), file, 'junction');
+ }
+ }
}
}
@@ -2209,12 +2239,20 @@ async function main() {
type: 'flag'
},
- // Only want 8uild one language during testing? This can chop down
+ // Only want to 8uild one language during testing? This can chop down
// 8uild times a pretty 8ig chunk! Just pass a single language code.
'lang': {
type: 'value'
},
+ // Working without a dev server and just using file:// URLs in your we8
+ // 8rowser? This will automatically append index.html to links across
+ // the site. Not recommended for production, since it isn't guaranteed
+ // 100% error-free (and index.html-style links are less pretty anyway).
+ 'append-index-html': {
+ type: 'flag'
+ },
+
'queue-size': {
type: 'value',
validate(size) {
@@ -2243,14 +2281,20 @@ async function main() {
errored = true;
}
};
- error(!dataPath, `Expected --data option or HSMUSIC_DATA to be set`);
- error(!mediaPath, `Expected --media option or HSMUSIC_MEDIA to be set`);
- error(!outputPath, `Expected --out option or HSMUSIC_OUT to be set`);
+ error(!dataPath, `Expected --data-path option or HSMUSIC_DATA to be set`);
+ error(!mediaPath, `Expected --media-path option or HSMUSIC_MEDIA to be set`);
+ error(!outputPath, `Expected --out-path option or HSMUSIC_OUT to be set`);
if (errored) {
return;
}
}
+ const appendIndexHTML = miscOptions['append-index-html'] ?? false;
+ if (appendIndexHTML) {
+ logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`;
+ unbound_link.globalOptions.appendIndexHTML = true;
+ }
+
const skipThumbs = miscOptions['skip-thumbs'] ?? false;
const thumbsOnly = miscOptions['thumbs-only'] ?? false;
@@ -2640,16 +2684,6 @@ async function main() {
}
}
- {
- for (const { references, name, album } of WD.trackData) {
- for (const ref of references) {
- if (!find.track(ref, {wikiData})) {
- logWarn`Track not found "${ref}" in ${name} (${album.name})`;
- }
- }
- }
- }
-
WD.contributionData = Array.from(new Set([
...WD.trackData.flatMap(track => [...track.artists || [], ...track.contributors || [], ...track.coverArtists || []]),
...WD.albumData.flatMap(album => [...album.artists || [], ...album.coverArtists || [], ...album.wallpaperArtists || [], ...album.bannerArtists || []]),
@@ -2662,68 +2696,142 @@ async function main() {
// more than once. (We 8uild a few additional links that can't 8e cre8ted
// at initial data processing time here too.)
- const filterNullArray = (parent, key) => {
- for (const obj of parent) {
- const array = obj[key];
- for (let i = 0; i < array.length; i++) {
- if (!array[i]) {
- const prev = array[i - 1] && array[i - 1].name;
- const next = array[i + 1] && array[i + 1].name;
- logWarn`Unexpected null in ${obj.name} (${obj.what}) (array key ${key} - prev: ${prev}, next: ${next})`;
+ const allContribSources = [];
+
+ // Collect all contrib data sources into one array, which will be processed
+ // later.
+ const collectContributors = function(thing, ...contribDataKeys) {
+ allContribSources.push(...contribDataKeys.map(key => ({
+ thing,
+ key,
+ data: thing[key]
+ })).filter(({ data }) => data?.length));
+ };
+
+ // Process in three parts:
+ // 1) collate all contrib data into one set (no duplicates)
+ // 2) convert every "who" contrib string into an actual artist object
+ // 3) filter each source (not the set!) by null who values
+ const postprocessContributors = function() {
+ const allContribData = new Set(allContribSources.flatMap(source => source.data));
+ const originalContribStrings = new Map();
+
+ for (const contrib of allContribData) {
+ originalContribStrings.set(contrib, contrib.who);
+ contrib.who = find.artist(contrib.who, {wikiData});
+ }
+
+ for (const { thing, key, data } of allContribSources) {
+ data.splice(0, data.length, ...data.filter(contrib => {
+ if (!contrib.who) {
+ const orig = originalContribStrings.get(contrib);
+ logWarn`Post-process: Contributor ${orig} didn't match any artist data - in ${thing.name} (key: ${key})`;
+ return false;
}
- }
- array.splice(0, array.length, ...array.filter(Boolean));
+ return true;
+ }));
}
};
- const filterNullValue = (parent, key) => {
- parent.splice(0, parent.length, ...parent.filter(obj => {
- if (!obj[key]) {
- logWarn`Unexpected null in ${obj.name} (value key ${key})`;
- return false;
+ // Note: this mutates the original object, but NOT the actual array it's
+ // operating on. This means if the array at the original thing[key] value
+ // was also used elsewhere, it will have the original values (not the mapped
+ // and filtered ones).
+ const mapAndFilter = function(thing, key, {
+ map,
+ filter = x => x,
+ context // only used for debugging
+ }) {
+ const replacement = [];
+ for (const value of thing[key]) {
+ const newValue = map(value);
+ if (filter(newValue)) {
+ replacement.push(newValue);
+ } else {
+ let contextPart = `${thing.name}`;
+ if (context) {
+ contextPart += ` (${context(thing)})`;
+ }
+ logWarn`Post-process: Value ${value} (${key}) didn't match any data - ${contextPart}`;
}
- return true;
- }));
+ }
+ thing[key] = replacement;
};
- WD.trackData.forEach(track => mapInPlace(track.references, r => find.track(r, {wikiData})));
- WD.trackData.forEach(track => track.aka = find.track(track.aka, {wikiData}));
- WD.trackData.forEach(track => mapInPlace(track.artTags, t => find.tag(t, {wikiData})));
- WD.albumData.forEach(album => mapInPlace(album.groups, g => find.group(g, {wikiData})));
- WD.albumData.forEach(album => mapInPlace(album.artTags, t => find.tag(t, {wikiData})));
- WD.artistAliasData.forEach(artist => artist.alias = find.artist(artist.alias, {wikiData}));
- WD.contributionData.forEach(contrib => contrib.who = find.artist(contrib.who, {wikiData}));
-
- filterNullArray(WD.trackData, 'references');
- filterNullArray(WD.trackData, 'artTags');
- filterNullArray(WD.albumData, 'groups');
- filterNullArray(WD.albumData, 'artTags');
- filterNullValue(WD.artistAliasData, 'alias');
- filterNullValue(WD.contributionData, 'who');
-
- WD.trackData.forEach(track1 => track1.referencedBy = WD.trackData.filter(track2 => track2.references.includes(track1)));
- WD.groupData.forEach(group => group.albums = WD.albumData.filter(album => album.groups.includes(group)));
- WD.tagData.forEach(tag => tag.things = sortByArtDate([...WD.albumData, ...WD.trackData]).filter(thing => thing.artTags.includes(tag)));
-
- WD.groupData.forEach(group => group.category = WD.groupCategoryData.find(x => x.name === group.category));
- WD.groupCategoryData.forEach(category => category.groups = WD.groupData.filter(x => x.category === category));
-
- WD.trackData.forEach(track => track.otherReleases = [
- track.aka,
- ...WD.trackData.filter(({ aka }) => aka === track || (track.aka && aka === track.aka)),
- ].filter(x => x && x !== track));
+ const bound = {
+ findGroup: x => find.group(x, {wikiData}),
+ findTrack: x => find.track(x, {wikiData}),
+ findTag: x => find.tag(x, {wikiData})
+ };
+
+ for (const track of WD.trackData) {
+ const context = () => track.album.name;
+ track.aka = find.track(track.aka, {wikiData});
+ mapAndFilter(track, 'references', {map: bound.findTrack, context});
+ mapAndFilter(track, 'artTags', {map: bound.findTag, context});
+ collectContributors(track, 'artists', 'contributors', 'coverArtists');
+ }
+
+ for (const track1 of WD.trackData) {
+ track1.referencedBy = WD.trackData.filter(track2 => track2.references.includes(track1));
+ track1.otherReleases = [
+ track1.aka,
+ ...WD.trackData.filter(track2 =>
+ track2.aka === track1 ||
+ (track1.aka && track2.aka === track1.aka))
+ ].filter(x => x && x !== track1);
+ }
+
+ for (const album of WD.albumData) {
+ mapAndFilter(album, 'groups', {map: bound.findGroup});
+ mapAndFilter(album, 'artTags', {map: bound.findTag});
+ collectContributors(album, 'artists', 'coverArtists', 'wallpaperArtists', 'bannerArtists');
+ }
+
+ mapAndFilter(WD, 'artistAliasData', {
+ map: artist => {
+ artist.alias = find.artist(artist.alias, {wikiData});
+ return artist;
+ },
+ filter: artist => artist.alias
+ });
+
+ for (const group of WD.groupData) {
+ group.albums = WD.albumData.filter(album => album.groups.includes(group));
+ group.category = WD.groupCategoryData.find(x => x.name === group.category);
+ }
+
+ for (const category of WD.groupCategoryData) {
+ category.groups = WD.groupData.filter(x => x.category === category);
+ }
+
+ const albumAndTrackDataSortedByArtDateMan = sortByArtDate([...WD.albumData, ...WD.trackData]);
+
+ for (const tag of WD.tagData) {
+ tag.things = albumAndTrackDataSortedByArtDateMan.filter(thing => thing.artTags.includes(tag));
+ }
if (WD.wikiInfo.features.flashesAndGames) {
- WD.flashData.forEach(flash => mapInPlace(flash.tracks, t => find.track(t, {wikiData})));
- WD.flashData.forEach(flash => flash.act = WD.flashActData.find(act => act.name === flash.act));
- WD.flashActData.forEach(act => act.flashes = WD.flashData.filter(flash => flash.act === act));
+ for (const flash of WD.flashData) {
+ flash.act = WD.flashActData.find(act => act.name === flash.act);
+ mapAndFilter(flash, 'tracks', {map: bound.findTrack});
+ collectContributors(flash, 'contributors');
+ }
- filterNullArray(WD.flashData, 'tracks');
+ for (const act of WD.flashActData) {
+ act.flashes = WD.flashData.filter(flash => flash.act === act);
+ }
- WD.trackData.forEach(track => track.flashes = WD.flashData.filter(flash => flash.tracks.includes(track)));
+ for (const track of WD.trackData) {
+ track.flashes = WD.flashData.filter(flash => flash.tracks.includes(track));
+ }
}
- WD.artistData.forEach(artist => {
+ // Process contributors before artist data, because a bunch of artist data
+ // will depend on accessing the values postprocessContributors() updates.
+ postprocessContributors();
+
+ for (const artist of WD.artistData) {
const filterProp = (array, prop) => array.filter(thing => thing[prop]?.some(({ who }) => who === artist));
const filterCommentary = array => array.filter(thing => thing.commentary && thing.commentary.replace(/<\/?b>/g, '').includes('<i>' + artist.name + ':</i>'));
artist.tracks = {
@@ -2747,7 +2855,7 @@ async function main() {
asContributor: filterProp(WD.flashData, 'contributors')
};
}
- });
+ }
WD.officialAlbumData = WD.albumData.filter(album => album.groups.some(group => group.directory === OFFICIAL_GROUP_DIRECTORY));
WD.fandomAlbumData = WD.albumData.filter(album => album.groups.every(group => group.directory !== OFFICIAL_GROUP_DIRECTORY));
@@ -2848,7 +2956,7 @@ async function main() {
writes = buildStepsWithTargets.flatMap(({ flag, pageSpec, targets }) => {
const writes = targets.flatMap(target =>
- pageSpec.write(target, {wikiData}).slice() || []);
+ pageSpec.write(target, {wikiData})?.slice() || []);
if (!validateWrites(writes, flag + '.write')) {
return [];
@@ -3035,7 +3143,6 @@ async function main() {
bound.getGridHTML = bindOpts(getGridHTML, {
[bindOpts.bindIndex]: 0,
- getLinkThemeString,
img,
strings
});
@@ -3044,15 +3151,15 @@ async function main() {
[bindOpts.bindIndex]: 0,
getAlbumCover: bound.getAlbumCover,
getGridHTML: bound.getGridHTML,
- strings,
- to
+ link: bound.link,
+ strings
});
bound.getFlashGridHTML = bindOpts(getFlashGridHTML, {
[bindOpts.bindIndex]: 0,
getFlashCover: bound.getFlashCover,
getGridHTML: bound.getGridHTML,
- to
+ link: bound.link
});
bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
diff --git a/src/url-spec.js b/src/url-spec.js
index 22b3bc8..3a77858 100644
--- a/src/url-spec.js
+++ b/src/url-spec.js
@@ -22,7 +22,7 @@ const urlSpec = {
root: '',
path: '<>',
- home: '',
+ home: '/',
album: 'album/<>/',
albumCommentary: 'commentary/album/<>/',
@@ -71,11 +71,12 @@ const urlSpec = {
path: '<>',
albumCover: 'album-art/<>/cover.jpg',
- albumWallpaper: 'album-art/<>/bg.jpg',
- albumBanner: 'album-art/<>/banner.jpg',
+ albumWallpaper: 'album-art/<>/bg.<>',
+ albumBanner: 'album-art/<>/banner.<>',
trackCover: 'album-art/<>/<>.jpg',
artistAvatar: 'artist-avatar/<>.jpg',
- flashArt: 'flash-art/<>.jpg'
+ flashArt: 'flash-art/<>.jpg',
+ flashArtGif: 'flash-art/<>.gif' // Hack! Sorry not sorry. ::::)
}
}
};
diff --git a/src/util/find.js b/src/util/find.js
index 1cbeb82..5f69bbe 100644
--- a/src/util/find.js
+++ b/src/util/find.js
@@ -1,15 +1,36 @@
import {
+ logError,
logWarn
} from './cli.js';
-function findHelper(keys, dataProp, findFn) {
- return (ref, {wikiData}) => {
- if (!ref) return null;
- ref = ref.replace(new RegExp(`^(${keys.join('|')}):`), '');
+function findHelper(keys, dataProp, findFns = {}) {
+ const byDirectory = findFns.byDirectory || matchDirectory;
+ const byName = findFns.byName || matchName;
+
+ const keyRefRegex = new RegExp(`^((${keys.join('|')}):)?(.*)$`);
+
+ return (fullRef, {wikiData}) => {
+ if (!fullRef) return null;
+ if (typeof fullRef !== 'string') {
+ throw new Error(`Got a reference that is ${typeof fullRef}, not string: ${fullRef}`);
+ }
+
+ const match = fullRef.match(keyRefRegex);
+ if (!match) {
+ throw new Error(`Malformed link reference: "${fullRef}"`);
+ }
+
+ const key = match[1];
+ const ref = match[3];
+
+ const data = wikiData[dataProp];
+
+ const found = (key
+ ? byDirectory(ref, data)
+ : byName(ref, data));
- const found = findFn(ref, wikiData[dataProp]);
if (!found) {
- logWarn`Didn't match anything for ${ref}! (${keys.join(', ')})`;
+ logWarn`Didn't match anything for ${fullRef}!`;
}
return found;
@@ -20,35 +41,45 @@ function matchDirectory(ref, data) {
return data.find(({ directory }) => directory === ref);
}
-function matchDirectoryOrName(ref, data) {
- let thing;
+function matchName(ref, data) {
+ const matches = data.filter(({ name }) => name.toLowerCase() === ref.toLowerCase());
- thing = matchDirectory(ref, data);
- if (thing) return thing;
+ if (matches.length > 1) {
+ logError`Multiple matches for reference "${ref}". Please resolve:`;
+ for (const match of matches) {
+ logError`- ${match.name} (${match.directory})`;
+ }
+ logError`Returning null for this reference.`;
+ return null;
+ }
- thing = data.find(({ name }) => name === ref);
- if (thing) return thing;
+ if (matches.length === 0) {
+ return null;
+ }
- thing = data.find(({ name }) => name.toLowerCase() === ref.toLowerCase());
- if (thing) {
+ const thing = matches[0];
+
+ if (ref !== thing.name) {
logWarn`Bad capitalization: ${'\x1b[31m' + ref} -> ${'\x1b[32m' + thing.name}`;
- return thing;
}
- return null;
+ return thing;
+}
+
+function matchTagName(ref, data) {
+ return matchName(ref.startsWith('cw: ') ? ref.slice(4) : ref, data);
}
const find = {
- album: findHelper(['album', 'album-commentary'], 'albumData', matchDirectoryOrName),
- artist: findHelper(['artist', 'artist-gallery'], 'artistData', matchDirectoryOrName),
- flash: findHelper(['flash'], 'flashData', matchDirectory),
- group: findHelper(['group', 'group-gallery'], 'groupData', matchDirectoryOrName),
- listing: findHelper(['listing'], 'listingSpec', matchDirectory),
- newsEntry: findHelper(['news-entry'], 'newsData', matchDirectory),
- staticPage: findHelper(['static'], 'staticPageData', matchDirectory),
- tag: findHelper(['tag'], 'tagData', (ref, data) =>
- matchDirectoryOrName(ref.startsWith('cw: ') ? ref.slice(4) : ref, data)),
- track: findHelper(['track'], 'trackData', matchDirectoryOrName)
+ album: findHelper(['album', 'album-commentary'], 'albumData'),
+ artist: findHelper(['artist', 'artist-gallery'], 'artistData'),
+ flash: findHelper(['flash'], 'flashData'),
+ group: findHelper(['group', 'group-gallery'], 'groupData'),
+ listing: findHelper(['listing'], 'listingSpec'),
+ newsEntry: findHelper(['news-entry'], 'newsData'),
+ staticPage: findHelper(['static'], 'staticPageData'),
+ tag: findHelper(['tag'], 'tagData', {byName: matchTagName}),
+ track: findHelper(['track'], 'trackData')
};
export default find;
diff --git a/src/util/link.js b/src/util/link.js
index 7ed5fd8..4e611df 100644
--- a/src/util/link.js
+++ b/src/util/link.js
@@ -19,6 +19,8 @@ export function getLinkThemeString(color) {
return `--primary-color: ${primary}; --dim-color: ${dim}`;
}
+const appendIndexHTMLRegex = /^(?!https?:\/\/).+\/$/;
+
const linkHelper = (hrefFn, {color = true, attr = null} = {}) =>
(thing, {
to,
@@ -27,18 +29,30 @@ const linkHelper = (hrefFn, {color = true, attr = null} = {}) =>
class: className = '',
color: color2 = true,
hash = ''
- }) => (
- html.tag('a', {
+ }) => {
+ let href = hrefFn(thing, {to});
+
+ if (link.globalOptions.appendIndexHTML) {
+ if (appendIndexHTMLRegex.test(href)) {
+ href += 'index.html';
+ }
+ }
+
+ if (hash) {
+ href += (hash.startsWith('#') ? '' : '#') + hash;
+ }
+
+ return html.tag('a', {
...attr ? attr(thing) : {},
...attributes ? attributes : {},
- href: hrefFn(thing, {to}) + (hash ? (hash.startsWith('#') ? '' : '#') + hash : ''),
+ href,
style: (
typeof color2 === 'string' ? getLinkThemeString(color2) :
color2 && color ? getLinkThemeString(thing.color) :
''),
class: className
}, text || thing.name)
- );
+ };
const linkDirectory = (key, {expose = null, attr = null, ...conf} = {}) =>
linkHelper((thing, {to}) => to('localized.' + key, thing.directory), {
@@ -53,6 +67,15 @@ const linkPathname = (key, conf) => linkHelper(({directory: pathname}, {to}) =>
const linkIndex = (key, conf) => linkHelper((_, {to}) => to('localized.' + key), conf);
const link = {
+ globalOptions: {
+ // This should usually only 8e used during development! It'll take any
+ // href that ends with `/` and append `index.html` to the returned
+ // value (for to.thing() functions). This is handy when developing
+ // without a local server (i.e. using file:// protocol URLs in your
+ // 8rowser), 8ut isn't guaranteed to 8e 100% 8ug-free.
+ appendIndexHTML: false
+ },
+
album: linkDirectory('album'),
albumCommentary: linkDirectory('albumCommentary'),
artist: linkDirectory('artist', {color: false}),
diff --git a/src/util/replacer.js b/src/util/replacer.js
index 0c16dc8..6c52477 100644
--- a/src/util/replacer.js
+++ b/src/util/replacer.js
@@ -324,7 +324,10 @@ function evaluateTag(node, opts) {
const source = input.slice(node.i, node.iEnd);
- const replacerKey = node.data.replacerKey?.data || 'track';
+ const replacerKeyImplied = !node.data.replacerKey;
+ const replacerKey = (replacerKeyImplied
+ ? 'track'
+ : node.data.replacerKey.data);
if (!replacerSpec[replacerKey]) {
logWarn`The link ${source} has an invalid replacer key!`;
@@ -343,7 +346,9 @@ function evaluateTag(node, opts) {
const value = (
valueFn ? valueFn(replacerValue) :
- findKey ? find[findKey](replacerValue, {wikiData}) :
+ findKey ? find[findKey]((replacerKeyImplied
+ ? replacerValue
+ : replacerKey + `:` + replacerValue), {wikiData}) :
{
directory: replacerValue,
name: null
diff --git a/src/util/urls.js b/src/util/urls.js
index f0f9cdb..e15c018 100644
--- a/src/util/urls.js
+++ b/src/util/urls.js
@@ -34,22 +34,33 @@ export function generateURLs(urlSpec) {
};
};
+ // This should be called on values which are going to be passed to
+ // path.relative, because relative will resolve a leading slash as the root
+ // directory of the working device, which we aren't looking for here.
+ const trimLeadingSlash = P => P.startsWith('/') ? P.slice(1) : P;
+
const generateTo = (fromPath, fromGroup) => {
+ const A = trimLeadingSlash(fromPath);
+
const rebasePrefix = '../'.repeat((fromGroup.prefix || '').split('/').filter(Boolean).length);
const pathHelper = (toPath, toGroup) => {
- let target = toPath;
+ let B = trimLeadingSlash(toPath);
let argIndex = 0;
- target = target.replaceAll('<>', () => `<${argIndex++}>`);
+ B = B.replaceAll('<>', () => `<${argIndex++}>`);
if (toGroup.prefix !== fromGroup.prefix) {
// TODO: Handle differing domains in prefixes.
- target = rebasePrefix + (toGroup.prefix || '') + target;
+ B = rebasePrefix + (toGroup.prefix || '') + B;
}
- return (path.relative(fromPath, target)
- + (toPath.endsWith('/') ? '/' : ''));
+ const suffix = (toPath.endsWith('/') ? '/' : '');
+
+ return {
+ posix: path.posix.relative(A, B) + suffix,
+ device: path.relative(A, B) + suffix
+ };
};
const groupSymbol = Symbol();
@@ -63,20 +74,31 @@ export function generateURLs(urlSpec) {
const relative = withEntries(urlSpec, entries => entries
.map(([key, urlGroup]) => [key, groupHelper(urlGroup)]));
- const to = (key, ...args) => {
- const { value: template, group: {[groupSymbol]: toGroup} } = getValueForFullKey(relative, key)
- let result = template.replaceAll(/<([0-9]+)>/g, (match, n) => args[n]);
+ const toHelper = (delimiterMode) => (key, ...args) => {
+ const {
+ value: {[delimiterMode]: template}
+ } = getValueForFullKey(relative, key);
+
+ let missing = 0;
+ let result = template.replaceAll(/<([0-9]+)>/g, (match, n) => {
+ if (n < args.length) {
+ return args[n];
+ } else {
+ missing++;
+ }
+ });
- // Kinda hacky lol, 8ut it works.
- const missing = result.match(/<([0-9]+)>/g);
if (missing) {
- throw new Error(`Expected ${missing[missing.length - 1]} arguments, got ${args.length}`);
+ throw new Error(`Expected ${missing + args.length} arguments, got ${args.length} (key ${key}, args [${args}])`);
}
return result;
};
- return {to, relative};
+ return {
+ to: toHelper('posix'),
+ toDevice: toHelper('device')
+ };
};
const generateFrom = () => {
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index 87c4b22..2f705f9 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -79,11 +79,11 @@ export function sortByName(a, b) {
// This function was originally made to sort just al8um data, 8ut its exact
// code works fine for sorting tracks too, so I made the varia8les and names
// more general.
-export function sortByDate(data) {
+export function sortByDate(data, dateKey = 'date') {
// Just to 8e clear: sort is a mutating function! I only return the array
// 8ecause then you don't have to define it as a separate varia8le 8efore
// passing it into this function.
- return data.sort((a, b) => a.date - b.date);
+ return data.sort((a, b) => a[dateKey] - b[dateKey]);
}
// Same details as the sortByDate, 8ut for covers~
@@ -143,7 +143,9 @@ export function getArtistCommentary(artist, {justEverythingMan}) {
}
export function getFlashCover(flash, {to}) {
- return to('media.flashArt', flash.directory);
+ return (flash.jiff
+ ? to('media.flashArtGif', flash.directory)
+ : to('media.flashArt', flash.directory));
}
export function getFlashLink(flash) {