From d59ed9687eb8e38aac3ca7dd5b190ba78b3b5556 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 7 Jan 2023 20:18:51 -0400 Subject: extract page templates (generateDocumentJSON, etc) towards basic dynamcis pt. 1 (#124) --- src/upd8.js | 630 ++------------------------------------------- src/write/page-template.js | 608 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 627 insertions(+), 611 deletions(-) create mode 100644 src/write/page-template.js diff --git a/src/upd8.js b/src/upd8.js index fb1239a0..a8dda34d 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -31,8 +31,7 @@ // 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 chroma from 'chroma-js'; - +import {execSync} from 'child_process'; import * as path from 'path'; import {fileURLToPath} from 'url'; @@ -45,26 +44,14 @@ import { 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 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 {replacerSpec} from './util/transform-content.js'; -import CacheableObject from './data/things/cacheable-object.js'; import {processLanguageFile} from './data/language.js'; import {serializeThings} from './data/serialize.js'; -import {bindUtilities} from './write/bind-utilities.js'; -import {validateWrites} from './write/validate-writes.js'; +import CacheableObject from './data/things/cacheable-object.js'; import { filterDuplicateDirectories, @@ -75,13 +62,14 @@ import { WIKI_INFO_FILE, } from './data/yaml.js'; -import { - getFooterLocalizationLinks, - getRevealStringFromWarnings, - img, -} from './misc-templates.js'; +import * as pageSpecs from './page/index.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 {replacerSpec} from './util/transform-content.js'; import { color, @@ -94,7 +82,14 @@ import { progressPromiseAll, } from './util/cli.js'; -import {validateReplacerSpec} from './util/replacer.js'; +import {bindUtilities} from './write/bind-utilities.js'; +import {validateWrites} from './write/validate-writes.js'; + +import { + generateDocumentHTML, + generateRedirectHTML, + generateOEmbedJSON, +} from './write/page-template.js'; /* import { @@ -223,566 +218,6 @@ export function getURLsFrom({ }; } -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({ - html, - class: 'info-card-art', - src: '', - link: true, - square: true, - })), - html.tag('div', {class: ['info-card-art-container', 'reveal']}, - img({ - html, - 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 `\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'), - }, - [ - ``, - - 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 = '', @@ -945,36 +380,6 @@ function writeSharedFilesAndPages({language, wikiData}) { ].filter(Boolean)); } -function generateRedirectHTML(title, target, {language}) { - return `\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; @@ -1691,6 +1096,9 @@ async function main() { .to('shared.path', paths.pathname + OEMBED_JSON_FILE); const pageHTML = generateDocumentHTML(pageInfo, { + buildTime: BUILD_TIME, + cachebust: '?' + CACHEBUST, + commit: COMMIT, defaultLanguage: finalDefaultLanguage, getThemeString: bound.getThemeString, language, diff --git a/src/write/page-template.js b/src/write/page-template.js new file mode 100644 index 00000000..61579549 --- /dev/null +++ b/src/write/page-template.js @@ -0,0 +1,608 @@ +import chroma from 'chroma-js'; + +import * as html from '../util/html.js'; +import {logWarn} from '../util/cli.js'; +import {getColors} from '../util/colors.js'; + +import { + getFooterLocalizationLinks, + getRevealStringFromWarnings, + img, +} from '../misc-templates.js'; + +export function generateDocumentHTML(pageInfo, { + buildTime = null, + cachebust = '', + commit = '', + 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({ + html, + class: 'info-card-art', + src: '', + link: true, + square: true, + })), + html.tag('div', {class: ['info-card-art-container', 'reveal']}, + img({ + html, + 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 `\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'), + }, + [ + ``, + + 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}`), + }), + ]), + ]); +} + +export 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)); +} + +export function generateRedirectHTML(title, target, { + language, +}) { + return `\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), + })), + ])), + ]); +} -- cgit 1.3.0-6-gf8a5