diff options
Diffstat (limited to 'src/upd8.js')
-rwxr-xr-x | src/upd8.js | 2232 |
1 files changed, 105 insertions, 2127 deletions
diff --git a/src/upd8.js b/src/upd8.js index 920f9039..0d9ec718 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -31,38 +31,18 @@ // Oh yeah, like. Just run this through some relatively recent version of // node.js and you'll 8e fine. ...Within the project root. O8viously. +import {execSync} from 'child_process'; import * as path from 'path'; import {fileURLToPath} from 'url'; -import chroma from 'chroma-js'; - -import { - copyFile, - mkdir, - stat, - symlink, - writeFile, - unlink, -} from 'fs/promises'; - -import { execSync } from 'child_process'; - import genThumbs from './gen-thumbs.js'; import {listingSpec, listingTargetSpec} from './listing-spec.js'; import urlSpec from './url-spec.js'; -import * as pageSpecs from './page/index.js'; -import find, {bindFind} from './util/find.js'; -import * as html from './util/html.js'; -import {getColors} from './util/colors.js'; -import {findFiles} from './util/io.js'; -import {isMain} from './util/node-utils.js'; +import {processLanguageFile} from './data/language.js'; import CacheableObject from './data/things/cacheable-object.js'; -import {processLanguageFile} from './data/language.js'; -import {serializeThings} from './data/serialize.js'; - import { filterDuplicateDirectories, filterReferenceErrors, @@ -72,33 +52,17 @@ import { WIKI_INFO_FILE, } from './data/yaml.js'; -import { - fancifyFlashURL, - fancifyURL, - generateAdditionalFilesShortcut, - generateAdditionalFilesList, - generateChronologyLinks, - generateCoverLink, - generateInfoGalleryLinks, - generateNavigationLinks, - generateStickyHeadingContainer, - generateTrackListDividedByGroups, - getAlbumGridHTML, - getAlbumStylesheet, - getArtistString, - getFlashGridHTML, - getFooterLocalizationLinks, - getGridHTML, - getCarouselHTML, - getRevealStringFromTags, - getRevealStringFromWarnings, - getThemeString as unbound_getThemeString, - iconifyURL, -} from './misc-templates.js'; - -import unbound_link, { - getLinkThemeString as unbound_getLinkThemeString, -} from './util/link.js'; +import find from './util/find.js'; +import {findFiles} from './util/io.js'; +import link from './util/link.js'; +import {isMain} from './util/node-utils.js'; +import {validateReplacerSpec} from './util/replacer.js'; +import {empty, showAggregate} from './util/sugar.js'; +import {replacerSpec} from './util/transform-content.js'; +import {generateURLs} from './util/urls.js'; + +import {generateDevelopersCommentHTML} from './write/page-template.js'; +import * as buildModes from './write/build-modes/index.js'; import { color, @@ -111,15 +75,6 @@ import { progressPromiseAll, } from './util/cli.js'; -import {validateReplacerSpec, transformInline} from './util/replacer.js'; - -import { - getAlbumCover, - getArtistAvatar, - getFlashCover, - getTrackCover, -} from './util/wiki-data.js'; - /* import { serializeContribs, @@ -131,18 +86,6 @@ import { } from './util/serialize.js'; */ -import { - bindOpts, - queue, - showAggregate, - withEntries, -} from './util/sugar.js'; - -import {generateURLs, thumb} from './util/urls.js'; - -// Pensive emoji! -import { OFFICIAL_GROUP_DIRECTORY } from './util/magic-constants.js'; - import FileSizePreloader from './file-size-preloader.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -160,1497 +103,46 @@ const BUILD_TIME = new Date(); const DEFAULT_STRINGS_FILE = 'strings-default.json'; -// Code that's common 8etween the 8uild code (i.e. upd8.js) and gener8ted -// site code should 8e put here. Which, uh, ~~only really means this one -// file~~ is now a variety of useful utilities! -// -// Rather than hard code it, anything in this directory can 8e shared across -// 8oth ends of the code8ase. -// (This gets symlinked into the --data-path directory.) -const UTILITY_DIRECTORY = 'util'; - -// Code that's used only in the static site! CSS, cilent JS, etc. -// (This gets symlinked into the --data-path directory.) -const STATIC_DIRECTORY = 'static'; - -// This exists adjacent to index.html for any page with oEmbed metadata. -const OEMBED_JSON_FILE = 'oembed.json'; - -// Automatically copied (if present) from media directory to site root. -const FAVICON_FILE = 'favicon.ico'; - -// Shared varia8les! These are more efficient to access than a shared varia8le -// (or at least I h8pe so), and are easier to pass across functions than a -// 8unch of specific arguments. -// -// Upd8: Okay yeah these aren't actually any different. Still cleaner than -// passing around a data object containing all this, though. -let dataPath; -let mediaPath; -let langPath; -let outputPath; - -// Glo8al data o8ject shared 8etween 8uild functions and all that. This keeps -// everything encapsul8ted in one place, so it's easy to pass and share across -// modules! -let wikiData = {}; - -let queueSize; - -const urls = generateURLs(urlSpec); - -function splitLines(text) { - return text.split(/\r\n|\r|\n/); -} - -const replacerSpec = { - album: { - find: 'album', - link: 'album', - }, - 'album-commentary': { - find: 'album', - link: 'albumCommentary', - }, - 'album-gallery': { - find: 'album', - link: 'albumGallery', - }, - artist: { - find: 'artist', - link: 'artist', - }, - 'artist-gallery': { - find: 'artist', - link: 'artistGallery', - }, - 'commentary-index': { - find: null, - link: 'commentaryIndex', - }, - date: { - find: null, - value: (ref) => new Date(ref), - html: (date, {language}) => - html.tag('time', - {datetime: date.toString()}, - language.formatDate(date)), - }, - 'flash-index': { - find: null, - link: 'flashIndex', - }, - flash: { - find: 'flash', - link: 'flash', - transformName(name, node, input) { - const nextCharacter = input[node.iEnd]; - const lastCharacter = name[name.length - 1]; - if (![' ', '\n', '<'].includes(nextCharacter) && lastCharacter === '.') { - return name.slice(0, -1); - } else { - return name; - } - }, - }, - group: { - find: 'group', - link: 'groupInfo', - }, - 'group-gallery': { - find: 'group', - link: 'groupGallery', - }, - home: { - find: null, - link: 'home', - }, - 'listing-index': { - find: null, - link: 'listingIndex', - }, - listing: { - find: 'listing', - link: 'listing', - }, - media: { - find: null, - link: 'media', - }, - 'news-index': { - find: null, - link: 'newsIndex', - }, - 'news-entry': { - find: 'newsEntry', - link: 'newsEntry', - }, - root: { - find: null, - link: 'root', - }, - site: { - find: null, - link: 'site', - }, - static: { - find: 'staticPage', - link: 'staticPage', - }, - string: { - find: null, - value: (ref) => ref, - html: (ref, {language, args}) => language.$(ref, args), - }, - tag: { - find: 'artTag', - link: 'tag', - }, - track: { - find: 'track', - link: 'track', - }, -}; - -if (!validateReplacerSpec(replacerSpec, {find, link: unbound_link})) { +if (!validateReplacerSpec(replacerSpec, {find, link})) { process.exit(); } -function parseAttributes(string, {to}) { - const attributes = Object.create(null); - const skipWhitespace = (i) => { - const ws = /\s/; - if (ws.test(string[i])) { - const match = string.slice(i).match(/[^\s]/); - if (match) { - return i + match.index; - } else { - return string.length; - } - } else { - return i; - } - }; - - for (let i = 0; i < string.length; ) { - i = skipWhitespace(i); - const aStart = i; - const aEnd = i + string.slice(i).match(/[\s=]|$/).index; - const attribute = string.slice(aStart, aEnd); - i = skipWhitespace(aEnd); - if (string[i] === '=') { - i = skipWhitespace(i + 1); - let end, endOffset; - if (string[i] === '"' || string[i] === "'") { - end = string[i]; - endOffset = 1; - i++; - } else { - end = '\\s'; - endOffset = 0; - } - const vStart = i; - const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index; - const value = string.slice(vStart, vEnd); - i = vEnd + endOffset; - if (attribute === 'src' && value.startsWith('media/')) { - attributes[attribute] = to('media.path', value.slice('media/'.length)); - } else { - attributes[attribute] = value; - } - } else { - attributes[attribute] = attribute; - } - } - return Object.fromEntries( - Object.entries(attributes).map(([key, val]) => [ - key, - val === 'true' - ? true - : val === 'false' - ? false - : val === key - ? true - : val, - ]) - ); -} - -function joinLineBreaks(sourceLines) { - const outLines = []; - - let lineSoFar = ''; - for (let i = 0; i < sourceLines.length; i++) { - const line = sourceLines[i]; - lineSoFar += line; - if (!line.endsWith('<br>')) { - outLines.push(lineSoFar); - lineSoFar = ''; - } - } - - if (lineSoFar) { - outLines.push(lineSoFar); - } - - return outLines; -} - -function transformMultiline(text, { - parseAttributes, - transformInline, - thumb = null, -}) { - // Heck yes, HTML magics. - - text = transformInline(text.trim()); - - const outLines = []; - - const indentString = ' '.repeat(4); - - let levelIndents = []; - const openLevel = (indent) => { - // opening a sublist is a pain: to be semantically *and* visually - // correct, we have to append the <ul> at the end of the existing - // previous <li> - const previousLine = outLines[outLines.length - 1]; - if (previousLine?.endsWith('</li>')) { - // we will re-close the <li> later - outLines[outLines.length - 1] = previousLine.slice(0, -5) + ' <ul>'; - } else { - // if the previous line isn't a list item, this is the opening of - // the first list level, so no need for indent - outLines.push('<ul>'); - } - levelIndents.push(indent); - }; - const closeLevel = () => { - levelIndents.pop(); - if (levelIndents.length) { - // closing a sublist, so close the list item containing it too - outLines.push(indentString.repeat(levelIndents.length) + '</ul></li>'); - } else { - // closing the final list level! no need for indent here - outLines.push('</ul>'); - } - }; - - // okay yes we should support nested formatting, more than one blockquote - // layer, etc, but hear me out here: making all that work would basically - // be the same as implementing an entire markdown converter, which im not - // interested in doing lol. sorry!!! - let inBlockquote = false; - - let lines = splitLines(text); - lines = joinLineBreaks(lines); - for (let line of lines) { - const imageLine = line.startsWith('<img'); - line = line.replace(/<img (.*?)>/g, (match, attributes) => - img({ - lazy: true, - link: true, - thumb, - ...parseAttributes(attributes), - }) - ); - - let indentThisLine = 0; - let lineContent = line; - let lineTag = 'p'; - - const listMatch = line.match(/^( *)- *(.*)$/); - if (listMatch) { - // is a list item! - if (!levelIndents.length) { - // first level is always indent = 0, regardless of actual line - // content (this is to avoid going to a lesser indent than the - // initial level) - openLevel(0); - } else { - // find level corresponding to indent - const indent = listMatch[1].length; - let i; - for (i = levelIndents.length - 1; i >= 0; i--) { - if (levelIndents[i] <= indent) break; - } - // note: i cannot equal -1 because the first indentation level - // is always 0, and the minimum indentation is also 0 - if (levelIndents[i] === indent) { - // same indent! return to that level - while (levelIndents.length - 1 > i) closeLevel(); - // (if this is already the current level, the above loop - // will do nothing) - } else if (levelIndents[i] < indent) { - // lesser indent! branch based on index - if (i === levelIndents.length - 1) { - // top level is lesser: add a new level - openLevel(indent); - } else { - // lower level is lesser: return to that level - while (levelIndents.length - 1 > i) closeLevel(); - } - } - } - // finally, set variables for appending content line - indentThisLine = levelIndents.length; - lineContent = listMatch[2]; - lineTag = 'li'; - } else { - // not a list item! close any existing list levels - while (levelIndents.length) closeLevel(); - - // like i said, no nested shenanigans - quotes only appear outside - // of lists. sorry! - const quoteMatch = line.match(/^> *(.*)$/); - if (quoteMatch) { - // is a quote! open a blockquote tag if it doesnt already exist - if (!inBlockquote) { - inBlockquote = true; - outLines.push('<blockquote>'); - } - indentThisLine = 1; - lineContent = quoteMatch[1]; - } else if (inBlockquote) { - // not a quote! close a blockquote tag if it exists - inBlockquote = false; - outLines.push('</blockquote>'); - } - - // let some escaped symbols display as the normal symbol, since the - // point of escaping them is just to avoid having them be treated as - // syntax markers! - if (lineContent.match(/( *)\\-/)) { - lineContent = lineContent.replace('\\-', '-'); - } else if (lineContent.match(/( *)\\>/)) { - lineContent = lineContent.replace('\\>', '>'); - } - } - - if (lineTag === 'p') { - // certain inline element tags should still be postioned within a - // paragraph; other elements (e.g. headings) should be added as-is - const elementMatch = line.match(/^<(.*?)[ >]/); - if ( - elementMatch && - !imageLine && - ![ - 'a', - 'abbr', - 'b', - 'bdo', - 'br', - 'cite', - 'code', - 'data', - 'datalist', - 'del', - 'dfn', - 'em', - 'i', - 'img', - 'ins', - 'kbd', - 'mark', - 'output', - 'picture', - 'q', - 'ruby', - 'samp', - 'small', - 'span', - 'strong', - 'sub', - 'sup', - 'svg', - 'time', - 'var', - 'wbr', - ].includes(elementMatch[1]) - ) { - lineTag = ''; - } - - // for sticky headings! - if (elementMatch) { - lineContent = lineContent.replace(/<h2/, `<h2 class="content-heading"`) - } - } - - let pushString = indentString.repeat(indentThisLine); - if (lineTag) { - pushString += `<${lineTag}>${lineContent}</${lineTag}>`; - } else { - pushString += lineContent; - } - outLines.push(pushString); - } - - // after processing all lines... - - // if still in a list, close all levels - while (levelIndents.length) closeLevel(); - - // if still in a blockquote, close its tag - if (inBlockquote) { - inBlockquote = false; - outLines.push('</blockquote>'); - } - - return outLines.join('\n'); -} - -function transformLyrics(text, {transformInline, transformMultiline}) { - // Different from transformMultiline 'cuz it joins multiple lines together - // with line 8reaks (<br>); transformMultiline treats each line as its own - // complete paragraph (or list, etc). - - // If it looks like old data, then like, oh god. - // Use the normal transformMultiline tool. - if (text.includes('<br')) { - return transformMultiline(text); - } - - text = transformInline(text.trim()); - - let buildLine = ''; - const addLine = () => outLines.push(`<p>${buildLine}</p>`); - const outLines = []; - for (const line of text.split('\n')) { - if (line.length) { - if (buildLine.length) { - buildLine += '<br>'; - } - buildLine += line; - } else if (buildLine.length) { - addLine(); - buildLine = ''; - } - } - if (buildLine.length) { - addLine(); - } - return outLines.join('\n'); -} - -function stringifyThings(thingData) { - return JSON.stringify(serializeThings(thingData)); -} - -function img({ - src, - alt, - noSrcText = '', - thumb: thumbKey, - reveal, - id, - class: className, - width, - height, - link = false, - lazy = false, - square = false, -}) { - const willSquare = square; - const willLink = typeof link === 'string' || link; - - const originalSrc = src; - const thumbSrc = src && (thumbKey ? thumb[thumbKey](src) : src); - - const imgAttributes = { - id: link ? '' : id, - class: className, - alt, - width, - height, - }; - - const noSrcHTML = - !src && - wrap( - html.tag('div', - {class: 'image-text-area'}, - noSrcText)); - - const nonlazyHTML = - src && - wrap( - html.tag('img', { - ...imgAttributes, - src: thumbSrc, - })); - - const lazyHTML = - src && - lazy && - wrap( - html.tag('img', - { - ...imgAttributes, - class: [className, 'lazy'], - 'data-original': thumbSrc, - }), - true); - - if (!src) { - return noSrcHTML; - } else if (lazy) { - return html.tag('noscript', nonlazyHTML) + '\n' + lazyHTML; - } else { - return nonlazyHTML; - } - - function wrap(input, hide = false) { - let wrapped = input; - - wrapped = html.tag('div', {class: 'image-container'}, wrapped); - - if (reveal) { - wrapped = html.tag('div', {class: 'reveal'}, [ - wrapped, - html.tag('span', {class: 'reveal-text'}, reveal), - ]); - } - - if (willSquare) { - wrapped = html.tag('div', {class: 'square-content'}, wrapped); - wrapped = html.tag('div', - {class: ['square', hide && !willLink && 'js-hide']}, - wrapped); - } - - if (willLink) { - wrapped = html.tag('a', - { - id, - class: ['box', hide && 'js-hide'], - href: typeof link === 'string' ? link : originalSrc, - }, - wrapped); - } - - return wrapped; - } -} - -function validateWritePath(path, urlGroup) { - if (!Array.isArray(path)) { - return {error: `Expected array, got ${path}`}; - } - - const {paths} = urlGroup; - - const definedKeys = Object.keys(paths); - const specifiedKey = path[0]; - - if (!definedKeys.includes(specifiedKey)) { - return {error: `Specified key ${specifiedKey} isn't defined`}; - } - - const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0; - const specifiedArgs = path.length - 1; - - if (specifiedArgs !== expectedArgs) { - return { - error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`, - }; - } - - return {success: true}; -} - -function validateWriteObject(obj) { - if (typeof obj !== 'object') { - return {error: `Expected object, got ${typeof obj}`}; - } - - if (typeof obj.type !== 'string') { - return {error: `Expected type to be string, got ${obj.type}`}; - } - - switch (obj.type) { - case 'legacy': { - if (typeof obj.write !== 'function') { - return {error: `Expected write to be string, got ${obj.write}`}; - } - - break; - } - - case 'page': { - const path = validateWritePath(obj.path, urlSpec.localized); - if (path.error) { - return {error: `Path validation failed: ${path.error}`}; - } - - if (typeof obj.page !== 'function') { - return {error: `Expected page to be function, got ${obj.content}`}; - } - - break; - } - - case 'data': { - const path = validateWritePath(obj.path, urlSpec.data); - if (path.error) { - return {error: `Path validation failed: ${path.error}`}; - } - - if (typeof obj.data !== 'function') { - return {error: `Expected data to be function, got ${obj.data}`}; - } - - 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}`}; - } - } - - return {success: true}; -} - -export function getURLsFrom({ - baseDirectory, - pageSubKey, - paths, -}) { - return (targetFullKey, ...args) => { - const [groupKey, subKey] = targetFullKey.split('.'); - let path = paths.subdirectoryPrefix; - - let from; - let to; - - // 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' && - groupKey !== 'localizedDefaultLanguage' && - baseDirectory - ) { - from = 'localizedWithBaseDirectory.' + pageSubKey; - to = targetFullKey; - } else if (groupKey === 'localizedDefaultLanguage' && baseDirectory) { - // Special case for specifically linking *from* a page with base - // directory *to* a page without! Used for the language switcher and - // hopefully nothing else oh god. - from = 'localizedWithBaseDirectory.' + pageSubKey; - to = 'localized.' + subKey; - } else if (groupKey === 'localizedDefaultLanguage') { - // Linking to the default, except surprise, we're already IN the default - // (no baseDirectory set). - from = 'localized.' + pageSubKey; - to = 'localized.' + subKey; - } else { - // If we're linking inside the localized area (or there just is no - // 8ase directory), the 8ase directory doesn't matter. - from = 'localized.' + pageSubKey; - to = targetFullKey; - } - - path += urls.from(from).to(to, ...args); - - return path; - }; -} - -export function generateDocumentHTML(pageInfo, { - defaultLanguage, - getThemeString, - language, - languages, - localizedPaths, - paths, - oEmbedJSONHref, - to, - transformMultiline, - wikiData, -}) { - const {wikiInfo} = wikiData; - - let { - title = '', - meta = {}, - theme = '', - stylesheet = '', - - showWikiNameInTitle = true, - themeColor = '', - - // missing properties are auto-filled, see below! - body = {}, - banner = {}, - main = {}, - sidebarLeft = {}, - sidebarRight = {}, - nav = {}, - secondaryNav = {}, - footer = {}, - socialEmbed = {}, - } = pageInfo; - - body.style ??= ''; - - theme = theme || getThemeString(wikiInfo.color); - - banner ||= {}; - banner.classes ??= []; - banner.src ??= ''; - banner.position ??= ''; - banner.dimensions ??= [0, 0]; - - main.classes ??= []; - main.content ??= ''; - - sidebarLeft ??= {}; - sidebarRight ??= {}; - - for (const sidebar of [sidebarLeft, sidebarRight]) { - sidebar.classes ??= []; - sidebar.content ??= ''; - sidebar.collapse ??= true; - } - - nav.classes ??= []; - nav.content ??= ''; - nav.bottomRowContent ??= ''; - nav.links ??= []; - nav.linkContainerClasses ??= []; - - secondaryNav ??= {}; - secondaryNav.content ??= ''; - secondaryNav.content ??= ''; - - footer.classes ??= []; - footer.content ??= wikiInfo.footerContent - ? transformMultiline(wikiInfo.footerContent) - : ''; - - const colors = themeColor - ? getColors(themeColor, {chroma}) - : null; - - const canonical = wikiInfo.canonicalBase - ? wikiInfo.canonicalBase + (paths.pathname === '/' ? '' : paths.pathname) - : ''; - - const localizedCanonical = wikiInfo.canonicalBase - ? Object.entries(localizedPaths).map(([code, {pathname}]) => ({ - lang: code, - href: wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname), - })) - : []; - - const collapseSidebars = - sidebarLeft.collapse !== false && sidebarRight.collapse !== false; - - const mainHTML = - main.content && - html.tag('main', - { - id: 'content', - class: main.classes, - }, - main.content); - - const footerHTML = - html.tag('footer', - { - [html.onlyIfContent]: true, - id: 'footer', - class: footer.classes, - }, - [ - html.tag('div', - { - [html.onlyIfContent]: true, - class: 'footer-content', - }, - footer.content), - - getFooterLocalizationLinks(paths.pathname, { - defaultLanguage, - html, - language, - languages, - paths, - to, - }), - ]); - - const generateSidebarHTML = (id, { - content, - multiple, - classes, - collapse = true, - wide = false, - - // 'last' - last or only sidebar box is sticky - // 'column' - entire column, incl. multiple boxes from top, is sticky - // 'none' - sidebar not sticky at all, stays at top of page - stickyMode = 'last', - }) => - content - ? html.tag('div', - { - id, - class: [ - 'sidebar-column', - 'sidebar', - wide && 'wide', - !collapse && 'no-hide', - stickyMode !== 'none' && 'sticky-' + stickyMode, - ...classes, - ], - }, - content) - : multiple - ? html.tag('div', - { - id, - class: [ - 'sidebar-column', - 'sidebar-multiple', - wide && 'wide', - !collapse && 'no-hide', - stickyMode !== 'none' && 'sticky-' + stickyMode, - ], - }, - multiple - .map((infoOrContent) => - (typeof infoOrContent === 'object' && !Array.isArray(infoOrContent)) - ? infoOrContent - : {content: infoOrContent}) - .filter(({content}) => content) - .map(({ - content, - classes: classes2 = [], - }) => - html.tag('div', - { - class: ['sidebar', ...classes, ...classes2], - }, - html.fragment(content)))) - : ''; - - const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebarLeft); - const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight); - - if (nav.simple) { - nav.linkContainerClasses = ['nav-links-hierarchy']; - nav.links = [{toHome: true}, {toCurrentPage: true}]; - } - - const links = (nav.links || []).filter(Boolean); - - const navLinkParts = []; - for (let i = 0; i < links.length; i++) { - let cur = links[i]; - - let {title: linkTitle} = cur; - - if (cur.toHome) { - linkTitle ??= wikiInfo.nameShort; - } else if (cur.toCurrentPage) { - linkTitle ??= title; - } - - let partContent; - - if (typeof cur.html === 'string') { - partContent = 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 - ? (() => { - 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 - )})` - ); - } - partContent = html.tag('a', attributes, linkTitle); - } - - if (!partContent) continue; - - const part = html.tag('span', - {class: cur.divider === false && 'no-divider'}, - partContent); - - navLinkParts.push(part); - } - - const navHTML = html.tag('nav', - { - [html.onlyIfContent]: true, - id: 'header', - class: [ - ...nav.classes, - links.length && 'nav-has-main-links', - nav.content && 'nav-has-content', - nav.bottomRowContent && 'nav-has-bottom-row', - ], - }, - [ - links.length && - html.tag( - 'div', - {class: ['nav-main-links', ...nav.linkContainerClasses]}, - navLinkParts - ), - nav.bottomRowContent && - html.tag('div', {class: 'nav-bottom-row'}, nav.bottomRowContent), - nav.content && html.tag('div', {class: 'nav-content'}, nav.content), - ]); - - const secondaryNavHTML = html.tag('nav', - { - [html.onlyIfContent]: true, - id: 'secondary-nav', - class: secondaryNav.classes, - }, - secondaryNav.content); - - const bannerSrc = banner.src - ? banner.src - : banner.path - ? to(...banner.path) - : null; - - const bannerHTML = - banner.position && - bannerSrc && - html.tag('div', - { - id: 'banner', - class: banner.classes, - }, - html.tag('img', { - src: bannerSrc, - alt: banner.alt, - width: banner.dimensions[0] || 1100, - height: banner.dimensions[1] || 200, - })); - - const layoutHTML = [ - navHTML, - banner.position === 'top' && bannerHTML, - secondaryNavHTML, - html.tag('div', - { - class: [ - 'layout-columns', - !collapseSidebars && 'vertical-when-thin', - (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar', - (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars', - !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars', - sidebarLeftHTML && 'has-sidebar-left', - sidebarRightHTML && 'has-sidebar-right', - ], - }, - [ - sidebarLeftHTML, - mainHTML, - sidebarRightHTML, - ]), - banner.position === 'bottom' && bannerHTML, - footerHTML, - ].filter(Boolean).join('\n'); - - const infoCardHTML = html.tag('div', {id: 'info-card-container'}, - html.tag('div', {id: 'info-card-decor'}, - html.tag('div', {id: 'info-card'}, [ - html.tag('div', {class: ['info-card-art-container', 'no-reveal']}, - img({ - class: 'info-card-art', - src: '', - link: true, - square: true, - })), - html.tag('div', {class: ['info-card-art-container', 'reveal']}, - img({ - class: 'info-card-art', - src: '', - link: true, - square: true, - reveal: getRevealStringFromWarnings( - html.tag('span', {class: 'info-card-art-warnings'}), - {html, language}), - })), - html.tag('h1', {class: 'info-card-name'}, - html.tag('a')), - html.tag('p', {class: 'info-card-album'}, - language.$('releaseInfo.from', { - album: html.tag('a'), - })), - html.tag('p', {class: 'info-card-artists'}, - language.$('releaseInfo.by', { - artists: html.tag('span'), - })), - html.tag('p', {class: 'info-card-cover-artists'}, - language.$('releaseInfo.coverArtBy', { - artists: html.tag('span'), - })), - ]))); - - const socialEmbedHTML = [ - socialEmbed.title && - html.tag('meta', {property: 'og:title', content: socialEmbed.title}), - - socialEmbed.description && - html.tag('meta', { - property: 'og:description', - content: socialEmbed.description, - }), - - socialEmbed.image && - html.tag('meta', {property: 'og:image', content: socialEmbed.image}), - - ...html.fragment( - colors && [ - html.tag('meta', { - name: 'theme-color', - content: colors.dark, - media: '(prefers-color-scheme: dark)', - }), - - html.tag('meta', { - name: 'theme-color', - content: colors.light, - media: '(prefers-color-scheme: light)', - }), - - html.tag('meta', { - name: 'theme-color', - content: colors.primary, - }), - ]), - - oEmbedJSONHref && - html.tag('link', { - type: 'application/json+oembed', - href: oEmbedJSONHref, - }), - ].filter(Boolean).join('\n'); - - return `<!DOCTYPE html>\n` + html.tag('html', - { - lang: language.intlCode, - 'data-language-code': language.code, - 'data-url-key': paths.urlPath[0], - ...Object.fromEntries( - paths.urlPath.slice(1).map((v, i) => [['data-url-value' + i], v]) - ), - 'data-rebase-localized': to('localized.root'), - 'data-rebase-shared': to('shared.root'), - 'data-rebase-media': to('media.root'), - 'data-rebase-data': to('data.root'), - }, - [ - `<!--\n` + [ - wikiInfo.canonicalBase - ? `hsmusic.wiki - ${wikiInfo.name}, ${wikiInfo.canonicalBase}` - : `hsmusic.wiki - ${wikiInfo.name}`, - 'Code copyright 2019-2022 Quasar Nebula et al (MIT License)', - ...wikiInfo.canonicalBase === 'https://hsmusic.wiki/' ? [ - 'Data avidly compiled and localization brought to you', - 'by our awesome team and community of wiki contributors', - '***', - 'Want to contribute? Join our Discord or leave feedback!', - '- https://hsmusic.wiki/discord/', - '- https://hsmusic.wiki/feedback/', - '- https://github.com/hsmusic/', - ] : [ - 'https://github.com/hsmusic/', - ], - '***', - `Site built: ${BUILD_TIME.toLocaleString('en-US', { - dateStyle: 'long', - timeStyle: 'long', - })}`, - `Latest code commit: ${COMMIT}`, - ] - .filter(Boolean) - .map(line => ` ` + line) - .join('\n') + `\n-->`, - - html.tag('head', [ - html.tag('title', - showWikiNameInTitle - ? language.formatString('misc.pageTitle.withWikiName', { - title, - wikiName: wikiInfo.nameShort, - }) - : language.formatString('misc.pageTitle', {title})), - - html.tag('meta', {charset: 'utf-8'}), - html.tag('meta', { - name: 'viewport', - content: 'width=device-width, initial-scale=1', - }), - - ...( - Object.entries(meta) - .filter(([key, value]) => value) - .map(([key, value]) => html.tag('meta', {[key]: value}))), - - canonical && - html.tag('link', { - rel: 'canonical', - href: canonical, - }), - - ...( - localizedCanonical - .map(({lang, href}) => html.tag('link', { - rel: 'alternate', - hreflang: lang, - href, - }))), - - socialEmbedHTML, - - html.tag('link', { - rel: 'stylesheet', - href: to('shared.staticFile', `site2.css?${CACHEBUST}`), - }), - - html.tag('style', - {[html.onlyIfContent]: true}, - [ - theme, - stylesheet, - ]), - - html.tag('script', { - src: to('shared.staticFile', `lazy-loading.js?${CACHEBUST}`), - }), - ]), - - html.tag('body', - {style: body.style || ''}, - [ - html.tag('div', {id: 'page-container'}, [ - mainHTML && - html.tag('div', {id: 'skippers'}, - [ - ['#content', language.$('misc.skippers.skipToContent')], - sidebarLeftHTML && - [ - '#sidebar-left', - sidebarRightHTML - ? language.$('misc.skippers.skipToSidebar.left') - : language.$('misc.skippers.skipToSidebar'), - ], - sidebarRightHTML && - [ - '#sidebar-right', - sidebarLeftHTML - ? language.$('misc.skippers.skipToSidebar.right') - : language.$('misc.skippers.skipToSidebar'), - ], - footerHTML && - ['#footer', language.$('misc.skippers.skipToFooter')], - ] - .filter(Boolean) - .map(([href, title]) => - html.tag('span', {class: 'skipper'}, - html.tag('a', {href}, title)))), - layoutHTML, - ]), - - infoCardHTML, - - html.tag('script', { - type: 'module', - src: to('shared.staticFile', `client.js?${CACHEBUST}`), - }), - ]), - ]); -} - -function generateOEmbedJSON(pageInfo, {language, wikiData}) { - const {socialEmbed} = pageInfo; - const {wikiInfo} = wikiData; - const {canonicalBase, nameShort} = wikiInfo; - - if (!socialEmbed) return ''; - - const entries = [ - socialEmbed.heading && [ - 'author_name', - language.$('misc.socialEmbed.heading', { - wikiName: nameShort, - heading: socialEmbed.heading, - }), - ], - socialEmbed.headingLink && - canonicalBase && [ - 'author_url', - canonicalBase.replace(/\/$/, '') + - '/' + - socialEmbed.headingLink.replace(/^\//, ''), - ], - ].filter(Boolean); - - if (!entries.length) return ''; - - return JSON.stringify(Object.fromEntries(entries)); -} - -async function writePage({ - html, - oEmbedJSON = '', - paths, -}) { - await mkdir(paths.output.directory, {recursive: true}); - - await Promise.all( - [ - writeFile(paths.output.documentHTML, html), - - oEmbedJSON && - writeFile(paths.output.oEmbedJSON, oEmbedJSON), - ].filter(Boolean) - ); -} - -function getPagePaths({ - baseDirectory, - fullKey, - urlArgs, - - file = 'index.html', -}) { - const [groupKey, subKey] = fullKey.split('.'); - - const pathname = - groupKey === 'localized' && baseDirectory - ? urls - .from('shared.root') - .toDevice( - 'localizedWithBaseDirectory.' + subKey, - baseDirectory, - ...urlArgs) - : urls - .from('shared.root') - .toDevice(fullKey, ...urlArgs); - - // Needed for the rare path arguments which themselves contains one or more - // slashes, e.g. for listings, with arguments like 'albums/by-name'. - const subdirectoryPrefix = - '../'.repeat(urlArgs.join('/').split('/').length - 1); - - const outputDirectory = path.join(outputPath, pathname); - - const output = { - directory: outputDirectory, - documentHTML: path.join(outputDirectory, file), - oEmbedJSON: path.join(outputDirectory, OEMBED_JSON_FILE) - }; +async function main() { + Error.stackTraceLimit = Infinity; - return { - urlPath: [fullKey, ...urlArgs], + const selectedBuildModeFlags = Object.keys( + await parseOptions(process.argv.slice(2), { + [parseOptions.handleUnknown]: () => {}, - output, - pathname, - subdirectoryPrefix, - }; -} + ...Object.fromEntries(Object.keys(buildModes) + .map((key) => [key, {type: 'flag'}])), + })); -async function writeFavicon() { - try { - await stat(path.join(mediaPath, FAVICON_FILE)); - } catch (error) { - return; - } + let selectedBuildModeFlag; - try { - await copyFile( - path.join(mediaPath, FAVICON_FILE), - path.join(outputPath, FAVICON_FILE) - ); - } catch (error) { - logWarn`Failed to copy favicon! ${error.message}`; + if (empty(selectedBuildModeFlags)) { + selectedBuildModeFlag = 'static-build'; + logInfo`No build mode specified, using default: ${selectedBuildModeFlag}`; + } else if (selectedBuildModeFlags.length > 1) { + logError`Building multiple modes (${selectedBuildModeFlags.join(', ')}) at once not supported.`; + logError`Please specify a maximum of one build mode.`; return; + } else { + selectedBuildModeFlag = selectedBuildModeFlags[0]; + logInfo`Using specified build mode: ${selectedBuildModeFlag}`; } - logInfo`Copied favicon to site root.`; -} + const selectedBuildMode = buildModes[selectedBuildModeFlag]; -function writeSymlinks() { - return progressPromiseAll('Writing site symlinks.', [ - link(path.join(__dirname, UTILITY_DIRECTORY), 'shared.utilityRoot'), - link(path.join(__dirname, STATIC_DIRECTORY), 'shared.staticRoot'), - link(mediaPath, 'media.root'), - ]); - - async function link(directory, urlKey) { - const pathname = urls.from('shared.root').toDevice(urlKey); - const file = path.join(outputPath, pathname); - try { - await unlink(file); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - try { - await symlink(path.resolve(directory), file); - } catch (error) { - if (error.code === 'EPERM') { - await symlink(path.resolve(directory), file, 'junction'); - } - } - } -} - -function writeSharedFilesAndPages({language, wikiData}) { - const {groupData, wikiInfo} = wikiData; - - const redirect = async (title, from, urlKey, directory) => { - const target = path.relative( - from, - urls.from('shared.root').to(urlKey, directory) - ); - const content = generateRedirectHTML(title, target, {language}); - await mkdir(path.join(outputPath, from), {recursive: true}); - await writeFile(path.join(outputPath, from, 'index.html'), content); + // This is about to get a whole lot more stuff put in it. + const wikiData = { + listingSpec, + listingTargetSpec, }; - return progressPromiseAll(`Writing files & pages shared across languages.`, [ - groupData?.some((group) => group.directory === 'fandom') && - redirect( - 'Fandom - Gallery', - 'albums/fandom', - 'localized.groupGallery', - 'fandom' - ), - - groupData?.some((group) => group.directory === 'official') && - redirect( - 'Official - Gallery', - 'albums/official', - 'localized.groupGallery', - 'official' - ), - - wikiInfo.enableListings && - redirect( - 'Album Commentary', - 'list/all-commentary', - 'localized.commentaryIndex', - '' - ), - - writeFile( - path.join(outputPath, 'data.json'), - ( - '{\n' + - [ - `"albumData": ${stringifyThings(wikiData.albumData)},`, - wikiInfo.enableFlashesAndGames && - `"flashData": ${stringifyThings(wikiData.flashData)},`, - `"artistData": ${stringifyThings(wikiData.artistData)}`, - ] - .filter(Boolean) - .map(line => ' ' + line) - .join('\n') + - '\n}')), - ].filter(Boolean)); -} - -function generateRedirectHTML(title, target, {language}) { - return `<!DOCTYPE html>\n` + html.tag('html', [ - html.tag('head', [ - html.tag('title', language.$('redirectPage.title', {title})), - html.tag('meta', {charset: 'utf-8'}), - - html.tag('meta', { - 'http-equiv': 'refresh', - content: `0;url=${target}`, - }), - - // TODO: Is this OK for localized pages? - html.tag('link', { - rel: 'canonical', - href: target, - }), - ]), - - html.tag('body', - html.tag('main', [ - html.tag('h1', - language.$('redirectPage.title', {title})), - html.tag('p', - language.$('redirectPage.infoLine', { - target: html.tag('a', {href: target}, target), - })), - ])), - ]); -} - -// Wrapper function for running a function once for all languages. -async function wrapLanguages(fn, {languages, writeOneLanguage = null}) { - const k = writeOneLanguage; - const languagesToRun = k ? {[k]: languages[k]} : languages; - - const entries = Object.entries(languagesToRun).filter( - ([key]) => key !== 'default' - ); - - for (let i = 0; i < entries.length; i++) { - const [_key, language] = entries[i]; - - await fn(language, i, entries); - } -} - -async function main() { - Error.stackTraceLimit = Infinity; - - const WD = wikiData; - - WD.listingSpec = listingSpec; - WD.listingTargetSpec = listingTargetSpec; + const cliOptions = await parseOptions(process.argv.slice(2), { + ...selectedBuildMode.getCLIOptions(), - const miscOptions = await parseOptions(process.argv.slice(2), { // Data files for the site, including flash, artist, and al8um data, // and like a jillion other things too. Pretty much everything which // makes an individual wiki what it is goes here! @@ -1679,16 +171,6 @@ async function main() { type: 'value', }, - // This is the output directory. It's the one you'll upload online with - // rsync or whatever when you're pushing an upd8, and also the one - // you'd archive if you wanted to make a 8ackup of the whole dang - // site. Just keep in mind that the gener8ted result will contain a - // couple symlinked directories, so if you're uploading, you're pro8a8ly - // gonna want to resolve those yourself. - 'out-path': { - type: 'value', - }, - // Thum8nail gener8tion is *usually* something you want, 8ut it can 8e // kinda a pain to run every time, since it does necessit8te reading // every media file at run time. Pass this to skip it. @@ -1709,20 +191,6 @@ async function main() { type: 'flag', }, - // Only want to 8uild one language during testing? This can chop down - // 8uild times a pretty 8ig chunk! Just pass a single language code. - lang: { - type: 'value', - }, - - // Working without a dev server and just using file:// URLs in your we8 - // 8rowser? This will automatically append index.html to links across - // the site. Not recommended for production, since it isn't guaranteed - // 100% error-free (and index.html-style links are less pretty anyway). - 'append-index-html': { - type: 'flag', - }, - // Want sweet, sweet trace8ack info in aggreg8te error messages? This // will print all the juicy details (or at least the first relevant // line) right to your output, 8ut also pro8a8ly give you a headache @@ -1764,12 +232,23 @@ async function main() { [parseOptions.handleUnknown]: () => {}, }); - dataPath = miscOptions['data-path'] || process.env.HSMUSIC_DATA; - mediaPath = miscOptions['media-path'] || process.env.HSMUSIC_MEDIA; - langPath = miscOptions['lang-path'] || process.env.HSMUSIC_LANG; // Can 8e left unset! - outputPath = miscOptions['out-path'] || process.env.HSMUSIC_OUT; + const dataPath = cliOptions['data-path'] || process.env.HSMUSIC_DATA; + const mediaPath = cliOptions['media-path'] || process.env.HSMUSIC_MEDIA; + const langPath = cliOptions['lang-path'] || process.env.HSMUSIC_LANG; // Can 8e left unset! + + const skipThumbs = cliOptions['skip-thumbs'] ?? false; + const thumbsOnly = cliOptions['thumbs-only'] ?? false; + const noBuild = cliOptions['no-build'] ?? false; - const writeOneLanguage = miscOptions['lang']; + const showAggregateTraces = cliOptions['show-traces'] ?? false; + + const precacheData = cliOptions['precache-data'] ?? false; + const showInvalidPropertyAccesses = cliOptions['show-invalid-property-accesses'] ?? false; + + // Makes writing nicer on the CPU and file I/O parts of the OS, with a + // marginal performance deficit while waiting for file writes to finish + // before proceeding to more page processing. + const queueSize = +(cliOptions['queue-size'] ?? 500); { let errored = false; @@ -1781,46 +260,11 @@ async function main() { }; error(!dataPath, `Expected --data-path option or HSMUSIC_DATA to be set`); error(!mediaPath, `Expected --media-path option or HSMUSIC_MEDIA to be set`); - error(!outputPath, `Expected --out-path option or HSMUSIC_OUT to be set`); if (errored) { return; } } - const appendIndexHTML = miscOptions['append-index-html'] ?? false; - if (appendIndexHTML) { - logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`; - unbound_link.globalOptions.appendIndexHTML = true; - } - - const skipThumbs = miscOptions['skip-thumbs'] ?? false; - const thumbsOnly = miscOptions['thumbs-only'] ?? false; - const noBuild = miscOptions['no-build'] ?? false; - const showAggregateTraces = miscOptions['show-traces'] ?? false; - const precacheData = miscOptions['precache-data'] ?? false; - - // NOT for ena8ling or disa8ling specific features of the site! - // This is only in charge of what general groups of files to 8uild. - // They're here to make development quicker when you're only working - // on some particular area(s) of the site rather than making changes - // across all of them. - const writeFlags = await parseOptions(process.argv.slice(2), { - all: {type: 'flag'}, // Defaults to true if none 8elow specified. - - // Kinda a hack t8h! - ...Object.fromEntries( - Object.keys(pageSpecs).map((key) => [key, {type: 'flag'}]) - ), - - [parseOptions.handleUnknown]: () => {}, - }); - - const writeAll = !Object.keys(writeFlags).length || writeFlags.all; - - logInfo`Writing site pages: ${ - writeAll ? 'all' : Object.keys(writeFlags).join(', ') - }`; - const niceShowAggregate = (error, ...opts) => { showAggregate(error, { showTraces: showAggregateTraces, @@ -1844,9 +288,6 @@ async function main() { if (thumbsOnly) return; } - const showInvalidPropertyAccesses = - miscOptions['show-invalid-property-accesses'] ?? false; - if (showInvalidPropertyAccesses) { CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true; } @@ -1905,7 +346,7 @@ async function main() { } } - if (!WD.wikiInfo) { + if (!wikiData.wikiInfo) { logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`; return; } @@ -1978,9 +419,7 @@ async function main() { progressCallAll('Caching all data values', Object.entries(wikiData) .filter(([key]) => key !== 'listingSpec' && - key !== 'listingTargetSpec' && - key !== 'officialAlbumData' && - key !== 'fandomAlbumData') + key !== 'listingTargetSpec') .map(([key, value]) => key === 'wikiInfo' ? [key, [value]] : key === 'homepageLayout' ? [key, [value]] : @@ -1990,8 +429,7 @@ async function main() { } const internalDefaultLanguage = await processLanguageFile( - path.join(__dirname, DEFAULT_STRINGS_FILE) - ); + path.join(__dirname, DEFAULT_STRINGS_FILE)); let languages; if (langPath) { @@ -1999,28 +437,25 @@ async function main() { filter: (f) => path.extname(f) === '.json', }); - const results = await progressPromiseAll( - `Reading & processing language files.`, - languageDataFiles.map((file) => processLanguageFile(file)) - ); + const results = await progressPromiseAll(`Reading & processing language files.`, + languageDataFiles.map((file) => processLanguageFile(file))); languages = Object.fromEntries( - results.map((language) => [language.code, language]) - ); + results.map((language) => [language.code, language])); } else { languages = {}; } const customDefaultLanguage = - languages[WD.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code]; + languages[wikiData.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code]; let finalDefaultLanguage; if (customDefaultLanguage) { logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; finalDefaultLanguage = customDefaultLanguage; - } else if (WD.wikiInfo.defaultLanguage) { - logError`Wiki info file specified default language is ${WD.wikiInfo.defaultLanguage}, but no such language file exists!`; + } else if (wikiData.wikiInfo.defaultLanguage) { + logError`Wiki info file specified default language is ${wikiData.wikiInfo.defaultLanguage}, but no such language file exists!`; if (langPath) { logError`Check if an appropriate file exists in ${langPath}?`; } else { @@ -2044,22 +479,15 @@ async function main() { if (noBuild) { logInfo`Not generating any site or page files this run (--no-build passed).`; - } else if (writeOneLanguage && !(writeOneLanguage in languages)) { - logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`; - return; - } else if (writeOneLanguage) { - logInfo`Writing only language ${writeOneLanguage} this run.`; - } else { - logInfo`Writing all languages.`; } { const tagRefs = new Set( - [...WD.trackData, ...WD.albumData] + [...wikiData.trackData, ...wikiData.albumData] .flatMap((thing) => thing.artTagsByRef ?? [])); for (const ref of tagRefs) { - if (find.artTag(ref, WD.artTagData)) { + if (find.artTag(ref, wikiData.artTagData)) { tagRefs.delete(ref); } } @@ -2072,10 +500,7 @@ async function main() { } } - WD.officialAlbumData = WD.albumData - .filter((album) => album.groups.some((group) => group.directory === OFFICIAL_GROUP_DIRECTORY)); - WD.fandomAlbumData = WD.albumData - .filter((album) => album.groups.every((group) => group.directory !== OFFICIAL_GROUP_DIRECTORY)); + const urls = generateURLs(urlSpec); const fileSizePreloader = new FileSizePreloader(); @@ -2087,7 +512,7 @@ async function main() { // function between them so that when site code requests a site path, // it'll get the size of the file at the corresponding device path. const additionalFilePaths = [ - ...WD.albumData.flatMap((album) => + ...wikiData.albumData.flatMap((album) => [ ...(album.additionalFiles ?? []), ...album.tracks.flatMap((track) => track.additionalFiles ?? []), @@ -2123,502 +548,55 @@ async function main() { if (noBuild) return; - // Makes writing nicer on the CPU and file I/O parts of the OS, with a - // marginal performance deficit while waiting for file writes to finish - // before proceeding to more page processing. - queueSize = +(miscOptions['queue-size'] ?? 500); - - const buildDictionary = pageSpecs; - - await writeFavicon(); - await writeSymlinks(); - await writeSharedFilesAndPages({language: finalDefaultLanguage, wikiData}); - - const buildSteps = writeAll - ? Object.entries(buildDictionary) - : Object.entries(buildDictionary).filter(([flag]) => writeFlags[flag]); - - let writes; - { - let error = false; - - const buildStepsWithTargets = buildSteps - .map(([flag, pageSpec]) => { - // Condition not met: skip this build step altogether. - if (pageSpec.condition && !pageSpec.condition({wikiData})) { - return null; - } - - // May still call writeTargetless if present. - if (!pageSpec.targets) { - return {flag, pageSpec, targets: []}; - } - - if (!pageSpec.write) { - logError`${flag + '.targets'} is specified, but ${flag + '.write'} is missing!`; - error = true; - return null; - } - - const targets = pageSpec.targets({wikiData}); - if (!Array.isArray(targets)) { - logError`${flag + '.targets'} was called, but it didn't return an array! (${typeof targets})`; - error = true; - return null; - } - - return {flag, pageSpec, targets}; - }) - .filter(Boolean); - - if (error) { - return; - } - - const validateWrites = (writes, fnName) => { - // Do a quick valid8tion! If one of the writeThingPages functions go - // wrong, this will stall out early and tell us which did. - - if (!Array.isArray(writes)) { - logError`${fnName} didn't return an array!`; - error = true; - return false; - } - - if ( - !( - writes.every((obj) => typeof obj === 'object') && - writes.every((obj) => { - const result = validateWriteObject(obj); - if (result.error) { - logError`Validating write object failed: ${result.error}`; - return false; - } else { - return true; - } - }) - ) - ) { - logError`${fnName} returned invalid entries!`; - error = true; - return false; - } - - return true; - }; - - // return; - - writes = progressCallAll('Computing page & data writes.', buildStepsWithTargets.flatMap(({flag, pageSpec, targets}) => { - const writesFns = targets.map(target => () => { - const writes = pageSpec.write(target, {wikiData})?.slice() || []; - return validateWrites(writes, flag + '.write') ? writes : []; - }); - - if (pageSpec.writeTargetless) { - writesFns.push(() => { - const writes = pageSpec.writeTargetless({wikiData}); - return validateWrites(writes, flag + '.writeTargetless') ? writes : []; - }); - } - - return writesFns; - })).flat(); - - if (error) { - return; - } - } - - const pageWrites = writes.filter(({type}) => type === 'page'); - const dataWrites = writes.filter(({type}) => type === 'data'); - const redirectWrites = writes.filter(({type}) => type === 'redirect'); - - if (writes.length) { - logInfo`Total of ${writes.length} writes returned. (${pageWrites.length} page, ${dataWrites.length} data [currently skipped], ${redirectWrites.length} redirect)`; - } else { - logWarn`No writes returned at all, so exiting early. This is probably a bug!`; - return; - } + const developersComment = generateDevelopersCommentHTML({ + buildTime: BUILD_TIME, + commit: COMMIT, + wikiData, + }); - /* - await progressPromiseAll(`Writing data files shared across languages.`, queue( - dataWrites.map(({path, data}) => () => { - const bound = {}; - - bound.serializeLink = bindOpts(serializeLink, {}); - - bound.serializeContribs = bindOpts(serializeContribs, {}); - - bound.serializeImagePaths = bindOpts(serializeImagePaths, { - thumb - }); - - bound.serializeCover = bindOpts(serializeCover, { - [bindOpts.bindIndex]: 2, - serializeImagePaths: bound.serializeImagePaths, - urls - }); - - bound.serializeGroupsForAlbum = bindOpts(serializeGroupsForAlbum, { - serializeLink - }); - - bound.serializeGroupsForTrack = bindOpts(serializeGroupsForTrack, { - serializeLink - }); - - // TODO: This only supports one <>-style argument. - return writeData(path[0], path[1], data({...bound})); - }), - queueSize - )); - */ - - const perLanguageFn = async (language, i, entries) => { - const baseDirectory = - language === finalDefaultLanguage ? '' : language.code; - - console.log(`\x1b[34;1m${`[${i + 1}/${entries.length}] ${language.code} (-> /${baseDirectory}) `.padEnd(60, '-')}\x1b[0m`); - - await progressPromiseAll(`Writing ${language.code}`, queue([ - ...pageWrites.map((props) => () => { - const {path, page} = props; - - const pageSubKey = path[0]; - const urlArgs = path.slice(1); - - const localizedPaths = Object.fromEntries( - Object.entries(languages) - .filter(([key, language]) => - key !== 'default' && - !language.hidden) - .map(([_key, language]) => [ - language.code, - getPagePaths({ - baseDirectory: - (language === finalDefaultLanguage - ? '' - : language.code), - fullKey: 'localized.' + pageSubKey, - urlArgs, - }), - ])); - - const paths = getPagePaths({ - baseDirectory, - fullKey: 'localized.' + pageSubKey, - urlArgs, - }); - - const to = getURLsFrom({ - baseDirectory, - pageSubKey, - paths, - }); - - const absoluteTo = (targetFullKey, ...args) => { - const [groupKey, subKey] = targetFullKey.split('.'); - const from = urls.from('shared.root'); - return ( - '/' + - (groupKey === 'localized' && baseDirectory - ? from.to( - 'localizedWithBaseDirectory.' + subKey, - baseDirectory, - ...args - ) - : from.to(targetFullKey, ...args)) - ); - }; - - // TODO: Is there some nicer way to define these, - // may8e without totally re-8inding everything for - // each page? - const bound = {}; - - bound.html = html; - - bound.getColors = bindOpts(getColors, { - chroma, - }); - - bound.getLinkThemeString = bindOpts(unbound_getLinkThemeString, { - getColors: bound.getColors, - }); - - bound.getThemeString = bindOpts(unbound_getThemeString, { - getColors: bound.getColors, - }); - - bound.link = withEntries(unbound_link, (entries) => - entries - .map(([key, fn]) => [key, bindOpts(fn, { - getLinkThemeString: bound.getLinkThemeString, - to, - })])); - - bound.parseAttributes = bindOpts(parseAttributes, { - to, - }); - - bound.find = bindFind(wikiData, {mode: 'warn'}); - - bound.transformInline = bindOpts(transformInline, { - find: bound.find, - link: bound.link, - replacerSpec, - language, - 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, { - html, - language, - to, - }); - - bound.fancifyURL = bindOpts(fancifyURL, { - html, - language, - }); - - bound.fancifyFlashURL = bindOpts(fancifyFlashURL, { - [bindOpts.bindIndex]: 2, - html, - language, - - fancifyURL: bound.fancifyURL, - }); - - bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, { - html, - language, - }); - - bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, { - language, - - getRevealStringFromWarnings: bound.getRevealStringFromWarnings, - }); - - bound.getArtistString = bindOpts(getArtistString, { - html, - link: bound.link, - language, - - iconifyURL: bound.iconifyURL, - }); - - bound.getAlbumCover = bindOpts(getAlbumCover, { - to, - }); - - bound.getTrackCover = bindOpts(getTrackCover, { - to, - }); - - bound.getFlashCover = bindOpts(getFlashCover, { - to, - }); - - bound.getArtistAvatar = bindOpts(getArtistAvatar, { - to, - }); - - bound.generateAdditionalFilesShortcut = bindOpts(generateAdditionalFilesShortcut, { - html, - language, - }); - - bound.generateAdditionalFilesList = bindOpts(generateAdditionalFilesList, { - html, - language, - }); - - bound.generateNavigationLinks = bindOpts(generateNavigationLinks, { - link: bound.link, - language, - }); - - bound.generateStickyHeadingContainer = bindOpts(generateStickyHeadingContainer, { - [bindOpts.bindIndex]: 0, - getRevealStringFromTags: bound.getRevealStringFromTags, - html, - img, - }); - - bound.generateChronologyLinks = bindOpts(generateChronologyLinks, { - html, - language, - link: bound.link, - wikiData, - - generateNavigationLinks: bound.generateNavigationLinks, - }); - - bound.generateCoverLink = bindOpts(generateCoverLink, { - [bindOpts.bindIndex]: 0, - html, - img, - link: bound.link, - language, - to, - wikiData, - - getRevealStringFromTags: bound.getRevealStringFromTags, - }); - - bound.generateInfoGalleryLinks = bindOpts(generateInfoGalleryLinks, { - [bindOpts.bindIndex]: 2, - link: bound.link, - language, - }); - - bound.generateTrackListDividedByGroups = bindOpts(generateTrackListDividedByGroups, { - html, - language, - wikiData, - }); - - bound.getGridHTML = bindOpts(getGridHTML, { - [bindOpts.bindIndex]: 0, - img, - html, - language, - - getRevealStringFromTags: bound.getRevealStringFromTags, - }); - - bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, { - [bindOpts.bindIndex]: 0, - link: bound.link, - language, - - getAlbumCover: bound.getAlbumCover, - getGridHTML: bound.getGridHTML, - }); - - bound.getFlashGridHTML = bindOpts(getFlashGridHTML, { - [bindOpts.bindIndex]: 0, - link: bound.link, - - getFlashCover: bound.getFlashCover, - getGridHTML: bound.getGridHTML, - }); - - bound.getCarouselHTML = bindOpts(getCarouselHTML, { - [bindOpts.bindIndex]: 0, - img, - html, - }) - - bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, { - to, - }); - - const pageInfo = page({ - ...bound, - - language, - - absoluteTo, - relativeTo: to, - to, - urls, - - getSizeOfAdditionalFile, - }); - - const oEmbedJSON = generateOEmbedJSON(pageInfo, { - language, - wikiData, - }); - - const oEmbedJSONHref = - oEmbedJSON && - wikiData.wikiInfo.canonicalBase && - wikiData.wikiInfo.canonicalBase + - urls - .from('shared.root') - .to('shared.path', paths.pathname + OEMBED_JSON_FILE); - - const pageHTML = generateDocumentHTML(pageInfo, { - defaultLanguage: finalDefaultLanguage, - getThemeString: bound.getThemeString, - language, - languages, - localizedPaths, - oEmbedJSONHref, - paths, - to, - transformMultiline: bound.transformMultiline, - wikiData, - }); - - return writePage({ - html: pageHTML, - oEmbedJSON, - paths, - }); - }), - ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => { - const title = titleFn({ - language, - }); - - const from = getPagePaths({ - baseDirectory, - fullKey: 'localized.' + fromPath[0], - urlArgs: fromPath.slice(1), - }); - - const to = getURLsFrom({ - baseDirectory, - pageSubKey: fromPath[0], - paths: from, - }); - - const target = to('localized.' + toPath[0], ...toPath.slice(1)); - const html = generateRedirectHTML(title, target, {language}); - return writePage({html, paths: from}); - }), - ], queueSize)); - }; + return selectedBuildMode.go({ + cliOptions, + dataPath, + mediaPath, + queueSize, + srcRootPath: __dirname, - await wrapLanguages(perLanguageFn, { + defaultLanguage: finalDefaultLanguage, languages, - writeOneLanguage, - }); + wikiData, + urls, + urlSpec, - // The single most important step. - logInfo`Written!`; + cachebust: '?' + CACHEBUST, + developersComment, + getSizeOfAdditionalFile, + }); } // TODO: isMain detection isn't consistent across platforms here /* eslint-disable-next-line no-constant-condition */ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmusic') { - main() - .catch((error) => { + (async () => { + let result; + + try { + result = await main(); + } catch (error) { if (error instanceof AggregateError) { showAggregate(error); } else { console.error(error); } - }) - .then(() => { - decorateTime.displayTime(); - CacheableObject.showInvalidAccesses(); - }); + } + + if (result !== true) { + process.exit(1); + return; + } + + decorateTime.displayTime(); + CacheableObject.showInvalidAccesses(); + + process.exit(0); + })(); } |