From d41853b617e1b0e7fa41309ff0d42611305c3149 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 15 May 2021 19:08:48 -0300 Subject: bigass code refactor (no more legacy page writes) --- src/strings-default.json | 2 + src/upd8.js | 3299 +++++++++++++++++++++++----------------------- src/util/html.js | 16 +- src/util/link.js | 10 +- src/util/replacer.js | 424 ++++++ src/util/strings.js | 11 +- src/util/sugar.js | 11 + 7 files changed, 2075 insertions(+), 1698 deletions(-) create mode 100644 src/util/replacer.js diff --git a/src/strings-default.json b/src/strings-default.json index 7a948d6..031389e 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -214,6 +214,8 @@ "groupInfoPage.albumList.item": "({YEAR}) {ALBUM}", "groupGalleryPage.title": "{GROUP} - Gallery", "groupGalleryPage.infoLine": "{TRACKS} across {ALBUMS}, totaling {TIME}.", + "groupGalleryPage.anotherGroupLine": "({LINK})", + "groupGalleryPage.anotherGroupLine.link": "Choose another group to filter by!", "listingIndex.title": "Listings", "listingIndex.infoLine": "{WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}.", "listingIndex.exploreList": "Feel free to explore any of the listings linked below and in the sidebar!", diff --git a/src/upd8.js b/src/upd8.js index fe5d1a7..56fdcf4 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -112,7 +112,7 @@ import { import find from './util/find.js'; import genThumbs from './gen-thumbs.js'; import * as html from './util/html.js'; -import link from './util/link.js'; +import unbound_link from './util/link.js'; import { decorateTime, @@ -128,6 +128,11 @@ import { getThemeString } from './util/colors.js'; +import { + validateReplacerSpec, + transformInline +} from './util/replacer.js'; + import { genStrings, count, @@ -147,8 +152,8 @@ import { } from './util/wiki-data.js'; import { + bindOpts, call, - escapeRegex, filterEmptyLines, mapInPlace, queue, @@ -540,465 +545,9 @@ const replacerSpec = { } }; -{ - let error = false; - for (const [key, {link: linkKey, find: findKey, value, html}] of Object.entries(replacerSpec)) { - if (!html && !link[linkKey]) { - logError`The replacer spec ${key} has invalid link key ${linkKey}! Specify it in link specs or fix typo.`; - error = true; - } - if (findKey && !find[findKey]) { - logError`The replacer spec ${key} has invalid find key ${findKey}! Specify it in find specs or fix typo.`; - error = true; - } - } - if (error) process.exit(); - - // Syntax literals. - const tagBeginning = '[['; - const tagEnding = ']]'; - const tagReplacerValue = ':'; - const tagHash = '#'; - const tagArgument = '*'; - const tagArgumentValue = '='; - const tagLabel = '|'; - - const noPrecedingWhitespace = '(? ({i, type: 'error', data: {message}}); - const endOfInput = (i, comment) => makeError(i, `Unexpected end of input (${comment}).`); - - // These are 8asically stored on the glo8al scope, which might seem odd - // for a recursive function, 8ut the values are only ever used immediately - // after they're set. - let stopped, - stop_iMatch, - stop_iParse, - stop_literal; - - const parseOneTextNode = function(input, i, stopAt) { - return parseNodes(input, i, stopAt, true)[0]; - }; - - const parseNodes = function(input, i, stopAt, textOnly) { - let nodes = []; - let escapeNext = false; - let string = ''; - let iString = 0; - - stopped = false; - - const pushTextNode = (isLast) => { - string = input.slice(iString, i); - - // If this is the last text node 8efore stopping (at a stopAt match - // or the end of the input), trim off whitespace at the end. - if (isLast) { - string = string.trimEnd(); - } - - if (string.length) { - nodes.push({i: iString, iEnd: i, type: 'text', data: string}); - string = ''; - } - }; - - const literalsToMatch = stopAt ? stopAt.concat([R_tagBeginning]) : [R_tagBeginning]; - - // The 8ackslash stuff here is to only match an even (or zero) num8er - // of sequential 'slashes. Even amounts always cancel out! Odd amounts - // don't, which would mean the following literal is 8eing escaped and - // should 8e counted only as part of the current string/text. - // - // Inspired 8y this: https://stackoverflow.com/a/41470813 - const regexpSource = `(?= 0) { - lineStart += 1; - } else { - lineStart = 0; - } - - let lineEnd = input.slice(i).indexOf('\n'); - if (lineEnd >= 0) { - lineEnd += i; - } else { - lineEnd = input.length; - } - - const line = input.slice(lineStart, lineEnd); - - const cursor = i - lineStart; - - throw new SyntaxError(fixWS` - Parse error (at pos ${i}): ${message} - ${line} - ${'-'.repeat(cursor) + '^'} - `); - } - }; -} - -/* -{ - const show = input => process.stdout.write(`-- ${input}\n` + util.inspect( - transformInline.parse(input), - { - depth: null, - colors: true - } - ) + '\n\n'); - - show(`[[album:are-you-lost|Cristata's new album]]`); - show(`[[string:content.donate.patreonLine*link=[[external:https://www.patreon.com/qznebula|Patreon]]]]`); -} - -{ - const test = input => { - let n = 0; - const s = 5; - const start = Date.now(); - const end = start + s * 1000; - while (Date.now() < end) { - transformInline.parse(input); - n++; - } - console.log(`Ran ${Math.round(n / s)} times/sec.`); - } - - test(fixWS` - [[string:content.donate.patreonLine*link=[[external:https://www.patreon.com/qznebula|Patreon]]]] - Hello, world! Wow [[album:the-beans-zone]] is some cool stuff. - `); +if (!validateReplacerSpec(replacerSpec, unbound_link)) { process.exit(); } -*/ - -{ - const evaluateTag = function(node, opts) { - const { input, strings, to, wikiData } = opts; - - const source = input.slice(node.i, node.iEnd); - - const replacerKey = node.data.replacerKey?.data || 'track'; - - if (!replacerSpec[replacerKey]) { - logWarn`The link ${source} has an invalid replacer key!`; - return source; - } - - const { - find: findKey, - link: linkKey, - value: valueFn, - html: htmlFn, - transformName - } = replacerSpec[replacerKey]; - - const replacerValue = transformNodes(node.data.replacerValue, opts); - - const value = ( - valueFn ? valueFn(replacerValue) : - findKey ? find[findKey](replacerValue, {wikiData}) : - { - directory: replacerValue, - name: null - }); - - if (!value) { - logWarn`The link ${source} does not match anything!`; - return source; - } - - const enteredLabel = node.data.label && transformNode(node.data.label, opts); - - const label = (enteredLabel - || transformName && transformName(value.name, node, input) - || value.name); - - if (!valueFn && !label) { - logWarn`The link ${source} requires a label be entered!`; - return source; - } - - const hash = node.data.hash && transformNodes(node.data.hash, opts); - - const args = node.data.args && Object.fromEntries(node.data.args.map( - ({ key, value }) => [ - transformNode(key, opts), - transformNodes(value, opts) - ])); - - const fn = (htmlFn - ? htmlFn - : strings.link[linkKey]); - - try { - return fn(value, {text: label, hash, args, strings, to}); - } catch (error) { - logError`The link ${source} failed to be processed: ${error}`; - return source; - } - }; - - const transformNode = function(node, opts) { - if (!node) { - throw new Error('Expected a node!'); - } - - if (Array.isArray(node)) { - throw new Error('Got an array - use transformNodes here!'); - } - - switch (node.type) { - case 'text': - return node.data; - case 'tag': - return evaluateTag(node, opts); - default: - throw new Error(`Unknown node type ${node.type}`); - } - }; - - const transformNodes = function(nodes, opts) { - if (!nodes || !Array.isArray(nodes)) { - throw new Error(`Expected an array of nodes! Got: ${nodes}`); - } - - return nodes.map(node => transformNode(node, opts)).join(''); - }; - - Object.assign(transformInline, { - evaluateTag, - transformNode, - transformNodes - }); -} - -function transformInline(input, {strings, to, wikiData}) { - if (!strings) throw new Error('Expected strings'); - if (!to) throw new Error('Expected to'); - if (!wikiData) throw new Error('Expected wikiData'); - - const nodes = transformInline.parse(input); - return transformInline.transformNodes(nodes, {input, strings, to, wikiData}); -} function parseAttributes(string, {to}) { const attributes = Object.create(null); @@ -1055,14 +604,13 @@ function parseAttributes(string, {to}) { ])); } -function transformMultiline(text, {strings, to, wikiData}) { +function transformMultiline(text, { + parseAttributes, + transformInline +}) { // Heck yes, HTML magics. - if (!strings) throw new Error('Expected strings'); - if (!to) throw new Error('Expected to'); - if (!wikiData) throw new Error('Expected wikiData'); - - text = transformInline(text.trim(), {strings, to, wikiData}); + text = transformInline(text.trim()); const outLines = []; @@ -1107,7 +655,7 @@ function transformMultiline(text, {strings, to, wikiData}) { lazy: true, link: true, thumb: 'medium', - ...parseAttributes(attributes, {to}) + ...parseAttributes(attributes) })); let indentThisLine = 0; @@ -1205,7 +753,10 @@ function transformMultiline(text, {strings, to, wikiData}) { return outLines.join('\n'); } -function transformLyrics(text, {strings, to, wikiData}) { +function transformLyrics(text, { + transformInline, + transformMultiline +}) { // Different from transformMultiline 'cuz it joins multiple lines together // with line 8reaks (
); transformMultiline treats each line as its own // complete paragraph (or list, etc). @@ -1213,10 +764,10 @@ function transformLyrics(text, {strings, to, wikiData}) { // If it looks like old data, then like, oh god. // Use the normal transformMultiline tool. if (text.includes(' outLines.push(`

${buildLine}

`); @@ -2029,14 +1580,14 @@ function stringifyArtistData({wikiData}) { } function img({ - src = '', - alt = '', - thumb: thumbKey = '', - reveal = '', - id = '', - class: className = '', - width = '', - height = '', + src, + alt, + thumb: thumbKey, + reveal, + id, + class: className, + width, + height, link = false, lazy = false, square = false @@ -2178,7 +1729,7 @@ function validateWritePath(path, urlGroup) { return {error: `Specified key ${specifiedKey} isn't defined`}; } - const expectedArgs = paths[specifiedKey].match(/<>/g).length; + const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0; const specifiedArgs = path.length - 1; if (specifiedArgs !== expectedArgs) { @@ -2232,6 +1783,24 @@ function validateWriteObject(obj) { break; } + case 'redirect': { + const fromPath = validateWritePath(obj.fromPath, urlSpec.localized); + if (fromPath.error) { + return {error: `Path (fromPath) validation failed: ${fromPath.error}`}; + } + + const toPath = validateWritePath(obj.toPath, urlSpec.localized); + if (toPath.error) { + return {error: `Path (toPath) validation failed: ${toPath.error}`}; + } + + if (typeof obj.title !== 'function') { + return {error: `Expected title to be function, got ${obj.title}`}; + } + + break; + } + default: { return {error: `Unknown type: ${obj.type}`}; } @@ -2245,35 +1814,37 @@ async function writeData(subKey, directory, data) { await writePage.write(JSON.stringify(data), {paths}); } -async function writePage(pageSubKey, directory, pageFn, {baseDirectory, strings, wikiData}) { - // Generally this function shouldn't 8e called directly - instead use the - // shadowed version provided 8y wrapLanguages, which automatically provides - // the appropriate baseDirectory and strings arguments. (The utility - // functions attached to this function are generally useful, though!) - - const paths = writePage.paths(baseDirectory, 'localized.' + pageSubKey, directory); - - const to = (targetFullKey, ...args) => { - const [ groupKey, subKey ] = targetFullKey.split('.')[0]; - let path = paths.subdirectoryPrefix - // When linking to *outside* the localized area of the site, we need to - // make sure the result is correctly relative to the 8ase directory. - if (groupKey !== 'localized' && baseDirectory) { - path += urls.from('localizedWithBaseDirectory.' + pageSubKey).to(targetFullKey, ...args); - } else { - // If we're linking inside the localized area (or there just is no - // 8ase directory), the 8ase directory doesn't matter. - path += urls.from('localized.' + pageSubKey).to(targetFullKey, ...args); - } - // console.log(pageSubKey, '->', targetFullKey, '=', path); - return path; - }; - - const content = writePage.html(pageFn, {paths, strings, to, wikiData}); - await writePage.write(content, {paths}); -} +// This used to 8e a function! It's long 8een divided into multiple helper +// functions, and nowadays we just directly access those, rather than ever +// touching the original one (which had contained everything). +const writePage = {}; + +writePage.to = ({ + baseDirectory, + pageSubKey, + paths +}) => (targetFullKey, ...args) => { + const [ groupKey, subKey ] = targetFullKey.split('.'); + let path = paths.subdirectoryPrefix; + // When linking to *outside* the localized area of the site, we need to + // make sure the result is correctly relative to the 8ase directory. + if (groupKey !== 'localized' && baseDirectory) { + path += urls.from('localizedWithBaseDirectory.' + pageSubKey).to(targetFullKey, ...args); + } else { + // If we're linking inside the localized area (or there just is no + // 8ase directory), the 8ase directory doesn't matter. + path += urls.from('localized.' + pageSubKey).to(targetFullKey, ...args); + } + return path; +}; -writePage.html = (pageFn, {paths, strings, to, wikiData}) => { +writePage.html = (pageFn, { + paths, + strings, + to, + transformMultiline, + wikiData +}) => { const { wikiInfo } = wikiData; let { @@ -2319,7 +1890,7 @@ writePage.html = (pageFn, {paths, strings, to, wikiData}) => { nav.links ??= []; footer.classes ??= []; - footer.content ??= (wikiInfo.footer ? transformMultiline(wikiInfo.footer, {strings, to, wikiData}) : ''); + footer.content ??= (wikiInfo.footer ? transformMultiline(wikiInfo.footer) : ''); const canonical = (wikiInfo.canonicalBase ? wikiInfo.canonicalBase + paths.pathname @@ -2378,14 +1949,8 @@ writePage.html = (pageFn, {paths, strings, to, wikiData}) => { if (nav.simple) { nav.links = [ - { - href: to('localized.home'), - title: wikiInfo.shortName - }, - { - href: '', - title - } + {toHome: true}, + {toCurrentPage: true} ]; } @@ -2393,15 +1958,42 @@ writePage.html = (pageFn, {paths, strings, to, wikiData}) => { const navLinkParts = []; for (let i = 0; i < links.length; i++) { - const link = links[i]; + let cur = links[i]; const prev = links[i - 1]; const next = links[i + 1]; - const { html, href, title, divider = true } = link; - let part = prev && divider ? '/ ' : ''; - if (typeof href === 'string') { - part += `${title}`; - } else if (html) { - part += `${html}`; + + let { title: linkTitle } = cur; + + if (cur.toHome) { + linkTitle ??= wikiInfo.shortName; + } else if (cur.toCurrentPage) { + linkTitle ??= title; + } + + let part = prev && (cur.divider ?? true) ? '/ ' : ''; + + if (typeof cur.html === 'string') { + if (!cur.html) { + logWarn`Empty HTML in nav link ${JSON.stringify(cur)}`; + } + part += `${cur.html}`; + } else { + const attributes = { + class: (cur.toCurrentPage || i === links.length - 1) && 'current', + href: ( + cur.toCurrentPage ? '' : + cur.toHome ? to('localized.home') : + cur.path ? to(...cur.path) : + cur.href ? call(() => { + logWarn`Using legacy href format nav link in ${paths.pathname}`; + return cur.href; + }) : + null) + }; + if (attributes.href === null) { + throw new Error(`Expected some href specifier for link to ${linkTitle} (${JSON.stringify(cur)})`); + } + part += html.tag('a', attributes, linkTitle); } navLinkParts.push(part); } @@ -2529,7 +2121,7 @@ writePage.write = async (content, {paths}) => { }; // TODO: This only supports one <>-style argument. -writePage.paths = (baseDirectory, fullKey, directory, { +writePage.paths = (baseDirectory, fullKey, directory = '', { file = 'index.html' } = {}) => { const [ groupKey, subKey ] = fullKey.split('.'); @@ -2577,10 +2169,13 @@ function getGridHTML({ `).join('\n'); } -function getAlbumGridHTML({strings, to, details = false, ...props}) { +function getAlbumGridHTML({ + getAlbumCover, getGridHTML, strings, to, + details = false, + ...props +}) { return getGridHTML({ - strings, - srcFn: album => getAlbumCover(album, {to}), + srcFn: getAlbumCover, hrefFn: album => to('localized.album', album.directory), detailsFn: details && (album => strings('misc.albumGridDetails', { tracks: strings.count.tracks(album.tracks.length, {unit: true}), @@ -2590,10 +2185,12 @@ function getAlbumGridHTML({strings, to, details = false, ...props}) { }); } -function getFlashGridHTML({strings, to, ...props}) { +function getFlashGridHTML({ + getFlashCover, getGridHTML, to, + ...props +}) { return getGridHTML({ - strings, - srcFn: flash => to('media.flashArt', flash.directory), + srcFn: getFlashCover, hrefFn: flash => to('localized.flash', flash.directory), ...props }); @@ -2767,97 +2364,108 @@ function writeSharedFilesAndPages({strings, wikiData}) { function writeHomepage({wikiData}) { const { newsData, staticPageData, homepageInfo, wikiInfo } = wikiData; - return ({strings, writePage}) => writePage('home', '', ({to}) => ({ - title: wikiInfo.name, + const page = { + type: 'page', + path: ['home'], + page: ({ + getAlbumGridHTML, + link, + strings, + to, + transformInline, + transformMultiline, + }) => ({ + title: wikiInfo.name, + + meta: { + description: wikiInfo.description + }, - meta: { - description: wikiInfo.description - }, + main: { + classes: ['top-index'], + content: fixWS` +

${wikiInfo.name}

+ ${homepageInfo.rows.map((row, i) => fixWS` +
+

${row.name}

+ ${row.type === 'albums' && fixWS` +
+ ${getAlbumGridHTML({ + entries: ( + row.group === 'new-releases' ? getNewReleases(row.groupCount, {wikiData}) : + row.group === 'new-additions' ? getNewAdditions(row.groupCount, {wikiData}) : + ((find.group(row.group, {wikiData})?.albums || []) + .slice() + .reverse() + .slice(0, row.groupCount) + .map(album => ({item: album}))) + ).concat(row.albums + .map(album => find.album(album, {wikiData})) + .map(album => ({item: album})) + ), + lazy: i > 0 + })} + ${row.actions.length && fixWS` +
+ ${row.actions.map(action => transformInline(action) + .replace(' + `} +
+ `} +
+ `).join('\n')} + ` + }, - main: { - classes: ['top-index'], - content: fixWS` -

${wikiInfo.name}

- ${homepageInfo.rows.map((row, i) => fixWS` -
-

${row.name}

- ${row.type === 'albums' && fixWS` -
- ${getAlbumGridHTML({ - strings, to, - entries: ( - row.group === 'new-releases' ? getNewReleases(row.groupCount, {wikiData}) : - row.group === 'new-additions' ? getNewAdditions(row.groupCount, {wikiData}) : - ((find.group(row.group, {wikiData})?.albums || []) - .slice() - .reverse() - .slice(0, row.groupCount) - .map(album => ({item: album}))) - ).concat(row.albums - .map(album => find.album(album, {wikiData})) - .map(album => ({item: album})) - ), - lazy: i > 0 + sidebarLeft: homepageInfo.sidebar && { + wide: true, + collapse: false, + // This is a pretty filthy hack! 8ut otherwise, the [[news]] part + // gets treated like it's a reference to the track named "news", + // which o8viously isn't what we're going for. Gotta catch that + // 8efore we pass it to transformMultiline, 'cuz otherwise it'll + // get repl8ced with just the word "news" (or anything else that + // transformMultiline does with references it can't match) -- and + // we can't match that for replacing it with the news column! + // + // And no, I will not make [[news]] into part of transformMultiline + // (even though that would 8e hilarious). + content: (transformMultiline(homepageInfo.sidebar.replace('[[news]]', '__GENERATE_NEWS__')) + .replace('

__GENERATE_NEWS__

', wikiInfo.features.news ? fixWS` +

${strings('homepage.news.title')}

+ ${newsData.slice(0, 3).map((entry, i) => fixWS` +
+

${link.newsEntry(entry)}

+ ${transformMultiline(entry.bodyShort)} + ${entry.bodyShort !== entry.body && link.newsEntry(entry, { + text: strings('homepage.news.entry.viewRest') })} - ${row.actions.length && fixWS` -
- ${row.actions.map(action => transformInline(action, {strings, to, wikiData}) - .replace(' - `} -
- `} -
- `).join('\n')} - ` - }, + + `).join('\n')} + ` : `

News requested in content description but this feature isn't enabled

`)) + }, - sidebarLeft: homepageInfo.sidebar && { - wide: true, - collapse: false, - // This is a pretty filthy hack! 8ut otherwise, the [[news]] part - // gets treated like it's a reference to the track named "news", - // which o8viously isn't what we're going for. Gotta catch that - // 8efore we pass it to transformMultiline, 'cuz otherwise it'll - // get repl8ced with just the word "news" (or anything else that - // transformMultiline does with references it can't match) -- and - // we can't match that for replacing it with the news column! - // - // And no, I will not make [[news]] into part of transformMultiline - // (even though that would 8e hilarious). - content: (transformMultiline(homepageInfo.sidebar.replace('[[news]]', '__GENERATE_NEWS__'), {strings, to, wikiData}) - .replace('

__GENERATE_NEWS__

', wikiInfo.features.news ? fixWS` -

${strings('homepage.news.title')}

- ${newsData.slice(0, 3).map((entry, i) => fixWS` -
-

${strings.link.newsEntry(entry, {to})}

- ${transformMultiline(entry.bodyShort, {strings, to, wikiData})} - ${entry.bodyShort !== entry.body && strings.link.newsEntry(entry, { - to, - text: strings('homepage.news.entry.viewRest') - })} -
- `).join('\n')} - ` : `

News requested in content description but this feature isn't enabled

`)) - }, + nav: { + content: fixWS` +

+ ${[ + link.home('', {text: wikiInfo.shortName, class: 'current', to}), + wikiInfo.features.listings && + link.listingIndex('', {text: strings('listingIndex.title'), to}), + wikiInfo.features.news && + 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) + ].filter(Boolean).map(link => `${link}`).join('\n')} +

+ ` + } + }) + }; - nav: { - content: fixWS` -

- ${[ - strings.link.home('', {text: wikiInfo.shortName, class: 'current', to}), - wikiInfo.features.listings && - strings.link.listingIndex('', {text: strings('listingIndex.title'), to}), - wikiInfo.features.news && - strings.link.newsIndex('', {text: strings('newsIndex.title'), to}), - wikiInfo.features.flashesAndGames && - strings.link.flashIndex('', {text: strings('flashIndex.title'), to}), - ...staticPageData.filter(page => page.listed).map(page => strings.link.staticPage(page, {to})) - ].filter(Boolean).map(link => `${link}`).join('\n')} -

- ` - } - })); + return [page]; } function writeMiscellaneousPages({wikiData}) { @@ -2882,70 +2490,93 @@ function writeNewsPages({wikiData}) { function writeNewsIndex({wikiData}) { const { newsData } = wikiData; - return ({strings, writePage}) => writePage('newsIndex', '', ({to}) => ({ - title: strings('newsIndex.title'), - - main: { - content: fixWS` -
-

${strings('newsIndex.title')}

- ${newsData.map(entry => fixWS` -
-

${strings.link.newsEntry(entry, {to})}

- ${transformMultiline(entry.bodyShort, {strings, to, wikiData})} - ${entry.bodyShort !== entry.body && `

${strings.link.newsEntry(entry, { - to, - text: strings('newsIndex.entry.viewRest') - })}

`} -
- `).join('\n')} -
- ` - }, + const page = { + type: 'page', + path: ['newsIndex'], + page: ({ + link, + strings, + transformMultiline + }) => ({ + title: strings('newsIndex.title'), - nav: {simple: true} - })); -} + main: { + content: fixWS` +
+

${strings('newsIndex.title')}

+ ${newsData.map(entry => fixWS` +
+

${link.newsEntry(entry)}

+ ${transformMultiline(entry.bodyShort)} + ${entry.bodyShort !== entry.body && `

${link.newsEntry(entry, { + text: strings('newsIndex.entry.viewRest') + })}

`} +
+ `).join('\n')} +
+ ` + }, -function writeNewsEntryPage(entry, {wikiData}) { - return ({strings, writePage}) => writePage('newsEntry', entry.directory, ({to}) => ({ - title: strings('newsEntryPage.title', {entry: entry.name}), - - main: { - content: fixWS` -
-

${strings('newsEntryPage.title', {entry: entry.name})}

-

${strings('newsEntryPage.published', {date: strings.count.date(entry.date)})}

- ${transformMultiline(entry.body, {strings, to, wikiData})} -
- ` - }, + nav: {simple: true} + }) + }; - nav: generateNewsEntryNav(entry, {strings, to, wikiData}) - })); + return [page]; } -function generateNewsEntryNav(entry, {strings, to, wikiData}) { - const { wikiInfo, newsData } = wikiData; - - // The newsData list is sorted reverse chronologically (newest ones first), +function writeNewsEntryPage(entry, {wikiData}) { + const page = { + type: 'page', + path: ['newsEntry', entry.directory], + page: ({ + link, + strings, + transformMultiline, + }) => ({ + title: strings('newsEntryPage.title', {entry: entry.name}), + + main: { + content: fixWS` +
+

${strings('newsEntryPage.title', {entry: entry.name})}

+

${strings('newsEntryPage.published', {date: strings.count.date(entry.date)})}

+ ${transformMultiline(entry.body)} +
+ ` + }, + + nav: generateNewsEntryNav(entry, {link, strings, wikiData}) + }) + }; + + return [page]; +} + +function generateNewsEntryNav(entry, {link, strings, wikiData}) { + const { wikiInfo, newsData } = wikiData; + + // The newsData list is sorted reverse chronologically (newest ones first), // so the way we find next/previous entries is flipped from normal. - const previousNextLinks = generatePreviousNextLinks('localized.newsEntry', entry, newsData.slice().reverse(), {strings, to}); + const previousNextLinks = generatePreviousNextLinks(entry, { + link, strings, + data: newsData.slice().reverse(), + linkKey: 'newsEntry' + }); return { links: [ { - href: to('localized.home'), + path: ['localized.home'], title: wikiInfo.shortName }, { - href: to('localized.newsIndex'), + path: ['localized.newsIndex'], title: strings('newsEntryPage.nav.news') }, { html: strings('newsEntryPage.nav.entry', { date: strings.count.date(entry.date), - entry: strings.link.newsEntry(entry, {class: 'current', to}) + entry: link.newsEntry(entry, {class: 'current'}) }) }, previousNextLinks && @@ -2962,21 +2593,30 @@ function writeStaticPages({wikiData}) { } function writeStaticPage(staticPage, {wikiData}) { - return ({strings, writePage}) => writePage('staticPage', staticPage.directory, ({to}) => ({ - title: staticPage.name, - stylesheet: staticPage.stylesheet, - - main: { - content: fixWS` -
-

${staticPage.name}

- ${transformMultiline(staticPage.content, {strings, to, wikiData})} -
- ` - }, + const page = { + type: 'page', + path: ['staticPage', staticPage.directory], + page: ({ + strings, + transformMultiline + }) => ({ + title: staticPage.name, + stylesheet: staticPage.stylesheet, - nav: {simple: true} - })); + main: { + content: fixWS` +
+

${staticPage.name}

+ ${transformMultiline(staticPage.content)} +
+ ` + }, + + nav: {simple: true} + }) + }; + + return [page]; } @@ -2990,7 +2630,7 @@ function getRevealStringFromTags(tags, {strings}) { } function generateCoverLink({ - strings, to, wikiData, + link, strings, wikiData, src, alt, tags = [] @@ -3013,7 +2653,7 @@ function generateCoverLink({ ${strings('releaseInfo.artTags')} ${(tags .filter(tag => !tag.isCW) - .map(tag => strings.link.tag(tag, {to})) + .map(link.tag) .join(',\n'))}

`} @@ -3039,10 +2679,10 @@ function writeAlbumPages({wikiData}) { function writeAlbumPage(album, {wikiData}) { const { wikiInfo } = wikiData; - const trackToListItem = (track, {strings, to}) => { + const trackToListItem = (track, {getArtistString, link, strings}) => { const itemOpts = { duration: strings.count.duration(track.duration), - track: strings.link.track(track, {to}) + track: link.track(track) }; return `
  • ${ (track.artists === album.artists @@ -3051,7 +2691,7 @@ function writeAlbumPage(album, {wikiData}) { ...itemOpts, by: `${ strings('trackList.item.withArtists.by', { - artists: getArtistString(track.artists, {strings, to}) + artists: getArtistString(track.artists) }) }` })) @@ -3095,142 +2735,152 @@ function writeAlbumPage(album, {wikiData}) { }) }; - const page = {type: 'page', path: ['album', album.directory], page: ({strings, to}) => ({ - title: strings('albumPage.title', {album: album.name}), - stylesheet: getAlbumStylesheet(album, {to}), - theme: getThemeString(album.color, [ - `--album-directory: ${album.directory}` - ]), - - banner: album.bannerArtists && { - dimensions: album.bannerDimensions, - src: to('media.albumBanner', album.directory), - alt: strings('misc.alt.albumBanner'), - position: 'top' - }, + const page = { + type: 'page', + path: ['album', album.directory], + page: ({ + generateCoverLink, + getAlbumStylesheet, + getArtistString, + link, + strings, + to, + transformMultiline + }) => ({ + title: strings('albumPage.title', {album: album.name}), + stylesheet: getAlbumStylesheet(album), + theme: getThemeString(album.color, [ + `--album-directory: ${album.directory}` + ]), - main: { - content: fixWS` - ${generateCoverLink({ - strings, to, wikiData, - src: to('media.albumCover', album.directory), - alt: strings('misc.alt.albumCover'), - tags: album.artTags - })} -

    ${strings('albumPage.title', {album: album.name})}

    -

    - ${[ - album.artists && strings('releaseInfo.by', { - artists: getArtistString(album.artists, { - strings, to, - showContrib: true, - showIcons: true - }) - }), - album.coverArtists && strings('releaseInfo.coverArtBy', { - artists: getArtistString(album.coverArtists, { - strings, to, - showContrib: true, - showIcons: true - }) - }), - album.wallpaperArtists && strings('releaseInfo.wallpaperArtBy', { - artists: getArtistString(album.wallpaperArtists, { - strings, to, - showContrib: true, - showIcons: true - }) - }), - album.bannerArtists && strings('releaseInfo.bannerArtBy', { - artists: getArtistString(album.bannerArtists, { - strings, to, - showContrib: true, - showIcons: true + banner: album.bannerArtists && { + dimensions: album.bannerDimensions, + src: to('media.albumBanner', album.directory), + alt: strings('misc.alt.albumBanner'), + position: 'top' + }, + + main: { + content: fixWS` + ${generateCoverLink({ + src: to('media.albumCover', album.directory), + alt: strings('misc.alt.albumCover'), + tags: album.artTags + })} +

    ${strings('albumPage.title', {album: album.name})}

    +

    + ${[ + album.artists && strings('releaseInfo.by', { + artists: getArtistString(album.artists, { + showContrib: true, + showIcons: true + }) + }), + album.coverArtists && strings('releaseInfo.coverArtBy', { + artists: getArtistString(album.coverArtists, { + showContrib: true, + showIcons: true + }) + }), + album.wallpaperArtists && strings('releaseInfo.wallpaperArtBy', { + artists: getArtistString(album.wallpaperArtists, { + showContrib: true, + showIcons: true + }) + }), + album.bannerArtists && strings('releaseInfo.bannerArtBy', { + artists: getArtistString(album.bannerArtists, { + showContrib: true, + showIcons: true + }) + }), + strings('releaseInfo.released', { + date: strings.count.date(album.date) + }), + +album.coverArtDate !== +album.date && strings('releaseInfo.artReleased', { + date: strings.count.date(album.coverArtDate) + }), + strings('releaseInfo.duration', { + duration: strings.count.duration(albumDuration, {approximate: album.tracks.length > 1}) }) - }), - strings('releaseInfo.released', { - date: strings.count.date(album.date) - }), - +album.coverArtDate !== +album.date && strings('releaseInfo.artReleased', { - date: strings.count.date(album.coverArtDate) - }), - strings('releaseInfo.duration', { - duration: strings.count.duration(albumDuration, {approximate: album.tracks.length > 1}) + ].filter(Boolean).join('
    \n')} +

    + ${commentaryEntries && `

    ${ + strings('releaseInfo.viewCommentary', { + link: `${ + strings('releaseInfo.viewCommentary.link') + }` }) - ].filter(Boolean).join('
    \n')} -

    - ${commentaryEntries && `

    ${ - strings('releaseInfo.viewCommentary', { - link: `${ - strings('releaseInfo.viewCommentary.link') - }` - }) - }

    `} - ${album.urls.length && `

    ${ - strings('releaseInfo.listenOn', { - links: strings.list.or(album.urls.map(url => fancifyURL(url, {album: true, strings}))) - }) - }

    `} - ${album.trackGroups ? fixWS` -
    - ${album.trackGroups.map(({ name, color, startIndex, tracks }) => fixWS` -
    ${ - strings('trackList.group', { - duration: strings.count.duration(getTotalDuration(tracks), {approximate: tracks.length > 1}), - group: name - }) - }
    -
    <${listTag === 'ol' ? `ol start="${startIndex + 1}"` : listTag}> - ${tracks.map(t => trackToListItem(t, {strings, to})).join('\n')} -
    - `).join('\n')} -
    - ` : fixWS` - <${listTag}> - ${album.tracks.map(t => trackToListItem(t, {strings, to})).join('\n')} - - `} -

    - ${[ - strings('releaseInfo.addedToWiki', { - date: strings.count.date(album.dateAdded) + }

    `} + ${album.urls.length && `

    ${ + strings('releaseInfo.listenOn', { + links: strings.list.or(album.urls.map(url => fancifyURL(url, {album: true, strings}))) }) - ].filter(Boolean).join('
    \n')} -

    - ${album.commentary && fixWS` -

    ${strings('releaseInfo.artistCommentary')}

    -
    - ${transformMultiline(album.commentary, {strings, to, wikiData})} -
    - `} - ` - }, + }

    `} + ${album.trackGroups ? fixWS` +
    + ${album.trackGroups.map(({ name, color, startIndex, tracks }) => fixWS` +
    ${ + strings('trackList.group', { + duration: strings.count.duration(getTotalDuration(tracks), {approximate: tracks.length > 1}), + group: name + }) + }
    +
    <${listTag === 'ol' ? `ol start="${startIndex + 1}"` : listTag}> + ${tracks.map(t => trackToListItem(t, {getArtistString, link, strings})).join('\n')} +
    + `).join('\n')} +
    + ` : fixWS` + <${listTag}> + ${album.tracks.map(t => trackToListItem(t, {getArtistString, link, strings})).join('\n')} + + `} +

    + ${[ + strings('releaseInfo.addedToWiki', { + date: strings.count.date(album.dateAdded) + }) + ].filter(Boolean).join('
    \n')} +

    + ${album.commentary && fixWS` +

    ${strings('releaseInfo.artistCommentary')}

    +
    + ${transformMultiline(album.commentary)} +
    + `} + ` + }, - sidebarLeft: generateSidebarForAlbum(album, null, {strings, to, wikiData}), + sidebarLeft: generateSidebarForAlbum(album, { + link, + strings, + transformMultiline, + wikiData + }), - nav: { - links: [ - { - href: to('localized.home'), - title: wikiInfo.shortName - }, - { - html: strings('albumPage.nav.album', { - album: strings.link.album(album, {class: 'current', to}) - }) - }, - { - divider: false, - html: generateAlbumNavLinks(album, null, {strings, to}) - } - ], - content: fixWS` -
    - ${generateAlbumChronologyLinks(album, null, {strings, to})} -
    - ` - } - })}; + nav: { + links: [ + {toHome: true}, + { + html: strings('albumPage.nav.album', { + album: link.album(album, {class: 'current'}) + }) + }, + album.tracks.length > 1 && + { + divider: false, + html: generateAlbumNavLinks(album, null, {link, strings}) + } + ], + content: fixWS` +
    + ${generateAlbumChronologyLinks(album, null, {link, strings})} +
    + ` + } + }) + }; return [page, data]; } @@ -3276,12 +2926,12 @@ function writeTrackPage(track, {wikiData}) { .flatMap(track => track.flashes.map(flash => ({flash, as: track})))); } - const generateTrackList = (tracks, {strings, to}) => html.tag('ul', + const unbound_generateTrackList = (tracks, {getArtistString, link, strings}) => html.tag('ul', tracks.map(track => { const line = strings('trackList.item.withArtists', { - track: strings.link.track(track, {to}), + track: link.track(track), by: `${strings('trackList.item.withArtists.by', { - artists: getArtistString(track.artists, {strings, to}) + artists: getArtistString(track.artists) })}` }); return (track.aka @@ -3291,21 +2941,23 @@ function writeTrackPage(track, {wikiData}) { ); const hasCommentary = track.commentary || otherReleases.some(t => t.commentary); - const generateCommentary = ({strings, to}) => transformMultiline( - [ - track.commentary, - ...otherReleases.map(track => - (track.commentary?.split('\n') - .filter(line => line.replace(/<\/b>/g, '').includes(':')) - .map(line => fixWS` - ${line} - ${strings('releaseInfo.artistCommentary.seeOriginalRelease', { - original: strings.link.track(track, {to}) - })} - `) - .join('\n'))) - ].filter(Boolean).join('\n'), - {strings, to, wikiData}); + const generateCommentary = ({ + link, + strings, + transformMultiline + }) => transformMultiline([ + track.commentary, + ...otherReleases.map(track => + (track.commentary?.split('\n') + .filter(line => line.replace(/<\/b>/g, '').includes(':')) + .map(line => fixWS` + ${line} + ${strings('releaseInfo.artistCommentary.seeOriginalRelease', { + original: link.track(track) + })} + `) + .join('\n'))) + ].filter(Boolean).join('\n')); const data = { type: 'data', @@ -3335,184 +2987,202 @@ function writeTrackPage(track, {wikiData}) { }) }; - const page = {type: 'page', path: ['track', track.directory], page: ({strings, to}) => ({ - title: strings('trackPage.title', {track: track.name}), - stylesheet: getAlbumStylesheet(album, {to}), - theme: getThemeString(track.color, [ - `--album-directory: ${album.directory}`, - `--track-directory: ${track.directory}` - ]), - - // disabled for now! shifting banner position per height of page is disorienting - /* - banner: album.bannerArtists && { - classes: ['dim'], - dimensions: album.bannerDimensions, - src: to('media.albumBanner', album.directory), - alt: strings('misc.alt.albumBanner'), - position: 'bottom' - }, - */ - - main: { - content: fixWS` - ${generateCoverLink({ - strings, to, wikiData, - src: getTrackCover(track, {to}), - alt: strings('misc.alt.trackCover'), - tags: track.artTags - })} -

    ${strings('trackPage.title', {track: track.name})}

    -

    - ${[ - strings('releaseInfo.by', { - artists: getArtistString(track.artists, { - strings, to, - showContrib: true, - showIcons: true - }) - }), - track.coverArtists && strings('releaseInfo.coverArtBy', { - artists: getArtistString(track.coverArtists, { - strings, to, - showContrib: true, - showIcons: true - }) - }), - album.directory !== UNRELEASED_TRACKS_DIRECTORY && strings('releaseInfo.released', { - date: strings.count.date(track.date) - }), - +track.coverArtDate !== +track.date && strings('releaseInfo.artReleased', { - date: strings.count.date(track.coverArtDate) - }), - track.duration && strings('releaseInfo.duration', { - duration: strings.count.duration(track.duration) - }) - ].filter(Boolean).join('
    \n')} -

    -

    ${ - (track.urls.length - ? strings('releaseInfo.listenOn', { - links: strings.list.or(track.urls.map(url => fancifyURL(url, {strings}))) - }) - : strings('releaseInfo.listenOn.noLinks')) - }

    - ${otherReleases.length && fixWS` -

    ${strings('releaseInfo.alsoReleasedAs')}

    -
      - ${otherReleases.map(track => fixWS` -
    • ${strings('releaseInfo.alsoReleasedAs.item', { - track: strings.link.track(track, {to}), - album: strings.link.album(track.album, {to}) - })}
    • - `).join('\n')} -
    - `} - ${track.contributors.textContent && fixWS` -

    - ${strings('releaseInfo.contributors')} -
    - ${transformInline(track.contributors.textContent, {strings, to, wikiData})} -

    - `} - ${track.contributors.length && fixWS` -

    ${strings('releaseInfo.contributors')}

    -
      - ${(track.contributors - .map(contrib => `
    • ${getArtistString([contrib], { - strings, to, - showContrib: true, - showIcons: true - })}
    • `) - .join('\n'))} -
    - `} - ${tracksReferenced.length && fixWS` -

    ${strings('releaseInfo.tracksReferenced', {track: `${track.name}`})}

    - ${generateTrackList(tracksReferenced, {strings, to})} - `} - ${tracksThatReference.length && fixWS` -

    ${strings('releaseInfo.tracksThatReference', {track: `${track.name}`})}

    - ${useDividedReferences && fixWS` -
    - ${ttrOfficial.length && fixWS` -
    ${strings('trackPage.referenceList.official')}
    -
    ${generateTrackList(ttrOfficial, {strings, to})}
    - `} - ${ttrFanon.length && fixWS` -
    ${strings('trackPage.referenceList.fandom')}
    -
    ${generateTrackList(ttrFanon, {strings, to})}
    - `} -
    - `} - ${!useDividedReferences && generateTrackList(tracksThatReference, {strings, to})} - `} - ${wikiInfo.features.flashesAndGames && flashesThatFeature.length && fixWS` -

    ${strings('releaseInfo.flashesThatFeature', {track: `${track.name}`})}

    -
      - ${flashesThatFeature.map(({ flash, as }) => fixWS` -
    • ${ - (as === track - ? strings('releaseInfo.flashesThatFeature.item', { - flash: strings.link.flash(flash, {to}) - }) - : strings('releaseInfo.flashesThatFeature.item.asDifferentRelease', { - flash: strings.link.flash(flash, {to}), - track: strings.link.track(as, {to}) - })) - }
    • - `).join('\n')} -
    - `} - ${track.lyrics && fixWS` -

    ${strings('releaseInfo.lyrics')}

    -
    - ${transformLyrics(track.lyrics, {strings, to, wikiData})} -
    - `} - ${hasCommentary && fixWS` -

    ${strings('releaseInfo.artistCommentary')}

    -
    - ${generateCommentary({strings, to})} -
    - `} - ` - }, - - sidebarLeft: generateSidebarForAlbum(album, track, {strings, to, wikiData}), + const page = { + type: 'page', + path: ['track', track.directory], + page: ({ + generateCoverLink, + getArtistString, + getTrackCover, + link, + strings, + transformInline, + transformLyrics, + transformMultiline, + to + }) => { + const generateTrackList = bindOpts(unbound_generateTrackList, {getArtistString, link, strings}); - nav: { - links: [ - { - href: to('localized.home'), - title: wikiInfo.shortName - }, - { - href: to('localized.album', album.directory), - title: album.name + return { + title: strings('trackPage.title', {track: track.name}), + stylesheet: getAlbumStylesheet(album, {to}), + theme: getThemeString(track.color, [ + `--album-directory: ${album.directory}`, + `--track-directory: ${track.directory}` + ]), + + // disabled for now! shifting banner position per height of page is disorienting + /* + banner: album.bannerArtists && { + classes: ['dim'], + dimensions: album.bannerDimensions, + src: to('media.albumBanner', album.directory), + alt: strings('misc.alt.albumBanner'), + position: 'bottom' }, - listTag === 'ol' ? { - html: strings('trackPage.nav.track.withNumber', { - number: album.tracks.indexOf(track) + 1, - track: strings.link.track(track, {class: 'current', to}) - }) - } : { - html: strings('trackPage.nav.track', { - track: strings.link.track(track, {class: 'current', to}) - }) + */ + + main: { + content: fixWS` + ${generateCoverLink({ + src: getTrackCover(track), + alt: strings('misc.alt.trackCover'), + tags: track.artTags + })} +

    ${strings('trackPage.title', {track: track.name})}

    +

    + ${[ + strings('releaseInfo.by', { + artists: getArtistString(track.artists, { + showContrib: true, + showIcons: true + }) + }), + track.coverArtists && strings('releaseInfo.coverArtBy', { + artists: getArtistString(track.coverArtists, { + showContrib: true, + showIcons: true + }) + }), + album.directory !== UNRELEASED_TRACKS_DIRECTORY && strings('releaseInfo.released', { + date: strings.count.date(track.date) + }), + +track.coverArtDate !== +track.date && strings('releaseInfo.artReleased', { + date: strings.count.date(track.coverArtDate) + }), + track.duration && strings('releaseInfo.duration', { + duration: strings.count.duration(track.duration) + }) + ].filter(Boolean).join('
    \n')} +

    +

    ${ + (track.urls.length + ? strings('releaseInfo.listenOn', { + links: strings.list.or(track.urls.map(url => fancifyURL(url, {strings}))) + }) + : strings('releaseInfo.listenOn.noLinks')) + }

    + ${otherReleases.length && fixWS` +

    ${strings('releaseInfo.alsoReleasedAs')}

    +
      + ${otherReleases.map(track => fixWS` +
    • ${strings('releaseInfo.alsoReleasedAs.item', { + track: link.track(track), + album: link.album(track.album) + })}
    • + `).join('\n')} +
    + `} + ${track.contributors.textContent && fixWS` +

    + ${strings('releaseInfo.contributors')} +
    + ${transformInline(track.contributors.textContent)} +

    + `} + ${track.contributors.length && fixWS` +

    ${strings('releaseInfo.contributors')}

    +
      + ${(track.contributors + .map(contrib => `
    • ${getArtistString([contrib], { + showContrib: true, + showIcons: true + })}
    • `) + .join('\n'))} +
    + `} + ${tracksReferenced.length && fixWS` +

    ${strings('releaseInfo.tracksReferenced', {track: `${track.name}`})}

    + ${generateTrackList(tracksReferenced)} + `} + ${tracksThatReference.length && fixWS` +

    ${strings('releaseInfo.tracksThatReference', {track: `${track.name}`})}

    + ${useDividedReferences && fixWS` +
    + ${ttrOfficial.length && fixWS` +
    ${strings('trackPage.referenceList.official')}
    +
    ${generateTrackList(ttrOfficial)}
    + `} + ${ttrFanon.length && fixWS` +
    ${strings('trackPage.referenceList.fandom')}
    +
    ${generateTrackList(ttrFanon)}
    + `} +
    + `} + ${!useDividedReferences && generateTrackList(tracksThatReference)} + `} + ${wikiInfo.features.flashesAndGames && flashesThatFeature.length && fixWS` +

    ${strings('releaseInfo.flashesThatFeature', {track: `${track.name}`})}

    +
      + ${flashesThatFeature.map(({ flash, as }) => fixWS` +
    • ${ + (as === track + ? strings('releaseInfo.flashesThatFeature.item', { + flash: link.flash(flash) + }) + : strings('releaseInfo.flashesThatFeature.item.asDifferentRelease', { + flash: link.flash(flash), + track: link.track(as) + })) + }
    • + `).join('\n')} +
    + `} + ${track.lyrics && fixWS` +

    ${strings('releaseInfo.lyrics')}

    +
    + ${transformLyrics(track.lyrics)} +
    + `} + ${hasCommentary && fixWS` +

    ${strings('releaseInfo.artistCommentary')}

    +
    + ${generateCommentary({link, strings, transformMultiline})} +
    + `} + ` }, - { - divider: false, - html: generateAlbumNavLinks(album, track, {strings, to}) + + sidebarLeft: generateSidebarForAlbum(album, { + currentTrack: track, + link, + strings, + transformMultiline, + wikiData + }), + + nav: { + links: [ + {toHome: true}, + { + path: ['localized.album', album.directory], + title: album.name + }, + listTag === 'ol' ? { + html: strings('trackPage.nav.track.withNumber', { + number: album.tracks.indexOf(track) + 1, + track: link.track(track, {class: 'current', to}) + }) + } : { + html: strings('trackPage.nav.track', { + track: link.track(track, {class: 'current', to}) + }) + }, + album.tracks.length > 1 && + { + divider: false, + html: generateAlbumNavLinks(album, track, {link, strings}) + } + ].filter(Boolean), + content: fixWS` +
    + ${generateAlbumChronologyLinks(album, track, {link, strings})} +
    + ` } - ].filter(Boolean), - content: fixWS` -
    - ${generateAlbumChronologyLinks(album, track, {strings, to})} -
    - ` + }; } - })}; + }; return [data, page]; } @@ -3629,19 +3299,22 @@ function writeArtistPage(artist, {wikiData}) { }))); } - const generateEntryAccents = ({ aka, entry, artists, contrib, strings, to }) => + const generateEntryAccents = ({ + getArtistString, strings, + aka, entry, artists, contrib + }) => (aka ? strings('artistPage.creditList.entry.rerelease', {entry}) : (artists.length ? (contrib.what ? strings('artistPage.creditList.entry.withArtists.withContribution', { entry, - artists: getArtistString(artists, {strings, to}), + artists: getArtistString(artists), contribution: contrib.what }) : strings('artistPage.creditList.entry.withArtists', { entry, - artists: getArtistString(artists, {strings, to}) + artists: getArtistString(artists) })) : (contrib.what ? strings('artistPage.creditList.entry.withContribution', { @@ -3650,11 +3323,13 @@ function writeArtistPage(artist, {wikiData}) { }) : entry))); - const generateTrackList = (chunks, {strings, to}) => fixWS` + const unbound_generateTrackList = (chunks, { + getArtistString, link, strings + }) => fixWS`
    ${chunks.map(({date, album, chunk, duration}) => fixWS`
    ${strings('artistPage.creditList.album.withDate.withDuration', { - album: strings.link.album(album, {to}), + album: link.album(album), date: strings.count.date(date), duration: strings.count.duration(duration, {approximate: true}) })}
    @@ -3663,12 +3338,12 @@ function writeArtistPage(artist, {wikiData}) { .map(({track, ...props}) => ({ aka: track.aka, entry: strings('artistPage.creditList.entry.track.withDuration', { - track: strings.link.track(track, {to}), - duration: strings.count.duration(track.duration, {to}) + track: link.track(track), + duration: strings.count.duration(track.duration) }), ...props })) - .map(({aka, ...opts}) => `
  • ${generateEntryAccents({strings, to, aka, ...opts})}
  • `) + .map(({aka, ...opts}) => `
  • ${generateEntryAccents({getArtistString, strings, aka, ...opts})}
  • `) .join('\n'))} `).join('\n')} @@ -3726,161 +3401,183 @@ function writeArtistPage(artist, {wikiData}) { const infoPage = { type: 'page', path: ['artist', artist.directory], - page: ({strings, to}) => ({ - title: strings('artistPage.title', {artist: name}), + page: ({ + generateCoverLink, + getArtistString, + link, + strings, + to, + transformMultiline + }) => { + const generateTrackList = bindOpts(unbound_generateTrackList, { + getArtistString, + link, + strings + }); - main: { - content: fixWS` - ${artist.hasAvatar && generateCoverLink({ - strings, to, wikiData, - src: to('localized.artistAvatar', artist.directory), - alt: strings('misc.alt.artistAvatar') - })} -

    ${strings('artistPage.title', {artist: name})}

    - ${note && fixWS` -

    ${strings('releaseInfo.note')}

    -
    - ${transformMultiline(note, {strings, to, wikiData})} -
    -
    - `} - ${urls.length && `

    ${strings('releaseInfo.visitOn', { - links: strings.list.or(urls.map(url => fancifyURL(url, {strings}))) - })}

    `} - ${hasGallery && `

    ${strings('artistPage.viewArtGallery', { - link: strings.link.artistGallery(artist, { - to, - text: strings('artistPage.viewArtGallery.link') - }) - })}

    `} -

    ${strings('misc.jumpTo.withLinks', { - links: strings.list.unit([ - [ - [...releasedTracks, ...unreleasedTracks].length && `${strings('artistPage.trackList.title')}`, - unreleasedTracks.length && `(${strings('artistPage.unreleasedTrackList.title')})` - ].filter(Boolean).join(' '), - artThingsAll.length && `${strings('artistPage.artList.title')}`, - wikiInfo.features.flashesAndGames && flashes.length && `${strings('artistPage.flashList.title')}`, - commentaryThings.length && `${strings('artistPage.commentaryList.title')}` - ].filter(Boolean)) - })}

    - ${(releasedTracks.length || unreleasedTracks.length) && fixWS` -

    ${strings('artistPage.trackList.title')}

    - `} - ${releasedTracks.length && fixWS` -

    ${strings('artistPage.contributedDurationLine', { - artist: artist.name, - duration: strings.count.duration(totalReleasedDuration, {approximate: true, unit: true}) - })}

    -

    ${strings('artistPage.musicGroupsLine', { - groups: strings.list.unit(musicGroups - .map(({ group, contributions }) => strings('artistPage.groupsLine.item', { - group: strings.link.groupInfo(group, {to}), - contributions: strings.count.contributions(contributions) - }))) - })}

    - ${generateTrackList(releasedTrackListChunks, {strings, to})} - `} - ${unreleasedTracks.length && fixWS` -

    ${strings('artistPage.unreleasedTrackList.title')}

    - ${generateTrackList(unreleasedTrackListChunks, {strings, to})} - `} - ${artThingsAll.length && fixWS` -

    ${strings('artistPage.artList.title')}

    - ${hasGallery && `

    ${strings('artistPage.viewArtGallery.orBrowseList', { - link: strings.link.artistGallery(artist, { - to, + return { + title: strings('artistPage.title', {artist: name}), + + main: { + content: fixWS` + ${artist.hasAvatar && generateCoverLink({ + src: to('localized.artistAvatar', artist.directory), + alt: strings('misc.alt.artistAvatar') + })} +

    ${strings('artistPage.title', {artist: name})}

    + ${note && fixWS` +

    ${strings('releaseInfo.note')}

    +
    + ${transformMultiline(note)} +
    +
    + `} + ${urls.length && `

    ${strings('releaseInfo.visitOn', { + links: strings.list.or(urls.map(url => fancifyURL(url, {strings}))) + })}

    `} + ${hasGallery && `

    ${strings('artistPage.viewArtGallery', { + link: link.artistGallery(artist, { text: strings('artistPage.viewArtGallery.link') }) })}

    `} -

    ${strings('artistPage.artGroupsLine', { - groups: strings.list.unit(artGroups - .map(({ group, contributions }) => strings('artistPage.groupsLine.item', { - group: strings.link.groupInfo(group, {to}), - contributions: strings.count.contributions(contributions) - }))) +

    ${strings('misc.jumpTo.withLinks', { + links: strings.list.unit([ + [ + [...releasedTracks, ...unreleasedTracks].length && `${strings('artistPage.trackList.title')}`, + unreleasedTracks.length && `(${strings('artistPage.unreleasedTrackList.title')})` + ].filter(Boolean).join(' '), + artThingsAll.length && `${strings('artistPage.artList.title')}`, + wikiInfo.features.flashesAndGames && flashes.length && `${strings('artistPage.flashList.title')}`, + commentaryThings.length && `${strings('artistPage.commentaryList.title')}` + ].filter(Boolean)) })}

    -
    - ${artListChunks.map(({date, album, chunk}) => fixWS` -
    ${strings('artistPage.creditList.album.withDate', { - album: strings.link.album(album, {to}), - date: strings.count.date(date) - })}
    -
      - ${(chunk - .map(({album, track, key, ...props}) => ({ - entry: (track + ${(releasedTracks.length || unreleasedTracks.length) && fixWS` +

      ${strings('artistPage.trackList.title')}

      + `} + ${releasedTracks.length && fixWS` +

      ${strings('artistPage.contributedDurationLine', { + artist: artist.name, + duration: strings.count.duration(totalReleasedDuration, {approximate: true, unit: true}) + })}

      +

      ${strings('artistPage.musicGroupsLine', { + groups: strings.list.unit(musicGroups + .map(({ group, contributions }) => strings('artistPage.groupsLine.item', { + group: link.groupInfo(group), + contributions: strings.count.contributions(contributions) + }))) + })}

      + ${generateTrackList(releasedTrackListChunks)} + `} + ${unreleasedTracks.length && fixWS` +

      ${strings('artistPage.unreleasedTrackList.title')}

      + ${generateTrackList(unreleasedTrackListChunks)} + `} + ${artThingsAll.length && fixWS` +

      ${strings('artistPage.artList.title')}

      + ${hasGallery && `

      ${strings('artistPage.viewArtGallery.orBrowseList', { + link: link.artistGallery(artist, { + text: strings('artistPage.viewArtGallery.link') + }) + })}

      `} +

      ${strings('artistPage.artGroupsLine', { + groups: strings.list.unit(artGroups + .map(({ group, contributions }) => strings('artistPage.groupsLine.item', { + group: link.groupInfo(group), + contributions: strings.count.contributions(contributions) + }))) + })}

      +
      + ${artListChunks.map(({date, album, chunk}) => fixWS` +
      ${strings('artistPage.creditList.album.withDate', { + album: link.album(album), + date: strings.count.date(date) + })}
      +
        + ${(chunk + .map(({album, track, key, ...props}) => ({ + entry: (track + ? strings('artistPage.creditList.entry.track', { + track: link.track(track) + }) + : `${strings('artistPage.creditList.entry.album.' + { + wallpaperArtists: 'wallpaperArt', + bannerArtists: 'bannerArt', + coverArtists: 'coverArt' + }[key])}`), + ...props + })) + .map(opts => generateEntryAccents({getArtistString, strings, ...opts})) + .map(row => `
      • ${row}
      • `) + .join('\n'))} +
      + `).join('\n')} +
      + `} + ${wikiInfo.features.flashesAndGames && flashes.length && fixWS` +

      ${strings('artistPage.flashList.title')}

      +
      + ${flashListChunks.map(({act, chunk, dateFirst, dateLast}) => fixWS` +
      ${strings('artistPage.creditList.flashAct.withDateRange', { + act: link.flash(chunk[0].flash, {text: act.name}), + dateRange: strings.count.dateRange([dateFirst, dateLast]) + })}
      +
        + ${(chunk + .map(({flash, ...props}) => ({ + entry: strings('artistPage.creditList.entry.flash', { + flash: link.flash(flash) + }), + ...props + })) + .map(opts => generateEntryAccents({getArtistString, strings, ...opts})) + .map(row => `
      • ${row}
      • `) + .join('\n'))} +
      + `).join('\n')} +
      + `} + ${commentaryThings.length && fixWS` +

      ${strings('artistPage.commentaryList.title')}

      +
      + ${commentaryListChunks.map(({album, chunk}) => fixWS` +
      ${strings('artistPage.creditList.album', { + album: link.album(album) + })}
      +
        + ${(chunk + .map(({album, track, ...props}) => track ? strings('artistPage.creditList.entry.track', { - track: strings.link.track(track, {to}) + track: link.track(track) }) - : `${strings('artistPage.creditList.entry.album.' + { - wallpaperArtists: 'wallpaperArt', - bannerArtists: 'bannerArt', - coverArtists: 'coverArt' - }[key])}`), - ...props - })) - .map(opts => generateEntryAccents({strings, to, ...opts})) - .map(row => `
      • ${row}
      • `) - .join('\n'))} -
      - `).join('\n')} -
      - `} - ${wikiInfo.features.flashesAndGames && flashes.length && fixWS` -

      ${strings('artistPage.flashList.title')}

      -
      - ${flashListChunks.map(({act, chunk, dateFirst, dateLast}) => fixWS` -
      ${strings('artistPage.creditList.flashAct.withDateRange', { - act: strings.link.flash(chunk[0].flash, {to, text: act.name}), - dateRange: strings.count.dateRange([dateFirst, dateLast]) - })}
      -
        - ${(chunk - .map(({flash, ...props}) => ({ - entry: strings('artistPage.creditList.entry.flash', { - flash: strings.link.flash(flash, {to}) - }), - ...props - })) - .map(opts => generateEntryAccents({strings, to, ...opts})) - .map(row => `
      • ${row}
      • `) - .join('\n'))} -
      - `).join('\n')} -
      - `} - ${commentaryThings.length && fixWS` -

      ${strings('artistPage.commentaryList.title')}

      -
      - ${commentaryListChunks.map(({album, chunk}) => fixWS` -
      ${strings('artistPage.creditList.album', { - album: strings.link.album(album, {to}) - })}
      -
        - ${(chunk - .map(({album, track, ...props}) => track - ? strings('artistPage.creditList.entry.track', { - track: strings.link.track(track, {to}) - }) - : `${strings('artistPage.creditList.entry.album.commentary')}`) - .map(row => `
      • ${row}
      • `) - .join('\n'))} -
      - `).join('\n')} -
      - `} - ` - }, + : `${strings('artistPage.creditList.entry.album.commentary')}`) + .map(row => `
    • ${row}
    • `) + .join('\n'))} +
    + `).join('\n')} +
    + `} + ` + }, - nav: generateNavForArtist(artist, {isGallery: false, hasGallery, strings, to, wikiData}) - }) + nav: generateNavForArtist(artist, false, { + link, strings, wikiData, + hasGallery + }) + }; + } }; const galleryPage = hasGallery && { type: 'page', path: ['artistGallery', artist.directory], - page: ({strings, to}) => ({ + page: ({ + getAlbumCover, + getGridHTML, + getTrackCover, + link, + strings, + to + }) => ({ title: strings('artistGalleryPage.title', {artist: name}), main: { @@ -3892,11 +3589,10 @@ function writeArtistPage(artist, {wikiData}) { })}

    ${getGridHTML({ - strings, to, entries: artThingsGallery.map(item => ({item})), srcFn: thing => (thing.album - ? getTrackCover(thing, {to}) - : getAlbumCover(thing, {to})), + ? getTrackCover(thing) + : getAlbumCover(thing)), hrefFn: thing => (thing.album ? to('localized.track', thing.directory) : to('localized.album', thing.directory)) @@ -3905,33 +3601,40 @@ function writeArtistPage(artist, {wikiData}) { ` }, - nav: generateNavForArtist(artist, {isGallery: true, hasGallery, strings, to, wikiData}) + nav: generateNavForArtist(artist, true, { + link, strings, wikiData, + hasGallery + }) }) }; return [data, infoPage, galleryPage].filter(Boolean); } -function generateNavForArtist(artist, {isGallery, hasGallery, strings, to, wikiData}) { +function generateNavForArtist(artist, isGallery, { + link, strings, wikiData, + hasGallery +}) { const { wikiInfo } = wikiData; const infoGalleryLinks = (hasGallery && - generateInfoGalleryLinks('artist', 'artistGallery', artist, isGallery, {strings, to})) + generateInfoGalleryLinks(artist, isGallery, { + link, strings, + linkKeyGallery: 'artistGallery', + linkKeyInfo: 'artist' + })) return { links: [ - { - href: to('localized.home'), - title: wikiInfo.shortName - }, + {toHome: true}, wikiInfo.features.listings && { - href: to('localized.listingIndex'), + path: ['localized.listingIndex'], title: strings('listingIndex.title') }, { html: strings('artistPage.nav.artist', { - artist: strings.link.artist(artist, {class: 'current', to}) + artist: link.artist(artist, {class: 'current'}) }) }, hasGallery && @@ -3943,17 +3646,19 @@ function generateNavForArtist(artist, {isGallery, hasGallery, strings, to, wikiD }; } -function writeArtistAliasPage(artist, {wikiData}) { +function writeArtistAliasPage(aliasArtist, {wikiData}) { // This function doesn't actually use wikiData, 8ut, um, consistency? - const { alias } = artist; + const { alias: targetArtist } = aliasArtist; - return async ({baseDirectory, strings, writePage}) => { - const { code } = strings; - const paths = writePage.paths(baseDirectory, 'artist', alias.directory); - const content = generateRedirectPage(alias.name, paths.pathname, {strings}); - await writePage.write(content, {paths}); + const redirect = { + type: 'redirect', + fromPath: ['artist', aliasArtist.directory], + toPath: ['artist', targetArtist.directory], + title: () => aliasArtist.name }; + + return [redirect]; } function generateRedirectPage(title, target, {strings}) { @@ -3995,121 +3700,145 @@ function writeFlashPages({wikiData}) { function writeFlashIndex({wikiData}) { const { flashActData } = wikiData; - return ({strings, writePage}) => writePage('flashIndex', '', ({to}) => ({ - title: strings('flashIndex.title'), - - main: { - classes: ['flash-index'], - content: fixWS` -

    ${strings('flashIndex.title')}

    -
    -

    ${strings('misc.jumpTo')}

    -
      - ${flashActData.filter(act => act.jump).map(({ anchor, jump, jumpColor }) => fixWS` -
    • ${jump}
    • - `).join('\n')} -
    -
    - ${flashActData.map((act, i) => fixWS` -

    ${act.name}

    -
    - ${getFlashGridHTML({ - strings, to, - entries: act.flashes.map(flash => ({item: flash})), - lazy: i === 0 ? 4 : true - })} + const page = { + type: 'page', + path: ['flashIndex'], + page: ({ + getFlashGridHTML, + link, + strings + }) => ({ + title: strings('flashIndex.title'), + + main: { + classes: ['flash-index'], + content: fixWS` +

    ${strings('flashIndex.title')}

    +
    +

    ${strings('misc.jumpTo')}

    +
      + ${flashActData.filter(act => act.jump).map(({ anchor, jump, jumpColor }) => fixWS` +
    • ${jump}
    • + `).join('\n')} +
    - `).join('\n')} - ` - }, + ${flashActData.map((act, i) => fixWS` +

    ${link.flash(act.flashes[0], {text: act.name})}

    +
    + ${getFlashGridHTML({ + entries: act.flashes.map(flash => ({item: flash})), + lazy: i === 0 ? 4 : true + })} +
    + `).join('\n')} + ` + }, + + nav: {simple: true} + }) + }; - nav: {simple: true} - })); + return [page]; } function writeFlashPage(flash, {wikiData}) { - return ({strings, writePage}) => writePage('flash', flash.directory, ({to}) => ({ - title: strings('flashPage.title', {flash: flash.name}), - theme: getThemeString(flash.color, [ - `--flash-directory: ${flash.directory}` - ]), - - main: { - content: fixWS` -

    ${strings('flashPage.title', {flash: flash.name})}

    - ${generateCoverLink({ - strings, to, wikiData, - src: to('media.flashArt', flash.directory), - alt: strings('misc.alt.flashArt') - })} -

    ${strings('releaseInfo.released', {date: strings.count.date(flash.date)})}

    - ${(flash.page || flash.urls.length) && `

    ${strings('releaseInfo.playOn', { - links: strings.list.or([ - flash.page && getFlashLink(flash), - ...flash.urls - ].map(url => fancifyFlashURL(url, flash, {strings}))) - })}

    `} - ${flash.tracks.length && fixWS` -

    Tracks featured in ${flash.name.replace(/\.$/, '')}:

    -
      - ${(flash.tracks - .map(track => strings('trackList.item.withArtists', { - track: strings.link.track(track, {strings, to}), - by: `${ - strings('trackList.item.withArtists.by', { - artists: getArtistString(track.artists, {strings, to}) - }) - }` - })) - .map(row => `
    • ${row}
    • `) - .join('\n'))} -
    - `} - ${flash.contributors.textContent && fixWS` -

    - ${strings('releaseInfo.contributors')} -
    - ${transformInline(flash.contributors.textContent, {strings, to, wikiData})} -

    - `} - ${flash.contributors.length && fixWS` -

    ${strings('releaseInfo.contributors')}

    -
      - ${flash.contributors - .map(contrib => `
    • ${getArtistString([contrib], { - strings, to, - showContrib: true, - showIcons: true - })}
    • `) - .join('\n')} -
    - `} - ` - }, + const page = { + type: 'page', + path: ['flash', flash.directory], + page: ({ + generateCoverLink, + getArtistString, + getFlashCover, + link, + strings, + transformInline + }) => ({ + title: strings('flashPage.title', {flash: flash.name}), + theme: getThemeString(flash.color, [ + `--flash-directory: ${flash.directory}` + ]), - sidebarLeft: generateSidebarForFlash(flash, {strings, to, wikiData}), - nav: generateNavForFlash(flash, {strings, to, wikiData}) - })); + main: { + content: fixWS` +

    ${strings('flashPage.title', {flash: flash.name})}

    + ${generateCoverLink({ + src: getFlashCover(flash), + alt: strings('misc.alt.flashArt') + })} +

    ${strings('releaseInfo.released', {date: strings.count.date(flash.date)})}

    + ${(flash.page || flash.urls.length) && `

    ${strings('releaseInfo.playOn', { + links: strings.list.or([ + flash.page && getFlashLink(flash), + ...flash.urls + ].map(url => fancifyFlashURL(url, flash, {strings}))) + })}

    `} + ${flash.tracks.length && fixWS` +

    Tracks featured in ${flash.name.replace(/\.$/, '')}:

    +
      + ${(flash.tracks + .map(track => strings('trackList.item.withArtists', { + track: link.track(track), + by: `${ + strings('trackList.item.withArtists.by', { + artists: getArtistString(track.artists) + }) + }` + })) + .map(row => `
    • ${row}
    • `) + .join('\n'))} +
    + `} + ${flash.contributors.textContent && fixWS` +

    + ${strings('releaseInfo.contributors')} +
    + ${transformInline(flash.contributors.textContent)} +

    + `} + ${flash.contributors.length && fixWS` +

    ${strings('releaseInfo.contributors')}

    +
      + ${flash.contributors + .map(contrib => `
    • ${getArtistString([contrib], { + showContrib: true, + showIcons: true + })}
    • `) + .join('\n')} +
    + `} + ` + }, + + sidebarLeft: generateSidebarForFlash(flash, {link, strings, wikiData}), + nav: generateNavForFlash(flash, {link, strings, wikiData}) + }) + }; + + return [page]; } -function generateNavForFlash(flash, {strings, to, wikiData}) { +function generateNavForFlash(flash, {link, strings, wikiData}) { const { flashData, wikiInfo } = wikiData; - const previousNextLinks = generatePreviousNextLinks('localized.flash', flash, flashData, {strings, to}); + const previousNextLinks = generatePreviousNextLinks(flash, { + link, strings, + data: flashData, + linkKey: 'flash' + }); return { links: [ { - href: to('localized.home'), + path: ['localized.home'], title: wikiInfo.shortName }, { - href: to('localized.flashIndex'), + path: ['localized.flashIndex'], title: strings('flashIndex.title') }, { html: strings('flashPage.nav.flash', { - flash: strings.link.flash(flash, {class: 'current', to}) + flash: link.flash(flash, {class: 'current'}) }) }, previousNextLinks && @@ -4122,7 +3851,7 @@ function generateNavForFlash(flash, {strings, to, wikiData}) { content: fixWS`
    ${chronologyLinks(flash, { - strings, to, wikiData, + link, strings, wikiData, headingString: 'misc.chronology.heading.flash', contribKey: 'contributors', getThings: artist => artist.flashes.asContributor @@ -4132,7 +3861,7 @@ function generateNavForFlash(flash, {strings, to, wikiData}) { }; } -function generateSidebarForFlash(flash, {strings, to, wikiData}) { +function generateSidebarForFlash(flash, {link, strings, wikiData}) { // all hard-coded, sorry :( // this doesnt have a super portable implementation/design...yet!! @@ -4152,7 +3881,7 @@ function generateSidebarForFlash(flash, {strings, to, wikiData}) { return { content: fixWS` -

    ${strings.link.flashIndex('', {to, text: strings('flashIndex.title')})}

    +

    ${link.flashIndex('', {text: strings('flashIndex.title')})}

    ${flashActData.filter(act => act.name.startsWith('Act 1') || @@ -4165,19 +3894,19 @@ function generateSidebarForFlash(flash, {strings, to, wikiData}) { true ))() ).flatMap(act => [ - act.name.startsWith('Act 1') && `
    Side 1 (Acts 1-5)
    ` - || act.name.startsWith('Act 6 Act 1') && `
    Side 2 (Acts 6-7)
    ` - || act.name.startsWith('Hiveswap Act 1') && `
    Outside Canon (Misc. Games)
    `, + act.name.startsWith('Act 1') && `
    ${link.flash(act.flashes[0], {color: '#4ac925', text: `Side 1 (Acts 1-5)`})}
    ` + || act.name.startsWith('Act 6 Act 1') && `
    ${link.flash(act.flashes[0], {color: '#1076a2', text: `Side 2 (Acts 6-7)`})}
    ` + || act.name.startsWith('Hiveswap Act 1') && `
    ${link.flash(act.flashes[0], {color: '#008282', text: `Outside Canon (Misc. Games)`})}
    `, (({index = flashActData.indexOf(act)} = {}) => ( index < act6 ? side === 1 : index < outsideCanon ? side === 2 : true ))() - && `
    ${act.name}
    `, + && `
    ${link.flash(act.flashes[0], {text: act.name})}
    `, act === currentAct && fixWS`
      ${act.flashes.map(f => fixWS` -
    • ${strings.link.flash(f, {to})}
    • +
    • ${link.flash(f)}
    • `).join('\n')}
    ` @@ -4197,9 +3926,9 @@ const listingSpec = [ .sort(sortByName); }, - row(album, {strings, to}) { + row(album, {link, strings}) { return strings('listingPage.listAlbums.byName.item', { - album: strings.link.album(album, {to}), + album: link.album(album), tracks: strings.count.tracks(album.tracks.length, {unit: true}) }); } @@ -4214,9 +3943,9 @@ const listingSpec = [ .sort((a, b) => b.tracks.length - a.tracks.length); }, - row(album, {strings, to}) { + row(album, {link, strings}) { return strings('listingPage.listAlbums.byTracks.item', { - album: strings.link.album(album, {to}), + album: link.album(album), tracks: strings.count.tracks(album.tracks.length, {unit: true}) }); } @@ -4232,9 +3961,9 @@ const listingSpec = [ .sort((a, b) => b.duration - a.duration); }, - row({album, duration}, {strings, to}) { + row({album, duration}, {link, strings}) { return strings('listingPage.listAlbums.byDuration.item', { - album: strings.link.album(album, {to}), + album: link.album(album), duration: strings.count.duration(duration) }); } @@ -4249,9 +3978,9 @@ const listingSpec = [ .filter(album => album.directory !== UNRELEASED_TRACKS_DIRECTORY)); }, - row(album, {strings, to}) { + row(album, {link, strings}) { return strings('listingPage.listAlbums.byDate.item', { - album: strings.link.album(album, {to}), + album: link.album(album), date: strings.count.date(album.date) }); } @@ -4268,7 +3997,7 @@ const listingSpec = [ }), ['dateAdded']); }, - html(chunks, {strings, to}) { + html(chunks, {link, strings}) { return fixWS`
    ${chunks.map(({dateAdded, chunk: albums}) => fixWS` @@ -4278,7 +4007,7 @@ const listingSpec = [
      ${(albums .map(album => strings('listingPage.listAlbums.byDateAdded.album', { - album: strings.link.album(album, {to}) + album: link.album(album) })) .map(row => `
    • ${row}
    • `) .join('\n'))} @@ -4299,10 +4028,10 @@ const listingSpec = [ .map(artist => ({artist, contributions: getArtistNumContributions(artist)})); }, - row({artist, contributions}, {strings, to}) { + row({artist, contributions}, {link, strings}) { return strings('listingPage.listArtists.byName.item', { - artist: strings.link.artist(artist, {to}), - contributions: strings.count.contributions(contributions, {to, unit: true}) + artist: link.artist(artist), + contributions: strings.count.contributions(contributions, {unit: true}) }); } }, @@ -4347,7 +4076,7 @@ const listingSpec = [ }; }, - html({toTracks, toArtAndFlashes, showAsFlashes}, {strings, to}) { + html({toTracks, toArtAndFlashes, showAsFlashes}, {link, strings}) { return fixWS`
      @@ -4355,7 +4084,7 @@ const listingSpec = [
        ${(toTracks .map(({ artist, contributions }) => strings('listingPage.listArtists.byContribs.item', { - artist: strings.link.artist(artist, {to}), + artist: link.artist(artist), contributions: strings.count.contributions(contributions, {unit: true}) })) .map(row => `
      • ${row}
      • `) @@ -4370,7 +4099,7 @@ const listingSpec = [
          ${(toArtAndFlashes .map(({ artist, contributions }) => strings('listingPage.listArtists.byContribs.item', { - artist: strings.link.artist(artist, {to}), + artist: link.artist(artist), contributions: strings.count.contributions(contributions, {unit: true}) })) .map(row => `
        • ${row}
        • `) @@ -4393,9 +4122,9 @@ const listingSpec = [ .sort((a, b) => b.entries - a.entries); }, - row({artist, entries}, {strings, to}) { + row({artist, entries}, {link, strings}) { return strings('listingPage.listArtists.byCommentary.item', { - artist: strings.link.artist(artist, {to}), + artist: link.artist(artist), entries: strings.count.commentaryEntries(entries, {unit: true}) }); } @@ -4414,9 +4143,9 @@ const listingSpec = [ .sort((a, b) => b.duration - a.duration); }, - row({artist, duration}, {strings, to}) { + row({artist, duration}, {link, strings}) { return strings('listingPage.listArtists.byDuration.item', { - artist: strings.link.artist(artist, {to}), + artist: link.artist(artist), duration: strings.count.duration(duration) }); } @@ -4468,7 +4197,7 @@ const listingSpec = [ }; }, - html({toTracks, toArtAndFlashes, showAsFlashes}, {strings, to}) { + html({toTracks, toArtAndFlashes, showAsFlashes}, {link, strings}) { return fixWS`
          @@ -4476,7 +4205,7 @@ const listingSpec = [
            ${(toTracks .map(({ artist, date }) => strings('listingPage.listArtists.byLatest.item', { - artist: strings.link.artist(artist, {to}), + artist: link.artist(artist), date: strings.count.date(date) })) .map(row => `
          • ${row}
          • `) @@ -4491,7 +4220,7 @@ const listingSpec = [
              ${(toArtAndFlashes .map(({ artist, date }) => strings('listingPage.listArtists.byLatest.item', { - artist: strings.link.artist(artist, {to}), + artist: link.artist(artist), date: strings.count.date(date) })) .map(row => `
            • ${row}
            • `) @@ -4509,11 +4238,10 @@ const listingSpec = [ condition: ({wikiData}) => wikiData.wikiInfo.features.groupUI, data: ({wikiData}) => wikiData.groupData.slice().sort(sortByName), - row(group, {strings, to}) { + row(group, {link, strings}) { return strings('listingPage.listGroups.byCategory.group', { - group: strings.link.groupInfo(group, {to}), - gallery: strings.link.groupGallery(group, { - to, + group: link.groupInfo(group), + gallery: link.groupGallery(group, { text: strings('listingPage.listGroups.byCategory.group.gallery') }) }); @@ -4526,19 +4254,18 @@ const listingSpec = [ condition: ({wikiData}) => wikiData.wikiInfo.features.groupUI, data: ({wikiData}) => wikiData.groupCategoryData, - html(groupCategoryData, {strings, to}) { + html(groupCategoryData, {link, strings}) { return fixWS`
              ${groupCategoryData.map(category => fixWS`
              ${strings('listingPage.listGroups.byCategory.category', { - category: strings.link.groupInfo(category.groups[0], {to, text: category.name}) + category: link.groupInfo(category.groups[0], {text: category.name}) })}
                ${(category.groups .map(group => strings('listingPage.listGroups.byCategory.group', { - group: strings.link.groupInfo(group, {to}), - gallery: strings.link.groupGallery(group, { - to, + group: link.groupInfo(group), + gallery: link.groupGallery(group, { text: strings('listingPage.listGroups.byCategory.group.gallery') }) })) @@ -4562,9 +4289,9 @@ const listingSpec = [ .sort((a, b) => b.albums - a.albums); }, - row({group, albums}, {strings, to}) { + row({group, albums}, {link, strings}) { return strings('listingPage.listGroups.byAlbums.item', { - group: strings.link.groupInfo(group, {to}), + group: link.groupInfo(group), albums: strings.count.albums(albums, {unit: true}) }); } @@ -4581,9 +4308,9 @@ const listingSpec = [ .sort((a, b) => b.tracks - a.tracks); }, - row({group, tracks}, {strings, to}) { + row({group, tracks}, {link, strings}) { return strings('listingPage.listGroups.byTracks.item', { - group: strings.link.groupInfo(group, {to}), + group: link.groupInfo(group), tracks: strings.count.tracks(tracks, {unit: true}) }); } @@ -4600,9 +4327,9 @@ const listingSpec = [ .sort((a, b) => b.duration - a.duration); }, - row({group, duration}, {strings, to}) { + row({group, duration}, {link, strings}) { return strings('listingPage.listGroups.byDuration.item', { - group: strings.link.groupInfo(group, {to}), + group: link.groupInfo(group), duration: strings.count.duration(duration) }); } @@ -4631,9 +4358,9 @@ const listingSpec = [ .reverse()).reverse() }, - row({group, date}, {strings, to}) { + row({group, date}, {link, strings}) { return strings('listingPage.listGroups.byLatest.item', { - group: strings.link.groupInfo(group, {to}), + group: link.groupInfo(group), date: strings.count.date(date) }); } @@ -4647,9 +4374,9 @@ const listingSpec = [ return wikiData.trackData.slice().sort(sortByName); }, - row(track, {strings, to}) { + row(track, {link, strings}) { return strings('listingPage.listTracks.byName.item', { - track: strings.link.track(track, {to}) + track: link.track(track) }); } }, @@ -4659,17 +4386,17 @@ const listingSpec = [ title: ({strings}) => strings('listingPage.listTracks.byAlbum.title'), data: ({wikiData}) => wikiData.albumData, - html(albumData, {strings, to}) { + html(albumData, {link, strings}) { return fixWS`
                ${albumData.map(album => fixWS`
                ${strings('listingPage.listTracks.byAlbum.album', { - album: strings.link.album(album, {to}) + album: link.album(album) })}
                  ${(album.tracks .map(track => strings('listingPage.listTracks.byAlbum.track', { - track: strings.link.track(track, {to}) + track: link.track(track) })) .map(row => `
                1. ${row}
                2. `) .join('\n'))} @@ -4691,22 +4418,22 @@ const listingSpec = [ ); }, - html(chunks, {strings, to}) { + html(chunks, {link, strings}) { return fixWS`
                  ${chunks.map(({album, date, chunk: tracks}) => fixWS`
                  ${strings('listingPage.listTracks.byDate.album', { - album: strings.link.album(album, {to}), + album: link.album(album), date: strings.count.date(date) })}
                    ${(tracks .map(track => track.aka ? `
                  • ${strings('listingPage.listTracks.byDate.track.rerelease', { - track: strings.link.track(track, {to}) + track: link.track(track) })}
                  • ` : `
                  • ${strings('listingPage.listTracks.byDate.track', { - track: strings.link.track(track, {to}) + track: link.track(track) })}
                  • `) .join('\n'))}
                  @@ -4728,9 +4455,9 @@ const listingSpec = [ .sort((a, b) => b.duration - a.duration); }, - row({track, duration}, {strings, to}) { + row({track, duration}, {link, strings}) { return strings('listingPage.listTracks.byDuration.item', { - track: strings.link.track(track, {to}), + track: link.track(track), duration: strings.count.duration(duration) }); } @@ -4747,17 +4474,17 @@ const listingSpec = [ })); }, - html(albums, {strings, to}) { + html(albums, {link, strings}) { return fixWS`
                  ${albums.map(({album, tracks}) => fixWS`
                  ${strings('listingPage.listTracks.byDurationInAlbum.album', { - album: strings.link.album(album, {to}) + album: link.album(album) })}
                    ${(tracks .map(track => strings('listingPage.listTracks.byDurationInAlbum.track', { - track: strings.link.track(track, {to}), + track: link.track(track), duration: strings.count.duration(track.duration) })) .map(row => `
                  • ${row}
                  • `) @@ -4780,9 +4507,9 @@ const listingSpec = [ .sort((a, b) => b.timesReferenced - a.timesReferenced); }, - row({track, timesReferenced}, {strings, to}) { + row({track, timesReferenced}, {link, strings}) { return strings('listingPage.listTracks.byTimesReferenced.item', { - track: strings.link.track(track, {to}), + track: link.track(track), timesReferenced: strings.count.timesReferenced(timesReferenced, {unit: true}) }); } @@ -4799,19 +4526,19 @@ const listingSpec = [ .filter(({ album }) => album.directory !== UNRELEASED_TRACKS_DIRECTORY); }, - html(chunks, {strings, to}) { + html(chunks, {link, strings}) { return fixWS`
                    ${chunks.map(({album, chunk: tracks}) => fixWS`
                    ${strings('listingPage.listTracks.inFlashes.byAlbum.album', { - album: strings.link.album(album, {to}), + album: link.album(album), date: strings.count.date(album.date) })}
                      ${(tracks .map(track => strings('listingPage.listTracks.inFlashes.byAlbum.track', { - track: strings.link.track(track, {to}), - flashes: strings.list.and(track.flashes.map(flash => strings.link.flash(flash, {to}))) + track: link.track(track), + flashes: strings.list.and(track.flashes.map(link.flash)) })) .map(row => `
                    • ${row}
                    • `) .join('\n'))} @@ -4828,19 +4555,19 @@ const listingSpec = [ condition: ({wikiData}) => wikiData.wikiInfo.features.flashesAndGames, data: ({wikiData}) => wikiData.flashData, - html(flashData, {strings, to}) { + html(flashData, {link, strings}) { return fixWS`
                      ${sortByDate(flashData.slice()).map(flash => fixWS`
                      ${strings('listingPage.listTracks.inFlashes.byFlash.flash', { - flash: strings.link.flash(flash, {to}), + flash: link.flash(flash), date: strings.count.date(flash.date) })}
                        ${(flash.tracks .map(track => strings('listingPage.listTracks.inFlashes.byFlash.track', { - track: strings.link.track(track, {to}), - album: strings.link.album(track.album, {to}) + track: link.track(track), + album: link.album(track.album) })) .map(row => `
                      • ${row}
                      • `) .join('\n'))} @@ -4859,18 +4586,18 @@ const listingSpec = [ return chunkByProperties(wikiData.trackData.filter(t => t.lyrics), ['album']); }, - html(chunks, {strings, to}) { + html(chunks, {link, strings}) { return fixWS`
                        ${chunks.map(({album, chunk: tracks}) => fixWS`
                        ${strings('listingPage.listTracks.withLyrics.album', { - album: strings.link.album(album, {to}), + album: link.album(album), date: strings.count.date(album.date) })}
                          ${(tracks .map(track => strings('listingPage.listTracks.withLyrics.track', { - track: strings.link.track(track, {to}), + track: link.track(track), })) .map(row => `
                        • ${row}
                        • `) .join('\n'))} @@ -4893,9 +4620,9 @@ const listingSpec = [ .map(tag => ({tag, timesUsed: tag.things.length})); }, - row({tag, timesUsed}, {strings, to}) { + row({tag, timesUsed}, {link, strings}) { return strings('listingPage.listTags.byName.item', { - tag: strings.link.tag(tag, {to}), + tag: link.tag(tag), timesUsed: strings.count.timesUsed(timesUsed, {unit: true}) }); } @@ -4913,9 +4640,9 @@ const listingSpec = [ .sort((a, b) => b.timesUsed - a.timesUsed); }, - row({tag, timesUsed}, {strings, to}) { + row({tag, timesUsed}, {link, strings}) { return strings('listingPage.listTags.byUses.item', { - tag: strings.link.tag(tag, {to}), + tag: link.tag(tag), timesUsed: strings.count.timesUsed(timesUsed, {unit: true}) }); } @@ -4930,7 +4657,7 @@ const listingSpec = [ fandomAlbumData: wikiData.fandomAlbumData }), - html: ({officialAlbumData, fandomAlbumData}, {strings, to}) => fixWS` + html: ({officialAlbumData, fandomAlbumData}, {strings}) => fixWS`

                          Choose a link to go to a random page in that category or album! If your browser doesn't support relatively modern JavaScript or you've disabled it, these links won't work - sorry.

                          (Data files are downloading in the background! Please wait for data to load.)

                          (Data files have finished being downloaded. The links should work!)

                          @@ -4978,30 +4705,39 @@ function writeListingIndex({wikiData}) { const releasedAlbums = albumData.filter(album => album.directory !== UNRELEASED_TRACKS_DIRECTORY); const duration = getTotalDuration(releasedTracks); - return ({strings, writePage}) => writePage('listingIndex', '', ({to}) => ({ - title: strings('listingIndex.title'), - - main: { - content: fixWS` -

                          ${strings('listingIndex.title')}

                          -

                          ${strings('listingIndex.infoLine', { - wiki: wikiInfo.name, - tracks: `${strings.count.tracks(releasedTracks.length, {unit: true})}`, - albums: `${strings.count.albums(releasedAlbums.length, {unit: true})}`, - duration: `${strings.count.duration(duration, {approximate: true, unit: true})}` - })}

                          -
                          -

                          ${strings('listingIndex.exploreList')}

                          - ${generateLinkIndexForListings(null, {strings, to, wikiData})} - ` - }, + const page = { + type: 'page', + path: ['listingIndex'], + page: ({ + strings, + link + }) => ({ + title: strings('listingIndex.title'), - sidebarLeft: { - content: generateSidebarForListings(null, {strings, to, wikiData}) - }, + main: { + content: fixWS` +

                          ${strings('listingIndex.title')}

                          +

                          ${strings('listingIndex.infoLine', { + wiki: wikiInfo.name, + tracks: `${strings.count.tracks(releasedTracks.length, {unit: true})}`, + albums: `${strings.count.albums(releasedAlbums.length, {unit: true})}`, + duration: `${strings.count.duration(duration, {approximate: true, unit: true})}` + })}

                          +
                          +

                          ${strings('listingIndex.exploreList')}

                          + ${generateLinkIndexForListings(null, {link, strings, wikiData})} + ` + }, + + sidebarLeft: { + content: generateSidebarForListings(null, {link, strings, wikiData}) + }, - nav: {simple: true} - })) + nav: {simple: true} + }) + }; + + return [page]; } function writeListingPage(listing, {wikiData}) { @@ -5015,68 +4751,70 @@ function writeListingPage(listing, {wikiData}) { ? listing.data({wikiData}) : null); - return ({strings, writePage}) => writePage('listing', listing.directory, ({to}) => ({ - title: listing.title({strings}), + const page = { + type: 'page', + path: ['listing', listing.directory], + page: ({ + link, + strings + }) => ({ + title: listing.title({strings}), - main: { - content: fixWS` -

                          ${listing.title({strings})}

                          - ${listing.html && (listing.data - ? listing.html(data, {strings, to}) - : listing.html({strings, to}))} - ${listing.row && fixWS` -
                            - ${(data - .map(item => listing.row(item, {strings, to})) - .map(row => `
                          • ${row}
                          • `) - .join('\n'))} -
                          - `} - ` - }, + main: { + content: fixWS` +

                          ${listing.title({strings})}

                          + ${listing.html && (listing.data + ? listing.html(data, {link, strings}) + : listing.html({link, strings}))} + ${listing.row && fixWS` +
                            + ${(data + .map(item => listing.row(item, {link, strings})) + .map(row => `
                          • ${row}
                          • `) + .join('\n'))} +
                          + `} + ` + }, - sidebarLeft: { - content: generateSidebarForListings(listing, {strings, to, wikiData}) - }, + sidebarLeft: { + content: generateSidebarForListings(listing, {link, strings, wikiData}) + }, - nav: { - links: [ - { - href: to('localized.home'), - title: wikiInfo.shortName - }, - { - href: to('localized.listingIndex'), - title: strings('listingIndex.title') - }, - { - href: '', - title: listing.title({strings}) - } - ] - } - })); + nav: { + links: [ + {toHome: true}, + { + path: ['localized.listingIndex'], + title: strings('listingIndex.title') + }, + {toCurrentPage: true} + ] + } + }) + }; + + return [page]; } -function generateSidebarForListings(currentListing, {strings, to, wikiData}) { +function generateSidebarForListings(currentListing, {link, strings, wikiData}) { return fixWS` -

                          ${strings.link.listingIndex('', {text: strings('listingIndex.title'), to})}

                          - ${generateLinkIndexForListings(currentListing, {strings, to, wikiData})} +

                          ${link.listingIndex('', {text: strings('listingIndex.title')})}

                          + ${generateLinkIndexForListings(currentListing, {link, strings, wikiData})} `; } -function generateLinkIndexForListings(currentListing, {strings, to, wikiData}) { +function generateLinkIndexForListings(currentListing, {link, strings, wikiData}) { const { listingSpec } = wikiData; return fixWS`
                            ${(listingSpec .filter(({ condition }) => !condition || condition({wikiData})) - .map(listing => fixWS` -
                          • - ${listing.title({strings})} -
                          • - `) + .map(listing => html.tag('li', + {class: [listing === currentListing && 'current']}, + link.listing(listing, {text: listing.title({strings})}) + )) .join('\n'))}
                          `; @@ -5113,35 +4851,44 @@ function writeCommentaryIndex({wikiData}) { const totalEntries = data.reduce((acc, {entries}) => acc + entries.length, 0); const totalWords = data.reduce((acc, {words}) => acc + words, 0); - return ({strings, writePage}) => writePage('commentaryIndex', '', ({to}) => ({ - title: strings('commentaryIndex.title'), + const page = { + type: 'page', + path: ['commentaryIndex'], + page: ({ + link, + strings + }) => ({ + title: strings('commentaryIndex.title'), - main: { - content: fixWS` -
                          -

                          ${strings('commentaryIndex.title')}

                          -

                          ${strings('commentaryIndex.infoLine', { - words: `${strings.count.words(totalWords, {unit: true})}`, - entries: `${strings.count.commentaryEntries(totalEntries, {unit: true})}` - })}

                          -

                          ${strings('commentaryIndex.albumList.title')}

                          -
                            - ${data - .map(({ album, entries, words }) => fixWS` -
                          • ${strings('commentaryIndex.albumList.item', { - album: strings.link.albumCommentary(album, {to}), - words: strings.count.words(words, {unit: true}), - entries: strings.count.commentaryEntries(entries.length, {unit: true}) - })}
                          • - `) - .join('\n')} -
                          -
                          - ` - }, + main: { + content: fixWS` +
                          +

                          ${strings('commentaryIndex.title')}

                          +

                          ${strings('commentaryIndex.infoLine', { + words: `${strings.count.words(totalWords, {unit: true})}`, + entries: `${strings.count.commentaryEntries(totalEntries, {unit: true})}` + })}

                          +

                          ${strings('commentaryIndex.albumList.title')}

                          +
                            + ${data + .map(({ album, entries, words }) => fixWS` +
                          • ${strings('commentaryIndex.albumList.item', { + album: link.albumCommentary(album), + words: strings.count.words(words, {unit: true}), + entries: strings.count.commentaryEntries(entries.length, {unit: true}) + })}
                          • + `) + .join('\n')} +
                          +
                          + ` + }, - nav: {simple: true} - })); + nav: {simple: true} + }) + }; + + return [page]; } function writeAlbumCommentaryPage(album, {wikiData}) { @@ -5150,57 +4897,66 @@ function writeAlbumCommentaryPage(album, {wikiData}) { const entries = [album, ...album.tracks].filter(x => x.commentary).map(x => x.commentary); const words = entries.join(' ').split(' ').length; - return ({strings, writePage}) => writePage('albumCommentary', album.directory, ({to}) => ({ - title: strings('albumCommentaryPage.title', {album: album.name}), - stylesheet: getAlbumStylesheet(album, {to}), - theme: getThemeString(album.color), - - main: { - content: fixWS` -
                          -

                          ${strings('albumCommentaryPage.title', { - album: strings.link.album(album, {to}) - })}

                          -

                          ${strings('albumCommentaryPage.infoLine', { - words: `${strings.count.words(words, {unit: true})}`, - entries: `${strings.count.commentaryEntries(entries.length, {unit: true})}` - })}

                          - ${album.commentary && fixWS` -

                          ${strings('albumCommentaryPage.entry.title.albumCommentary')}

                          -
                          - ${transformMultiline(album.commentary, {strings, to, wikiData})} -
                          - `} - ${album.tracks.filter(t => t.commentary).map(track => fixWS` -

                          ${strings('albumCommentaryPage.entry.title.trackCommentary', { - track: strings.link.track(track, {to}) - })}

                          -
                          - ${transformMultiline(track.commentary, {strings, to, wikiData})} -
                          - `).join('\n')} -
                          - ` - }, + const page = { + type: 'page', + path: ['albumCommentary', album.directory], + page: ({ + getAlbumStylesheet, + link, + strings, + to, + transformMultiline + }) => ({ + title: strings('albumCommentaryPage.title', {album: album.name}), + stylesheet: getAlbumStylesheet(album), + theme: getThemeString(album.color), - nav: { - links: [ - { - href: to('localized.home'), - title: wikiInfo.shortName - }, - { - href: to('localized.commentaryIndex'), - title: strings('commentaryIndex.title') - }, - { - html: strings('albumCommentaryPage.nav.album', { - album: strings.link.albumCommentary(album, {class: 'current', to}) - }) - } - ] - } - })); + main: { + content: fixWS` +
                          +

                          ${strings('albumCommentaryPage.title', { + album: link.album(album) + })}

                          +

                          ${strings('albumCommentaryPage.infoLine', { + words: `${strings.count.words(words, {unit: true})}`, + entries: `${strings.count.commentaryEntries(entries.length, {unit: true})}` + })}

                          + ${album.commentary && fixWS` +

                          ${strings('albumCommentaryPage.entry.title.albumCommentary')}

                          +
                          + ${transformMultiline(album.commentary)} +
                          + `} + ${album.tracks.filter(t => t.commentary).map(track => fixWS` +

                          ${strings('albumCommentaryPage.entry.title.trackCommentary', { + track: link.track(track) + })}

                          +
                          + ${transformMultiline(track.commentary)} +
                          + `).join('\n')} +
                          + ` + }, + + nav: { + links: [ + {toHome: true}, + { + path: ['localized.commentaryIndex'], + title: strings('commentaryIndex.title') + }, + { + html: strings('albumCommentaryPage.nav.album', { + album: link.albumCommentary(album, {class: 'current'}) + }) + } + ] + } + }) + }; + + return [page]; } function writeTagPages({wikiData}) { @@ -5219,61 +4975,70 @@ function writeTagPage(tag, {wikiData}) { const { wikiInfo } = wikiData; const { things } = tag; - return ({strings, writePage}) => writePage('tag', tag.directory, ({to}) => ({ - title: strings('tagPage.title', {tag: tag.name}), - theme: getThemeString(tag.color), - - main: { - classes: ['top-index'], - content: fixWS` -

                          ${strings('tagPage.title', {tag: tag.name})}

                          -

                          ${strings('tagPage.infoLine', { - coverArts: strings.count.coverArts(things.length, {unit: true}) - })}

                          -
                          - ${getGridHTML({ - strings, to, - entries: things.map(item => ({item})), - srcFn: thing => (thing.album - ? getTrackCover(thing, {to}) - : getAlbumCover(thing, {to})), - hrefFn: thing => (thing.album - ? to('localized.track', thing.directory) - : to('localized.album', thing.directory)) - })} -
                          - ` - }, + const page = { + type: 'page', + path: ['tag', tag.directory], + page: ({ + getAlbumCover, + getGridHTML, + getTrackCover, + link, + strings, + to + }) => ({ + title: strings('tagPage.title', {tag: tag.name}), + theme: getThemeString(tag.color), - nav: { - links: [ - { - href: to('localized.home'), - title: wikiInfo.shortName - }, - wikiInfo.features.listings && - { - href: to('localized.listingIndex'), - title: strings('listingIndex.title') - }, - { - html: strings('tagPage.nav.tag', { - tag: strings.link.tag(tag, {class: 'current', to}) - }) - } - ] - } - })); + main: { + classes: ['top-index'], + content: fixWS` +

                          ${strings('tagPage.title', {tag: tag.name})}

                          +

                          ${strings('tagPage.infoLine', { + coverArts: strings.count.coverArts(things.length, {unit: true}) + })}

                          +
                          + ${getGridHTML({ + entries: things.map(item => ({item})), + srcFn: thing => (thing.album + ? getTrackCover(thing) + : getAlbumCover(thing)), + hrefFn: thing => (thing.album + ? to('localized.track', thing.directory) + : to('localized.album', thing.directory)) + })} +
                          + ` + }, + + nav: { + links: [ + {toHome: true}, + wikiInfo.features.listings && + { + path: ['localized.listingIndex'], + title: strings('listingIndex.title') + }, + {toCurrentPage: true} + ] + } + }) + }; + + return [page]; } -function getArtistString(artists, {strings, to, showIcons = false, showContrib = false}) { +function getArtistString(artists, { + iconifyURL, link, strings, + showIcons = false, + showContrib = false +}) { return strings.list.and(artists.map(({ who, what }) => { const { urls, directory, name } = who; return [ - strings.link.artist(who, {to}), + link.artist(who), showContrib && what && `(${what})`, showIcons && urls.length && `(${ - strings.list.unit(urls.map(url => iconifyURL(url, {strings, to}))) + strings.list.unit(urls.map(url => iconifyURL(url, {strings}))) })` ].filter(Boolean).join(' '); })); @@ -5359,10 +5124,12 @@ function iconifyURL(url, {strings, to}) { } function chronologyLinks(currentThing, { - strings, to, wikiData, - headingString, contribKey, - getThings + getThings, + headingString, + link, + strings, + wikiData }) { const { albumData } = wikiData; @@ -5390,13 +5157,21 @@ function chronologyLinks(currentThing, { const previous = releasedThings[index - 1]; const next = releasedThings[index + 1]; const parts = [ - previous && `Previous`, - next && `Next` + previous && linkAnythingMan(previous, { + link, + text: strings('misc.nav.previous'), + wikiData + }), + next && linkAnythingMan(next, { + link, + text: strings('misc.nav.next'), + wikiData + }) ].filter(Boolean); const stringOpts = { index: strings.count.index(index + 1, {strings}), - artist: strings.link.artist(artist, {to}) + artist: link.artist(artist) }; return fixWS` @@ -5408,12 +5183,16 @@ function chronologyLinks(currentThing, { }).filter(Boolean).join('\n'); } -function generateAlbumNavLinks(album, currentTrack, {strings, to}) { +function generateAlbumNavLinks(album, currentTrack, {link, strings}) { if (album.tracks.length <= 1) { return ''; } - const previousNextLinks = currentTrack && generatePreviousNextLinks('localized.track', currentTrack, album.tracks, {strings, to}) + const previousNextLinks = currentTrack && generatePreviousNextLinks(currentTrack, { + link, strings, + data: album.tracks, + linkKey: 'track' + }); const randomLink = `${ (currentTrack ? strings('trackPage.nav.random') @@ -5425,34 +5204,44 @@ function generateAlbumNavLinks(album, currentTrack, {strings, to}) { : `(${randomLink})`); } -function generateAlbumChronologyLinks(album, currentTrack, {strings, to}) { +function generateAlbumChronologyLinks(album, currentTrack, {link, strings}) { return [ currentTrack && chronologyLinks(currentTrack, { - strings, to, wikiData, - headingString: 'misc.chronology.heading.track', contribKey: 'artists', - getThings: artist => [...artist.tracks.asArtist, ...artist.tracks.asContributor] + getThings: artist => [...artist.tracks.asArtist, ...artist.tracks.asContributor], + headingString: 'misc.chronology.heading.track', + strings, + link, + wikiData }), chronologyLinks(currentTrack || album, { - strings, to, wikiData, - headingString: 'misc.chronology.heading.coverArt', contribKey: 'coverArtists', - getThings: artist => [...artist.albums.asCoverArtist, ...artist.tracks.asCoverArtist] + getThings: artist => [...artist.albums.asCoverArtist, ...artist.tracks.asCoverArtist], + headingString: 'misc.chronology.heading.coverArt', + link, + strings, + wikiData }) ].filter(Boolean).join('\n'); } -function generateSidebarForAlbum(album, currentTrack, {strings, to, wikiData}) { +function generateSidebarForAlbum(album, { + currentTrack = null, + link, + strings, + transformMultiline, + wikiData +}) { const listTag = getAlbumListTag(album); const trackToListItem = track => `
                        • ${ strings('albumSidebar.trackList.item', { - track: strings.link.track(track, {to}) + track: link.track(track) }) }
                        • `; const trackListPart = fixWS` -

                          ${album.name}

                          +

                          ${link.album(album)}

                          ${(album.trackGroups ? album.trackGroups.map(({ name, color, startIndex, tracks }) => html.tag('details', { @@ -5496,10 +5285,10 @@ function generateSidebarForAlbum(album, currentTrack, {strings, to, wikiData}) { }).map(({group, next, previous}) => fixWS`

                          ${ strings('albumSidebar.groupBox.title', { - group: `${group.name}` + group: link.groupInfo(group) }) }

                          - ${!currentTrack && transformMultiline(group.descriptionShort, {strings, to, wikiData})} + ${!currentTrack && transformMultiline(group.descriptionShort)} ${group.urls.length && `

                          ${ strings('releaseInfo.visitOn', { links: strings.list.or(group.urls.map(url => fancifyURL(url, {strings}))) @@ -5508,12 +5297,12 @@ function generateSidebarForAlbum(album, currentTrack, {strings, to, wikiData}) { ${!currentTrack && fixWS` ${next && `

                          `} ${previous && ``} `} @@ -5543,69 +5332,92 @@ function generateSidebarForAlbum(album, currentTrack, {strings, to, wikiData}) { } } -function generateSidebarForGroup(currentGroup, {isGallery, strings, to, wikiData}) { +function generateSidebarForGroup(currentGroup, isGallery, {link, strings, wikiData}) { const { groupCategoryData, wikiInfo } = wikiData; if (!wikiInfo.features.groupUI) { return null; } - const urlKey = isGallery ? 'localized.groupGallery' : 'localized.groupInfo'; + const linkKey = isGallery ? 'groupGallery' : 'groupInfo'; return { content: fixWS`

                          ${strings('groupSidebar.title')}

                          -
                          - ${groupCategoryData.map(category => [ - fixWS` -
                          ${ - strings('groupSidebar.groupList.category', { - category: `${category.name}` - }) - }
                          -
                            - ${category.groups.map(group => fixWS` -
                          • ${ - strings('groupSidebar.groupList.item', { - group: `${group.name}` - }) - }
                          • - `).join('\n')} -
                          - ` - ]).join('\n')} + ${groupCategoryData.map(category => + html.tag('details', { + open: category === currentGroup.category, + class: category === currentGroup.category && 'current' + }, [ + html.tag('summary', + {style: getLinkThemeString(category.color)}, + strings('groupSidebar.groupList.category', { + category: `${category.name}` + })), + html.tag('ul', + category.groups.map(group => fixWS` +
                        • ${ + strings('groupSidebar.groupList.item', { + group: link[linkKey](group) + }) + }
                        • + `)) + ])).join('\n')}
                          ` }; } -function generateInfoGalleryLinks(urlKeyInfo, urlKeyGallery, currentThing, isGallery, {strings, to}) { +function generateInfoGalleryLinks(currentThing, isGallery, { + link, strings, + linkKeyGallery, + linkKeyInfo +}) { return [ - strings.link[urlKeyInfo](currentThing, { - to, + link[linkKeyInfo](currentThing, { class: isGallery ? '' : 'current', text: strings('misc.nav.info') }), - strings.link[urlKeyGallery](currentThing, { - to, + link[linkKeyGallery](currentThing, { class: isGallery ? 'current' : '', text: strings('misc.nav.gallery') }) ].join(', '); } -function generatePreviousNextLinks(urlKey, currentThing, thingData, {strings, to}) { - const index = thingData.indexOf(currentThing); - const previous = thingData[index - 1]; - const next = thingData[index + 1]; +function generatePreviousNextLinks(current, { + data, + link, + linkKey, + strings +}) { + const linkFn = link[linkKey]; + + const index = data.indexOf(current); + const previous = data[index - 1]; + const next = data[index + 1]; return [ - previous && `${strings('misc.nav.previous')}`, - next && `${strings('misc.nav.next')}` + previous && linkFn(previous, { + attributes: { + id: 'previous-button', + title: previous.name + }, + text: strings('misc.nav.previous'), + color: false + }), + next && linkFn(next, { + attributes: { + id: 'next-button', + title: next.name + }, + text: strings('misc.nav.next'), + color: false + }) ].filter(Boolean).join(', '); } -function generateNavForGroup(currentGroup, {isGallery, strings, to, wikiData}) { +function generateNavForGroup(currentGroup, isGallery, {link, strings, wikiData}) { const { groupData, wikiInfo } = wikiData; if (!wikiInfo.features.groupUI) { @@ -5615,23 +5427,29 @@ function generateNavForGroup(currentGroup, {isGallery, strings, to, wikiData}) { const urlKey = isGallery ? 'localized.groupGallery' : 'localized.groupInfo'; const linkKey = isGallery ? 'groupGallery' : 'groupInfo'; - const infoGalleryLinks = generateInfoGalleryLinks('groupInfo', 'groupGallery', currentGroup, isGallery, {strings, to}); - const previousNextLinks = generatePreviousNextLinks(urlKey, currentGroup, groupData, {strings, to}) + const infoGalleryLinks = generateInfoGalleryLinks(currentGroup, isGallery, { + link, strings, + linkKeyGallery: 'groupGallery', + linkKeyInfo: 'groupInfo' + }); + + const previousNextLinks = generatePreviousNextLinks(currentGroup, { + link, strings, + data: groupData, + linkKey + }); return { links: [ - { - href: to('localized.home'), - title: wikiInfo.shortName - }, + {toHome: true}, wikiInfo.features.listings && { - href: to('localized.listingIndex'), + path: ['localized.listingIndex'], title: strings('listingIndex.title') }, { html: strings('groupPage.nav.group', { - group: strings.link[linkKey](currentGroup, {class: 'current', to}) + group: link[linkKey](currentGroup, {class: 'current'}) }) }, { @@ -5655,8 +5473,14 @@ function writeGroupPage(group, {wikiData}) { const releasedTracks = releasedAlbums.flatMap(album => album.tracks); const totalDuration = getTotalDuration(releasedTracks); - return async ({strings, writePage}) => { - await writePage('groupInfo', group.directory, ({to}) => ({ + const infoPage = { + type: 'page', + path: ['groupInfo', group.directory], + page: ({ + link, + strings, + transformMultiline + }) => ({ title: strings('groupInfoPage.title', {group: group.name}), theme: getThemeString(group.color), @@ -5669,14 +5493,14 @@ function writeGroupPage(group, {wikiData}) { }) }

                          `}
                          - ${transformMultiline(group.description, {strings, to, wikiData})} + ${transformMultiline(group.description)}

                          ${strings('groupInfoPage.albumList.title')}

                          ${ strings('groupInfoPage.viewAlbumGallery', { - link: `${ - strings('groupInfoPage.viewAlbumGallery.link') - }` + link: link.groupGallery(group, { + text: strings('groupInfoPage.viewAlbumGallery.link') + }) }) }

                            @@ -5684,7 +5508,7 @@ function writeGroupPage(group, {wikiData}) {
                          • ${ strings('groupInfoPage.albumList.item', { year: album.date.getFullYear(), - album: `${album.name}` + album: link.album(album) }) }
                          • `).join('\n')} @@ -5692,11 +5516,19 @@ function writeGroupPage(group, {wikiData}) { ` }, - sidebarLeft: generateSidebarForGroup(group, {isGallery: false, strings, to, wikiData}), - nav: generateNavForGroup(group, {isGallery: false, strings, to, wikiData}) - })); + sidebarLeft: generateSidebarForGroup(group, false, {link, strings, wikiData}), + nav: generateNavForGroup(group, false, {link, strings, wikiData}) + }) + }; - await writePage('groupGallery', group.directory, ({to}) => ({ + const galleryPage = { + type: 'page', + path: ['groupGallery', group.directory], + page: ({ + getAlbumGridHTML, + link, + strings + }) => ({ title: strings('groupGalleryPage.title', {group: group.name}), theme: getThemeString(group.color), @@ -5711,10 +5543,16 @@ function writeGroupPage(group, {wikiData}) { time: `${strings.count.duration(totalDuration, {unit: true})}` }) }

                            - ${wikiInfo.features.groupUI && wikiInfo.features.listings && `

                            (Choose another group to filter by!)

                            `} + ${wikiInfo.features.groupUI && wikiInfo.features.listings && html.tag('p', + {class: 'quick-info'}, + strings('groupGalleryPage.anotherGroupLine', { + link: link.listing(listingSpec.find(l => l.directory === 'groups/by-category'), { + text: strings('groupGalleryPage.anotherGroupLine.link') + }) + }) + )}
                            ${getAlbumGridHTML({ - strings, to, entries: sortByDate(group.albums.map(item => ({item}))).reverse(), details: true })} @@ -5722,18 +5560,22 @@ function writeGroupPage(group, {wikiData}) { ` }, - sidebarLeft: generateSidebarForGroup(group, {isGallery: true, strings, to, wikiData}), - nav: generateNavForGroup(group, {isGallery: true, strings, to, wikiData}) - })); + sidebarLeft: generateSidebarForGroup(group, true, {link, strings, wikiData}), + nav: generateNavForGroup(group, true, {link, strings, wikiData}) + }) }; + + return [infoPage, galleryPage]; } -function toAnythingMan(anythingMan, {to, wikiData}) { +// RIP toAnythingMan (previously getHrefOfAnythingMan), 2020-05-25<>2021-05-14. +// ........Yet the function 8reathes life anew as linkAnythingMan! ::::) +function linkAnythingMan(anythingMan, {link, wikiData, ...opts}) { return ( - wikiData.albumData.includes(anythingMan) ? to('localized.album', anythingMan.directory) : - wikiData.trackData.includes(anythingMan) ? to('localized.track', anythingMan.directory) : - wikiData.flashData?.includes(anythingMan) ? to('localized.flash', anythingMan.directory) : - 'idk-bud' + wikiData.albumData.includes(anythingMan) ? link.album(anythingMan, opts) : + wikiData.trackData.includes(anythingMan) ? link.track(anythingMan, opts) : + wikiData.flashData?.includes(anythingMan) ? link.flash(anythingMan, opts) : + 'idk bud' ) } @@ -5751,6 +5593,10 @@ function getTrackCover(track, {to}) { } } +function getFlashCover(flash, {to}) { + return to('media.flashArt', flash.directory); +} + function getFlashLink(flash) { return `https://homestuck.com/story/${flash.page}`; } @@ -5780,21 +5626,17 @@ async function processLanguageFile(file, defaultStrings = null) { defaultJSON: defaultStrings?.json, bindUtilities: { count, - link, // Technically unnecessary 8ut future-proofing, 'mkay? list } }); } -// Wrapper function for running a function once for all languages. It provides: -// * the language strings -// * a shadowing writePages function for outputing to the appropriate subdir -// * a shadowing urls object for linking to the appropriate relative paths -async function wrapLanguages(fn, {writeOneLanguage = null, wikiData}) { - const k = writeOneLanguage +// Wrapper function for running a function once for all languages. +async function wrapLanguages(fn, {writeOneLanguage = null}) { + const k = writeOneLanguage; const languagesToRun = (k ? {[k]: languages[k]} - : languages) + : languages); const entries = Object.entries(languagesToRun) .filter(([ key ]) => key !== 'default'); @@ -5804,21 +5646,16 @@ async function wrapLanguages(fn, {writeOneLanguage = null, wikiData}) { const baseDirectory = (strings === languages.default ? '' : strings.code); - const shadow_writePage = (urlKey, directory, pageFn) => writePage(urlKey, directory, pageFn, {baseDirectory, strings, wikiData}); - - // 8ring the utility functions over too! - Object.assign(shadow_writePage, writePage); - await fn({ baseDirectory, - strings, - wikiData, - writePage: shadow_writePage + strings }, i, entries); } } async function main() { + Error.stackTraceLimit = Infinity; + const WD = wikiData; WD.listingSpec = listingSpec; @@ -6344,7 +6181,9 @@ async function main() { parent.splice(0, parent.length, ...parent.filter(obj => { if (!obj[key]) { logWarn`Unexpected null in ${obj.name} (value key ${key})`; + return false; } + return true; })); }; @@ -6479,8 +6318,8 @@ async function main() { // The writeThingPages functions don't actually immediately do any file // writing themselves; an initial call will only gather the relevant data // which is *then* used for writing. So the return value is a function - // (or an array of functions) which expects {writePage, strings}, and - // *that's* what we call after -- multiple times, once for each language. + // (or an array of functions) which expects {strings}, and *that's* what + // we call after -- multiple times, once for each language. let writes; { let error = false; @@ -6522,37 +6361,11 @@ async function main() { if (error) { return; } - - // The modern(TM) return format for each writeThingPages function is an - // array of arrays, each of which's items are 8ig Complicated Objects - // that 8asically look like {type, path, content}. 8ut surprise, these - // aren't actually implemented in most places yet! So, we transform - // stuff in the old format here. 'Scept keep in mind, the OLD FORMAT - // doesn't really give us most of the info we want for Cool And Modern - // Reasons, so they're going into a fancy {type: 'legacy'} sort of - // o8ject, with a plain {write} property for, uh, the writing stuff, - // same as usual. - // - // I promise this document8tion will get 8etter when we make progress - // actually moving old pages over. Also it'll 8e hecks of less work - // than previous restructures, don't worry. - writes = writes.map(entry => - typeof entry === 'object' ? entry : - typeof entry === 'function' ? {type: 'legacy', write: entry} : - {type: 'wut', entry}); - - const wut = writes.filter(({ type }) => type === 'wut'); - if (wut.length) { - // Oh g*d oh h*ck. - logError`Uhhhhh writes contains something 8esides o8jects and functions?`; - logError`Definitely a 8ug!`; - console.log(wut); - return; - } } - const localizedWrites = writes.filter(({ type }) => type === 'page' || type === 'legacy'); + const pageWrites = writes.filter(({ type }) => type === 'page'); const dataWrites = writes.filter(({ type }) => type === 'data'); + const redirectWrites = writes.filter(({ type }) => type === 'redirect'); await progressPromiseAll(`Writing data files shared across languages.`, queue( // TODO: This only supports one <>-style argument. @@ -6560,28 +6373,146 @@ async function main() { queueSize )); - await wrapLanguages(async ({strings, ...opts}, i, entries) => { + const perLanguageFn = async ({strings, ...opts}, i, entries) => { console.log(`\x1b[34;1m${ (`[${i + 1}/${entries.length}] ${strings.code} (-> /${opts.baseDirectory}) ` .padEnd(60, '-')) }\x1b[0m`); - await progressPromiseAll(`Writing ${strings.code}`, queue( - localizedWrites.map(({type, ...props}) => () => { - switch (type) { - case 'legacy': { - const { write } = props; - return write({strings, ...opts}); - } - case 'page': { - const { path, page } = props; - // TODO: This only supports one <>-style argument. - return opts.writePage(path[0], path[1], ({to}) => page({strings, to})); - } - } + + await progressPromiseAll(`Writing ${strings.code}`, queue([ + ...pageWrites.map(({type, ...props}) => () => { + const { path, page } = props; + const { baseDirectory } = opts; + + // TODO: This only supports one <>-style argument. + const pageSubKey = path[0]; + const directory = path[1]; + + const paths = writePage.paths(baseDirectory, 'localized.' + pageSubKey, directory); + const to = writePage.to({baseDirectory, pageSubKey, paths}); + + // TODO: Is there some nicer way to define these, + // may8e without totally re-8inding everything for + // each page? + const bound = {}; + + bound.link = withEntries(unbound_link, entries => entries + .map(([ key, fn ]) => [key, bindOpts(fn, {to})])); + + bound.parseAttributes = bindOpts(parseAttributes, { + to + }); + + bound.transformInline = bindOpts(transformInline, { + link: bound.link, + replacerSpec, + strings, + to, + wikiData + }); + + bound.transformMultiline = bindOpts(transformMultiline, { + transformInline: bound.transformInline, + parseAttributes: bound.parseAttributes + }); + + bound.transformLyrics = bindOpts(transformLyrics, { + transformInline: bound.transformInline, + transformMultiline: bound.transformMultiline + }); + + bound.iconifyURL = bindOpts(iconifyURL, { + strings, + to + }); + + bound.getArtistString = bindOpts(getArtistString, { + iconifyURL: bound.iconifyURL, + link: bound.link, + strings + }); + + bound.getAlbumCover = bindOpts(getAlbumCover, { + to + }); + + bound.getTrackCover = bindOpts(getTrackCover, { + to + }); + + bound.getFlashCover = bindOpts(getFlashCover, { + to + }); + + bound.generateCoverLink = bindOpts(generateCoverLink, { + [bindOpts.bindIndex]: 0, + link: bound.link, + strings, + wikiData + }); + + bound.getGridHTML = bindOpts(getGridHTML, { + [bindOpts.bindIndex]: 0, + strings + }); + + bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, { + [bindOpts.bindIndex]: 0, + getAlbumCover: bound.getAlbumCover, + getGridHTML: bound.getGridHTML, + strings, + to + }); + + bound.getFlashGridHTML = bindOpts(getFlashGridHTML, { + [bindOpts.bindIndex]: 0, + getFlashCover: bound.getFlashCover, + getGridHTML: bound.getGridHTML, + to + }); + + bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, { + to + }); + + const pageFn = () => page({ + ...bound, + strings, + to + }); + + const content = writePage.html(pageFn, { + paths, + strings, + to, + transformMultiline: bound.transformMultiline, + wikiData + }); + + return writePage.write(content, {paths}); }), - queueSize - )); - }, {writeOneLanguage, wikiData}); + ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => { + const { baseDirectory } = opts; + + const title = titleFn({ + strings + }); + + // TODO: This only supports one <>-style argument. + const fromPaths = writePage.paths(baseDirectory, 'localized.' + fromPath[0], fromPath[1]); + const to = writePage.to({baseDirectory, pageSubKey: fromPath[0], paths: fromPaths}); + + const target = to('localized.' + toPath[0], ...toPath.slice(1)); + const content = generateRedirectPage(title, target, {strings}); + return writePage.write(content, {paths: fromPaths}); + }) + ], queueSize)); + }; + + await wrapLanguages(perLanguageFn, { + writeOneLanguage, + wikiData + }); decorateTime.displayTime(); diff --git a/src/util/html.js b/src/util/html.js index 4895301..9475698 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -73,18 +73,20 @@ export function escapeAttributeValue(value) { export function attributes(attribs) { return Object.entries(attribs) .map(([ key, val ]) => { - if (!val) - return [key, val]; - else if (typeof val === 'string' || typeof val === 'boolean') - return [key, val]; + if (typeof val === 'undefined' || val === null) + return [key, val, false]; + else if (typeof val === 'string') + return [key, val, true]; + else if (typeof val === 'boolean') + return [key, val, val]; else if (typeof val === 'number') - return [key, val.toString()]; + return [key, val.toString(), true]; else if (Array.isArray(val)) - return [key, val.join(' ')]; + return [key, val.filter(Boolean).join(' '), val.length > 0]; else throw new Error(`Attribute value for ${key} should be primitive or array, got ${typeof val}`); }) - .filter(([ key, val ]) => val) + .filter(([ key, val, keep ]) => keep) .map(([ key, val ]) => (typeof val === 'boolean' ? `${key}` : `${key}="${escapeAttributeValue(val)}"`)) diff --git a/src/util/link.js b/src/util/link.js index e5c3c59..107b35f 100644 --- a/src/util/link.js +++ b/src/util/link.js @@ -14,15 +14,21 @@ import { getLinkThemeString } from './colors.js' const linkHelper = (hrefFn, {color = true, attr = null} = {}) => (thing, { - strings, to, + to, text = '', + attributes = null, class: className = '', + color: color2 = true, hash = '' }) => ( html.tag('a', { ...attr ? attr(thing) : {}, + ...attributes ? attributes : {}, href: hrefFn(thing, {to}) + (hash ? (hash.startsWith('#') ? '' : '#') + hash : ''), - style: color ? getLinkThemeString(thing.color) : '', + style: ( + typeof color2 === 'string' ? getLinkThemeString(color2) : + color2 && color ? getLinkThemeString(thing.color) : + ''), class: className }, text || thing.name) ); diff --git a/src/util/replacer.js b/src/util/replacer.js new file mode 100644 index 0000000..a1e880e --- /dev/null +++ b/src/util/replacer.js @@ -0,0 +1,424 @@ +import find from './find.js'; +import {logError} from './cli.js'; +import {escapeRegex} from './sugar.js'; + +export function validateReplacerSpec(replacerSpec, link) { + let success = true; + + for (const [key, {link: linkKey, find: findKey, value, html}] of Object.entries(replacerSpec)) { + if (!html && !link[linkKey]) { + logError`The replacer spec ${key} has invalid link key ${linkKey}! Specify it in link specs or fix typo.`; + success = false; + } + if (findKey && !find[findKey]) { + logError`The replacer spec ${key} has invalid find key ${findKey}! Specify it in find specs or fix typo.`; + success = false; + } + } + + return success; +} + +// Syntax literals. +const tagBeginning = '[['; +const tagEnding = ']]'; +const tagReplacerValue = ':'; +const tagHash = '#'; +const tagArgument = '*'; +const tagArgumentValue = '='; +const tagLabel = '|'; + +const noPrecedingWhitespace = '(? ({i, type: 'error', data: {message}}); +const endOfInput = (i, comment) => makeError(i, `Unexpected end of input (${comment}).`); + +// These are 8asically stored on the glo8al scope, which might seem odd +// for a recursive function, 8ut the values are only ever used immediately +// after they're set. +let stopped, + stop_iMatch, + stop_iParse, + stop_literal; + +function parseOneTextNode(input, i, stopAt) { + return parseNodes(input, i, stopAt, true)[0]; +} + +function parseNodes(input, i, stopAt, textOnly) { + let nodes = []; + let escapeNext = false; + let string = ''; + let iString = 0; + + stopped = false; + + const pushTextNode = (isLast) => { + string = input.slice(iString, i); + + // If this is the last text node 8efore stopping (at a stopAt match + // or the end of the input), trim off whitespace at the end. + if (isLast) { + string = string.trimEnd(); + } + + if (string.length) { + nodes.push({i: iString, iEnd: i, type: 'text', data: string}); + string = ''; + } + }; + + const literalsToMatch = stopAt ? stopAt.concat([R_tagBeginning]) : [R_tagBeginning]; + + // The 8ackslash stuff here is to only match an even (or zero) num8er + // of sequential 'slashes. Even amounts always cancel out! Odd amounts + // don't, which would mean the following literal is 8eing escaped and + // should 8e counted only as part of the current string/text. + // + // Inspired 8y this: https://stackoverflow.com/a/41470813 + const regexpSource = `(?= 0) { + lineStart += 1; + } else { + lineStart = 0; + } + + let lineEnd = input.slice(i).indexOf('\n'); + if (lineEnd >= 0) { + lineEnd += i; + } else { + lineEnd = input.length; + } + + const line = input.slice(lineStart, lineEnd); + + const cursor = i - lineStart; + + throw new SyntaxError(fixWS` + Parse error (at pos ${i}): ${message} + ${line} + ${'-'.repeat(cursor) + '^'} + `); + } +} + +function evaluateTag(node, opts) { + const { input, link, replacerSpec, strings, to, wikiData } = opts; + + const source = input.slice(node.i, node.iEnd); + + const replacerKey = node.data.replacerKey?.data || 'track'; + + if (!replacerSpec[replacerKey]) { + logWarn`The link ${source} has an invalid replacer key!`; + return source; + } + + const { + find: findKey, + link: linkKey, + value: valueFn, + html: htmlFn, + transformName + } = replacerSpec[replacerKey]; + + const replacerValue = transformNodes(node.data.replacerValue, opts); + + const value = ( + valueFn ? valueFn(replacerValue) : + findKey ? find[findKey](replacerValue, {wikiData}) : + { + directory: replacerValue, + name: null + }); + + if (!value) { + logWarn`The link ${source} does not match anything!`; + return source; + } + + const enteredLabel = node.data.label && transformNode(node.data.label, opts); + + const label = (enteredLabel + || transformName && transformName(value.name, node, input) + || value.name); + + if (!valueFn && !label) { + logWarn`The link ${source} requires a label be entered!`; + return source; + } + + const hash = node.data.hash && transformNodes(node.data.hash, opts); + + const args = node.data.args && Object.fromEntries(node.data.args.map( + ({ key, value }) => [ + transformNode(key, opts), + transformNodes(value, opts) + ])); + + const fn = (htmlFn + ? htmlFn + : link[linkKey]); + + try { + return fn(value, {text: label, hash, args, strings, to}); + } catch (error) { + logError`The link ${source} failed to be processed: ${error}`; + return source; + } +} + +function transformNode(node, opts) { + if (!node) { + throw new Error('Expected a node!'); + } + + if (Array.isArray(node)) { + throw new Error('Got an array - use transformNodes here!'); + } + + switch (node.type) { + case 'text': + return node.data; + case 'tag': + return evaluateTag(node, opts); + default: + throw new Error(`Unknown node type ${node.type}`); + } +} + +function transformNodes(nodes, opts) { + if (!nodes || !Array.isArray(nodes)) { + throw new Error(`Expected an array of nodes! Got: ${nodes}`); + } + + return nodes.map(node => transformNode(node, opts)).join(''); +} + +export function transformInline(input, {replacerSpec, link, strings, to, wikiData}) { + if (!replacerSpec) throw new Error('Expected replacerSpec'); + if (!link) throw new Error('Expected link'); + if (!strings) throw new Error('Expected strings'); + if (!to) throw new Error('Expected to'); + if (!wikiData) throw new Error('Expected wikiData'); + + const nodes = parseInput(input); + return transformNodes(nodes, {input, link, replacerSpec, strings, to, wikiData}); +} diff --git a/src/util/strings.js b/src/util/strings.js index 99104aa..c066435 100644 --- a/src/util/strings.js +++ b/src/util/strings.js @@ -1,4 +1,5 @@ import { logWarn } from './cli.js'; +import { bindOpts } from './sugar.js'; // Localiz8tion time! Or l10n as the neeeeeeeerds call it. Which is a terri8le // name and not one I intend on using, thank you very much. (Don't even get me @@ -194,13 +195,13 @@ export function genStrings(stringsJSON, { } }; - const bindOpts = (obj, bind) => Object.fromEntries(Object.entries(obj).map( - ([ key, fn ]) => [key, (value, opts = {}) => fn(value, {...bind, ...opts})] - )); - // And the provided utility dictionaries themselves, of course! for (const [key, utilDict] of Object.entries(bindUtilities)) { - strings[key] = bindOpts(utilDict, {strings}); + const boundUtilDict = {}; + for (const [key, fn] of Object.entries(utilDict)) { + boundUtilDict[key] = bindOpts(fn, {strings}); + } + strings[key] = boundUtilDict; } return strings; diff --git a/src/util/sugar.js b/src/util/sugar.js index c24c617..79a271b 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -76,3 +76,14 @@ export function delay(ms) { export function escapeRegex(string) { return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } + +export function bindOpts(fn, bind) { + const bindIndex = bind[bindOpts.bindIndex] ?? 1; + + return (...args) => { + const opts = args[bindIndex] ?? {}; + return fn(...args.slice(0, bindIndex), {...bind, ...opts}); + }; +} + +bindOpts.bindIndex = Symbol(); -- cgit 1.3.0-6-gf8a5