diff options
Diffstat (limited to 'src/content')
17 files changed, 1250 insertions, 1369 deletions
diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js index eb9fc8b0..56e6686c 100644 --- a/src/content/dependencies/generateAdditionalFilesList.js +++ b/src/content/dependencies/generateAdditionalFilesList.js @@ -1,5 +1,29 @@ import {empty} from '../../util/sugar.js'; +function validateFileMapping(v, validateValue) { + return value => { + v.isObject(value); + + const valueErrors = []; + for (const [fileKey, fileValue] of Object.entries(value)) { + if (fileValue === null) { + continue; + } + + try { + validateValue(fileValue); + } catch (error) { + error.message = `(${fileKey}) ` + error.message; + valueErrors.push(error); + } + } + + if (!empty(valueErrors)) { + throw new AggregateError(valueErrors, `Errors validating values`); + } + }; +} + export default { extraDependencies: [ 'html', @@ -14,108 +38,67 @@ export default { }; }, - generate(data, { + slots: { + fileLinks: { + validate: v => validateFileMapping(v, v.isHTML), + }, + + fileSizes: { + validate: v => validateFileMapping(v, v.isWholeNumber), + }, + }, + + generate(data, slots, { html, language, }) { - const fileKeys = data.additionalFiles.flatMap(({files}) => files); - const validateFileMapping = (v, validateValue) => { - return value => { - v.isObject(value); - - // It's OK to skip values for files, but if keys are provided for files - // which don't exist, that's an error. - - const unexpectedKeys = - Object.keys(value).filter(key => !fileKeys.includes(key)) - - if (!empty(unexpectedKeys)) { - throw new TypeError(`Unexpected file keys: ${unexpectedKeys.join(', ')}`); - } - - const valueErrors = []; - for (const [fileKey, fileValue] of Object.entries(value)) { - if (fileValue === null) { - continue; - } - - try { - validateValue(fileValue); - } catch (error) { - error.message = `(${fileKey}) ` + error.message; - valueErrors.push(error); - } - } - - if (!empty(valueErrors)) { - throw new AggregateError(valueErrors, `Errors validating values`); - } - }; - }; - - return html.template({ - annotation: 'generateAdditionalFilesList', - - slots: { - fileLinks: { - validate: v => validateFileMapping(v, v.isHTML), - }, - - fileSizes: { - validate: v => validateFileMapping(v, v.isWholeNumber), - }, - }, - - content(slots) { - if (!slots.fileSizes) { - return html.blank(); - } - - const filesWithLinks = new Set( - Object.entries(slots.fileLinks) - .filter(([key, value]) => value) - .map(([key]) => key)); - - if (filesWithLinks.size === 0) { - return html.blank(); - } - - const filteredFileGroups = data.additionalFiles - .map(({title, description, files}) => ({ - title, - description, - files: files.filter(f => filesWithLinks.has(f)), - })) - .filter(({files}) => !empty(files)); - - if (empty(filteredFileGroups)) { - return html.blank(); - } - - return html.tag('dl', - filteredFileGroups.flatMap(({title, description, files}) => [ - html.tag('dt', - (description - ? language.$('releaseInfo.additionalFiles.entry.withDescription', { - title, - description, - }) - : language.$('releaseInfo.additionalFiles.entry', {title}))), - - html.tag('dd', - html.tag('ul', - files.map(file => - html.tag('li', - (slots.fileSizes[file] - ? language.$('releaseInfo.additionalFiles.file.withSize', { - file: slots.fileLinks[file], - size: language.formatFileSize(slots.fileSizes[file]), - }) - : language.$('releaseInfo.additionalFiles.file', { - file: slots.fileLinks[file], - })))))), - ])); - }, - }); + if (!slots.fileSizes) { + return html.blank(); + } + + const filesWithLinks = new Set( + Object.entries(slots.fileLinks) + .filter(([key, value]) => value) + .map(([key]) => key)); + + if (filesWithLinks.size === 0) { + return html.blank(); + } + + const filteredFileGroups = data.additionalFiles + .map(({title, description, files}) => ({ + title, + description, + files: files.filter(f => filesWithLinks.has(f)), + })) + .filter(({files}) => !empty(files)); + + if (empty(filteredFileGroups)) { + return html.blank(); + } + + return html.tag('dl', + filteredFileGroups.flatMap(({title, description, files}) => [ + html.tag('dt', + (description + ? language.$('releaseInfo.additionalFiles.entry.withDescription', { + title, + description, + }) + : language.$('releaseInfo.additionalFiles.entry', {title}))), + + html.tag('dd', + html.tag('ul', + files.map(file => + html.tag('li', + (slots.fileSizes[file] + ? language.$('releaseInfo.additionalFiles.file.withSize', { + file: slots.fileLinks[file], + size: language.formatFileSize(slots.fileSizes[file]), + }) + : language.$('releaseInfo.additionalFiles.file', { + file: slots.fileLinks[file], + })))))), + ])); }, }; diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index 9d1d87c3..0237fdec 100644 --- a/src/content/dependencies/generateAlbumNavAccent.js +++ b/src/content/dependencies/generateAlbumNavAccent.js @@ -53,68 +53,62 @@ export default { }; }, - generate(data, relations, {html, language}) { - return html.template({ - annotation: `generateAlbumNavAccent`, - - slots: { - showTrackNavigation: {type: 'boolean', default: false}, - showExtraLinks: {type: 'boolean', default: false}, - - currentExtra: { - validate: v => v.is('gallery', 'commentary'), - }, - }, - - content(slots) { - const {content: extraLinks = []} = - slots.showExtraLinks && - {content: [ - relations.albumGalleryLink?.slots({ - attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('albumPage.nav.gallery'), - }), - - relations.albumCommentaryLink?.slots({ - attributes: {class: slots.currentExtra === 'commentary' && 'current'}, - content: language.$('albumPage.nav.commentary'), - }), - ]}; - - const {content: previousNextLinks = []} = - slots.showTrackNavigation && - data.isTrackPage && - data.hasMultipleTracks && - relations.previousNextLinks.slots({ - previousLink: relations.previousTrackLink, - nextLink: relations.nextTrackLink, - }); - - const randomLink = - slots.showTrackNavigation && - data.hasMultipleTracks && - html.tag('a', - { - href: '#', - 'data-random': 'track-in-album', - id: 'random-button', - }, - (data.isTrackPage - ? language.$('trackPage.nav.random') - : language.$('albumPage.nav.randomTrack'))); - - const allLinks = [ - ...previousNextLinks, - ...extraLinks, - randomLink, - ].filter(Boolean); - - if (empty(allLinks)) { - return html.blank(); - } - - return `(${language.formatUnitList(allLinks)})` - }, - }); + slots: { + showTrackNavigation: {type: 'boolean', default: false}, + showExtraLinks: {type: 'boolean', default: false}, + + currentExtra: { + validate: v => v.is('gallery', 'commentary'), + }, + }, + + generate(data, relations, slots, {html, language}) { + const {content: extraLinks = []} = + slots.showExtraLinks && + {content: [ + relations.albumGalleryLink?.slots({ + attributes: {class: slots.currentExtra === 'gallery' && 'current'}, + content: language.$('albumPage.nav.gallery'), + }), + + relations.albumCommentaryLink?.slots({ + attributes: {class: slots.currentExtra === 'commentary' && 'current'}, + content: language.$('albumPage.nav.commentary'), + }), + ]}; + + const {content: previousNextLinks = []} = + slots.showTrackNavigation && + data.isTrackPage && + data.hasMultipleTracks && + relations.previousNextLinks.slots({ + previousLink: relations.previousTrackLink, + nextLink: relations.nextTrackLink, + }); + + const randomLink = + slots.showTrackNavigation && + data.hasMultipleTracks && + html.tag('a', + { + href: '#', + 'data-random': 'track-in-album', + id: 'random-button', + }, + (data.isTrackPage + ? language.$('trackPage.nav.random') + : language.$('albumPage.nav.randomTrack'))); + + const allLinks = [ + ...previousNextLinks, + ...extraLinks, + randomLink, + ].filter(Boolean); + + if (empty(allLinks)) { + return html.blank(); + } + + return `(${language.formatUnitList(allLinks)})`; }, }; diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js index 1c27af9e..94536c3b 100644 --- a/src/content/dependencies/generateAlbumSidebarGroupBox.js +++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js @@ -43,46 +43,40 @@ export default { return relations; }, - generate(relations, {html, language}) { - return html.template({ - annotation: `generateAlbumSidebarGroupBox`, - - slots: { - isAlbumPage: {type: 'boolean', default: false}, - }, - - content(slots) { - return html.tags([ - html.tag('h1', - language.$('albumSidebar.groupBox.title', { - group: relations.groupLink, - })), - - slots.isAlbumPage && - relations.description - ?.slot('mode', 'multiline'), - - !empty(relations.externalLinks) && - html.tag('p', - language.$('releaseInfo.visitOn', { - links: language.formatDisjunctionList(relations.externalLinks), - })), - - slots.isAlbumPage && - relations.nextAlbumLink && - html.tag('p', {class: 'group-chronology-link'}, - language.$('albumSidebar.groupBox.next', { - album: relations.nextAlbumLink, - })), - - slots.isAlbumPage && - relations.previousAlbumLink && - html.tag('p', {class: 'group-chronology-link'}, - language.$('albumSidebar.groupBox.previous', { - album: relations.previousAlbumLink, - })), - ]); - }, - }); + slots: { + isAlbumPage: {type: 'boolean', default: false}, + }, + + generate(relations, slots, {html, language}) { + return html.tags([ + html.tag('h1', + language.$('albumSidebar.groupBox.title', { + group: relations.groupLink, + })), + + slots.isAlbumPage && + relations.description + ?.slot('mode', 'multiline'), + + !empty(relations.externalLinks) && + html.tag('p', + language.$('releaseInfo.visitOn', { + links: language.formatDisjunctionList(relations.externalLinks), + })), + + slots.isAlbumPage && + relations.nextAlbumLink && + html.tag('p', {class: 'group-chronology-link'}, + language.$('albumSidebar.groupBox.next', { + album: relations.nextAlbumLink, + })), + + slots.isAlbumPage && + relations.previousAlbumLink && + html.tag('p', {class: 'group-chronology-link'}, + language.$('albumSidebar.groupBox.previous', { + album: relations.previousAlbumLink, + })), + ]); }, }; diff --git a/src/content/dependencies/generateArtistNavLinks.js b/src/content/dependencies/generateArtistNavLinks.js index f283b30d..f78b45a1 100644 --- a/src/content/dependencies/generateArtistNavLinks.js +++ b/src/content/dependencies/generateArtistNavLinks.js @@ -40,66 +40,61 @@ export default { }; }, - generate(data, relations, {html, language}) { - return html.template({ - annotation: `generateArtistNav`, - slots: { - showExtraLinks: {type: 'boolean', default: false}, - - currentExtra: { - validate: v => v.is('gallery'), + slots: { + showExtraLinks: {type: 'boolean', default: false}, + + currentExtra: { + validate: v => v.is('gallery'), + }, + }, + + generate(data, relations, slots, {html, language}) { + const infoLink = + relations.artistInfoLink?.slots({ + attributes: {class: slots.currentExtra === null && 'current'}, + content: language.$('misc.nav.info'), + }); + + const {content: extraLinks = []} = + slots.showExtraLinks && + {content: [ + relations.artistGalleryLink?.slots({ + attributes: {class: slots.currentExtra === 'gallery' && 'current'}, + content: language.$('misc.nav.gallery'), + }), + ]}; + + const mostAccentLinks = [ + ...extraLinks, + ].filter(Boolean); + + // Don't show the info accent link all on its own. + const allAccentLinks = + (empty(mostAccentLinks) + ? [] + : [infoLink, ...mostAccentLinks]); + + const accent = + (empty(allAccentLinks) + ? html.blank() + : `(${language.formatUnitList(allAccentLinks)})`); + + return [ + {auto: 'home'}, + + data.enableListings && + { + path: ['localized.listingIndex'], + title: language.$('listingIndex.title'), }, - }, - content(slots) { - const infoLink = - relations.artistInfoLink?.slots({ - attributes: {class: slots.currentExtra === null && 'current'}, - content: language.$('misc.nav.info'), - }); - - const {content: extraLinks = []} = - slots.showExtraLinks && - {content: [ - relations.artistGalleryLink?.slots({ - attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('misc.nav.gallery'), - }), - ]}; - - const mostAccentLinks = [ - ...extraLinks, - ].filter(Boolean); - - // Don't show the info accent link all on its own. - const allAccentLinks = - (empty(mostAccentLinks) - ? [] - : [infoLink, ...mostAccentLinks]); - - const accent = - (empty(allAccentLinks) - ? html.blank() - : `(${language.formatUnitList(allAccentLinks)})`); - - return [ - {auto: 'home'}, - - data.enableListings && - { - path: ['localized.listingIndex'], - title: language.$('listingIndex.title'), - }, - - { - accent, - html: - language.$('artistPage.nav.artist', { - artist: relations.artistMainLink, - }), - }, - ]; + { + accent, + html: + language.$('artistPage.nav.artist', { + artist: relations.artistMainLink, + }), }, - }); + ]; }, }; diff --git a/src/content/dependencies/generateChronologyLinks.js b/src/content/dependencies/generateChronologyLinks.js index a61b5e6f..15c0898c 100644 --- a/src/content/dependencies/generateChronologyLinks.js +++ b/src/content/dependencies/generateChronologyLinks.js @@ -3,86 +3,80 @@ import {accumulateSum, empty} from '../../util/sugar.js'; export default { extraDependencies: ['html', 'language'], - generate({html, language}) { - return html.template({ - annotation: `generateChronologyLinks`, - - slots: { - chronologyInfoSets: { - validate: v => - v.arrayOf( - v.validateProperties({ - headingString: v.isString, - contributions: v.arrayOf(v.validateProperties({ - index: v.isCountingNumber, - artistLink: v.isHTML, - previousLink: v.isHTML, - nextLink: v.isHTML, - })), - })), - } - }, + slots: { + chronologyInfoSets: { + validate: v => + v.arrayOf( + v.validateProperties({ + headingString: v.isString, + contributions: v.arrayOf(v.validateProperties({ + index: v.isCountingNumber, + artistLink: v.isHTML, + previousLink: v.isHTML, + nextLink: v.isHTML, + })), + })), + } + }, - content(slots) { - if (empty(slots.chronologyInfoSets)) { - return html.blank(); - } + generate(slots, {html, language}) { + if (empty(slots.chronologyInfoSets)) { + return html.blank(); + } - const totalContributionCount = - accumulateSum( - slots.chronologyInfoSets, - ({contributions}) => contributions.length); + const totalContributionCount = + accumulateSum( + slots.chronologyInfoSets, + ({contributions}) => contributions.length); - if (totalContributionCount === 0) { - return html.blank(); - } + if (totalContributionCount === 0) { + return html.blank(); + } - if (totalContributionCount > 8) { - return html.tag('div', {class: 'chronology'}, - language.$('misc.chronology.seeArtistPages')); - } + if (totalContributionCount > 8) { + return html.tag('div', {class: 'chronology'}, + language.$('misc.chronology.seeArtistPages')); + } - return html.tags( - slots.chronologyInfoSets.map(({ - headingString, - contributions, - }) => - contributions.map(({ - index, - artistLink, - previousLink, - nextLink, - }) => { - const heading = - html.tag('span', {class: 'heading'}, - language.$(headingString, { - index: language.formatIndex(index), - artist: artistLink, - })); + return html.tags( + slots.chronologyInfoSets.map(({ + headingString, + contributions, + }) => + contributions.map(({ + index, + artistLink, + previousLink, + nextLink, + }) => { + const heading = + html.tag('span', {class: 'heading'}, + language.$(headingString, { + index: language.formatIndex(index), + artist: artistLink, + })); - const navigation = - (previousLink || nextLink) && - html.tag('span', {class: 'buttons'}, - language.formatUnitList([ - previousLink?.slots({ - tooltip: true, - color: false, - content: language.$('misc.nav.previous'), - }), + const navigation = + (previousLink || nextLink) && + html.tag('span', {class: 'buttons'}, + language.formatUnitList([ + previousLink?.slots({ + tooltip: true, + color: false, + content: language.$('misc.nav.previous'), + }), - nextLink?.slots({ - tooltip: true, - color: false, - content: language.$('misc.nav.next'), - }), - ].filter(Boolean))); + nextLink?.slots({ + tooltip: true, + color: false, + content: language.$('misc.nav.next'), + }), + ].filter(Boolean))); - return html.tag('div', {class: 'chronology'}, - (navigation - ? language.$('misc.chronology.withNavigation', {heading, navigation}) - : heading)); - }))); - }, - }); + return html.tag('div', {class: 'chronology'}, + (navigation + ? language.$('misc.chronology.withNavigation', {heading, navigation}) + : heading)); + }))); }, }; diff --git a/src/content/dependencies/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js index 1666ef4b..ccaf1076 100644 --- a/src/content/dependencies/generateContentHeading.js +++ b/src/content/dependencies/generateContentHeading.js @@ -1,27 +1,19 @@ export default { - extraDependencies: [ - 'html', - ], + extraDependencies: ['html'], - generate({html}) { - return html.template({ - annotation: 'generateContentHeading', + slots: { + title: {type: 'html'}, + id: {type: 'string'}, + tag: {type: 'string', default: 'p'}, + }, - slots: { - title: {type: 'html'}, - id: {type: 'string'}, - tag: {type: 'string', default: 'p'}, + generate(slots, {html}) { + return html.tag(slots.tag, + { + class: 'content-heading', + id: slots.id, + tabindex: '0', }, - - content(slots) { - return html.tag(slots.tag, - { - class: 'content-heading', - id: slots.id, - tabindex: '0', - }, - slots.title); - }, - }); + slots.title); } } diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index a7a7f859..e9c91cf7 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -22,62 +22,56 @@ export default { return relations; }, - generate(relations, {html, language}) { - return html.template({ - annotation: 'generateCoverArtwork', + slots: { + path: { + validate: v => v.validateArrayItems(v.isString), + }, - slots: { - path: { - validate: v => v.validateArrayItems(v.isString), - }, + alt: { + type: 'string', + }, - alt: { - type: 'string', - }, - - displayMode: { - validate: v => v.is('primary', 'thumbnail'), - default: 'primary', - }, - }, + displayMode: { + validate: v => v.is('primary', 'thumbnail'), + default: 'primary', + }, + }, - content(slots) { - switch (slots.displayMode) { - case 'primary': - return html.tag('div', {id: 'cover-art-container'}, [ - relations.image - .slots({ - path: slots.path, - alt: slots.alt, - thumb: 'medium', - id: 'cover-art', - reveal: true, - link: true, - square: true, - }), + generate(relations, slots, {html, language}) { + switch (slots.displayMode) { + case 'primary': + return html.tag('div', {id: 'cover-art-container'}, [ + relations.image + .slots({ + path: slots.path, + alt: slots.alt, + thumb: 'medium', + id: 'cover-art', + reveal: true, + link: true, + square: true, + }), - !empty(relations.tagLinks) && - html.tag('p', - language.$('releaseInfo.artTags.inline', { - tags: language.formatUnitList(relations.tagLinks), - })), - ]); + !empty(relations.tagLinks) && + html.tag('p', + language.$('releaseInfo.artTags.inline', { + tags: language.formatUnitList(relations.tagLinks), + })), + ]); - case 'thumbnail': - return relations.image - .slots({ - path: slots.path, - alt: slots.alt, - thumb: 'small', - reveal: false, - link: false, - square: true, - }); + case 'thumbnail': + return relations.image + .slots({ + path: slots.path, + alt: slots.alt, + thumb: 'small', + reveal: false, + link: false, + square: true, + }); - case 'default': - return html.blank(); - } - }, - }); + default: + return html.blank(); + } }, }; diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index fdd9f8b7..a024ae25 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -1,44 +1,38 @@ export default { extraDependencies: ['html'], - generate({html}) { - return html.template({ - annotation: `generateCoverGrid`, + slots: { + images: {validate: v => v.arrayOf(v.isHTML)}, + links: {validate: v => v.arrayOf(v.isHTML)}, + names: {validate: v => v.arrayOf(v.isString)}, - slots: { - images: {validate: v => v.arrayOf(v.isHTML)}, - links: {validate: v => v.arrayOf(v.isHTML)}, - names: {validate: v => v.arrayOf(v.isString)}, - - lazy: {validate: v => v.oneOf(v.isWholeNumber, v.isBoolean)}, - }, + lazy: {validate: v => v.oneOf(v.isWholeNumber, v.isBoolean)}, + }, - content(slots) { - return ( - html.tag('div', {class: 'grid-listing'}, - slots.images.map((image, i) => { - const link = slots.links[i]; - const name = slots.names[i]; - return link.slots({ - content: [ - image.slots({ - thumb: 'medium', - lazy: - (typeof slots.lazy === 'number' - ? i >= slots.lazy - : typeof slots.lazy === 'boolean' - ? slots.lazy - : false), - square: true, - }), - html.tag('span', name), - ], - attributes: { - class: ['grid-item', 'box', /* large && 'large-grid-item' */], - }, - }); - }))); - }, - }); + generate(slots, {html}) { + return ( + html.tag('div', {class: 'grid-listing'}, + slots.images.map((image, i) => { + const link = slots.links[i]; + const name = slots.names[i]; + return link.slots({ + content: [ + image.slots({ + thumb: 'medium', + lazy: + (typeof slots.lazy === 'number' + ? i >= slots.lazy + : typeof slots.lazy === 'boolean' + ? slots.lazy + : false), + square: true, + }), + html.tag('span', name), + ], + attributes: { + class: ['grid-item', 'box', /* large && 'large-grid-item' */], + }, + }); + }))); }, }; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 84acca0b..610b4a1f 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -1,5 +1,48 @@ import {empty, openAggregate} from '../../util/sugar.js'; +function sidebarSlots(side) { + return { + // Content is a flat HTML array. It'll generate one sidebar section + // if specified. + [side + 'Content']: {type: 'html'}, + + // Multiple is an array of {content: (HTML)} objects. Each of these + // will generate one sidebar section. + [side + 'Multiple']: { + validate: v => + v.arrayOf( + v.validateProperties({ + content: v.isHTML, + })), + }, + + // Sticky mode controls which sidebar section(s), if any, follow the + // scroll position, "sticking" to the top of the browser viewport. + // + // '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 + // + // Note: This doesn't affect the content of any sidebar section, only + // the whole section's containing box (or the sidebar column as a whole). + [side + 'StickyMode']: { + validate: v => v.is('last', 'column', 'static'), + }, + + // Collapsing sidebars disappear when the viewport is sufficiently + // thin. (This is the default.) Override as false to make the sidebar + // stay visible in thinner viewports, where the page layout will be + // reflowed so the sidebar is as wide as the screen and appears below + // nav, above the main content. + [side + 'Collapse']: {type: 'boolean', default: true}, + + // Wide sidebars generally take up more horizontal space in the normal + // page layout, and should be used if the content of the sidebar has + // a greater than typical focus compared to main content. + [side + 'Wide']: {type: 'boolean', defualt: false}, + }; +} + export default { contentDependencies: [ 'generateColorStyleRules', @@ -49,495 +92,446 @@ export default { return relations; }, - generate(data, relations, { + slots: { + title: {type: 'html'}, + showWikiNameInTitle: {type: 'boolean', default: true}, + + cover: {type: 'html'}, + + socialEmbed: {type: 'html'}, + + colorStyleRules: { + validate: v => v.arrayOf(v.isString), + default: [], + }, + + additionalStyleRules: { + validate: v => v.arrayOf(v.isString), + default: [], + }, + + mainClasses: { + validate: v => v.arrayOf(v.isString), + default: [], + }, + + // Main + + mainContent: {type: 'html'}, + + headingMode: { + validate: v => v.is('sticky', 'static'), + default: 'static', + }, + + // Sidebars + + ...sidebarSlots('leftSidebar'), + ...sidebarSlots('rightSidebar'), + + // Nav & Footer + + navContent: {type: 'html'}, + navBottomRowContent: {type: 'html'}, + + navLinkStyle: { + validate: v => v.is('hierarchical', 'index'), + default: 'index', + }, + + navLinks: { + validate: v => + v.arrayOf(object => { + v.isObject(object); + + const aggregate = openAggregate({message: `Errors validating navigation link`}); + + aggregate.call(v.validateProperties({ + auto: () => true, + html: () => true, + + path: () => true, + title: () => true, + accent: () => true, + }), object); + + if (object.auto || object.html) { + if (object.auto && object.html) { + aggregate.push(new TypeError(`Don't specify both auto and html`)); + } else if (object.auto) { + aggregate.call(v.is('home', 'current'), object.auto); + } else { + aggregate.call(v.isHTML, object.html); + } + + if (object.path || object.title) { + aggregate.push(new TypeError(`Don't specify path or title along with auto or html`)); + } + } else { + aggregate.call(v.validateProperties({ + path: v.arrayOf(v.isString), + title: v.isString, + }), { + path: object.path, + title: object.title, + }); + } + + aggregate.close(); + + return true; + }) + }, + + footerContent: {type: 'html'}, + }, + + generate(data, relations, slots, { cachebust, html, language, to, }) { - const sidebarSlots = side => ({ - // Content is a flat HTML array. It'll generate one sidebar section - // if specified. - [side + 'Content']: {type: 'html'}, - - // Multiple is an array of {content: (HTML)} objects. Each of these - // will generate one sidebar section. - [side + 'Multiple']: { - validate: v => - v.arrayOf( - v.validateProperties({ - content: v.isHTML, - })), - }, - - // Sticky mode controls which sidebar section(s), if any, follow the - // scroll position, "sticking" to the top of the browser viewport. - // - // '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 - // - // Note: This doesn't affect the content of any sidebar section, only - // the whole section's containing box (or the sidebar column as a whole). - [side + 'StickyMode']: { - validate: v => v.is('last', 'column', 'static'), - }, - - // Collapsing sidebars disappear when the viewport is sufficiently - // thin. (This is the default.) Override as false to make the sidebar - // stay visible in thinner viewports, where the page layout will be - // reflowed so the sidebar is as wide as the screen and appears below - // nav, above the main content. - [side + 'Collapse']: {type: 'boolean', default: true}, - - // Wide sidebars generally take up more horizontal space in the normal - // page layout, and should be used if the content of the sidebar has - // a greater than typical focus compared to main content. - [side + 'Wide']: {type: 'boolean', defualt: false}, - }); - - return html.template({ - annotation: 'generatePageLayout', - - slots: { - title: {type: 'html'}, - showWikiNameInTitle: {type: 'boolean', default: true}, - - cover: {type: 'html'}, - - socialEmbed: {type: 'html'}, - - colorStyleRules: { - validate: v => v.arrayOf(v.isString), - default: [], - }, - - additionalStyleRules: { - validate: v => v.arrayOf(v.isString), - default: [], - }, - - mainClasses: { - validate: v => v.arrayOf(v.isString), - default: [], - }, - - // Main - - mainContent: {type: 'html'}, - - headingMode: { - validate: v => v.is('sticky', 'static'), - default: 'static', - }, - - // Sidebars - - ...sidebarSlots('leftSidebar'), - ...sidebarSlots('rightSidebar'), - - // Nav & Footer - - navContent: {type: 'html'}, - navBottomRowContent: {type: 'html'}, - - navLinkStyle: { - validate: v => v.is('hierarchical', 'index'), - default: 'index', - }, - - navLinks: { - validate: v => - v.arrayOf(object => { - v.isObject(object); - - const aggregate = openAggregate({message: `Errors validating navigation link`}); - - aggregate.call(v.validateProperties({ - auto: () => true, - html: () => true, - - path: () => true, - title: () => true, - accent: () => true, - }), object); - - if (object.auto || object.html) { - if (object.auto && object.html) { - aggregate.push(new TypeError(`Don't specify both auto and html`)); - } else if (object.auto) { - aggregate.call(v.is('home', 'current'), object.auto); - } else { - aggregate.call(v.isHTML, object.html); - } - - if (object.path || object.title) { - aggregate.push(new TypeError(`Don't specify path or title along with auto or html`)); - } - } else { - aggregate.call(v.validateProperties({ - path: v.arrayOf(v.isString), - title: v.isString, - }), { - path: object.path, - title: object.title, - }); - } + let titleHTML = null; + + if (!html.isBlank(slots.title)) { + switch (slots.headingMode) { + case 'sticky': + titleHTML = + relations.stickyHeadingContainer.slots({ + title: slots.title, + cover: slots.cover, + }); + break; + case 'static': + titleHTML = html.tag('h1', slots.title); + break; + } + } + + let footerContent = slots.footerContent; + + if (html.isBlank(footerContent)) { + footerContent = relations.defaultFooterContent + .slot('mode', 'multiline'); + } + + const mainHTML = + html.tag('main', { + id: 'content', + class: slots.mainClasses, + }, [ + titleHTML, + + slots.cover, + + html.tag('div', + { + [html.onlyIfContent]: true, + class: 'main-content-container', + }, + slots.mainContent), + ]); - aggregate.close(); + const footerHTML = + html.tag('footer', + {[html.onlyIfContent]: true, id: 'footer'}, + [ + html.tag('div', + { + [html.onlyIfContent]: true, + class: 'footer-content', + }, + footerContent), - return true; - }) - }, + relations.footerLocalizationLinks, + ]); - footerContent: {type: 'html'}, + const navHTML = html.tag('nav', + { + [html.onlyIfContent]: true, + id: 'header', + class: [ + !empty(slots.navLinks) && 'nav-has-main-links', + !html.isBlank(slots.navContent) && 'nav-has-content', + !html.isBlank(slots.navBottomRowContent) && 'nav-has-bottom-row', + ], }, - - content(slots) { - let titleHTML = null; - - if (!html.isBlank(slots.title)) { - switch (slots.headingMode) { - case 'sticky': - titleHTML = - relations.stickyHeadingContainer.slots({ - title: slots.title, - cover: slots.cover, - }); - break; - case 'static': - titleHTML = html.tag('h1', slots.title); - break; - } - } - - let footerContent = slots.footerContent; - - if (html.isBlank(footerContent)) { - footerContent = relations.defaultFooterContent - .slot('mode', 'multiline'); - } - - const mainHTML = - html.tag('main', { - id: 'content', - class: slots.mainClasses, - }, [ - titleHTML, - - slots.cover, - - html.tag('div', - { - [html.onlyIfContent]: true, - class: 'main-content-container', - }, - slots.mainContent), - ]); - - const footerHTML = - html.tag('footer', - {[html.onlyIfContent]: true, id: 'footer'}, - [ - html.tag('div', - { - [html.onlyIfContent]: true, - class: 'footer-content', - }, - footerContent), - - relations.footerLocalizationLinks, - ]); - - const navHTML = html.tag('nav', + [ + html.tag('div', { [html.onlyIfContent]: true, - id: 'header', class: [ - !empty(slots.navLinks) && 'nav-has-main-links', - !html.isBlank(slots.navContent) && 'nav-has-content', - !html.isBlank(slots.navBottomRowContent) && 'nav-has-bottom-row', + 'nav-main-links', + 'nav-links-' + slots.navLinkStyle, ], }, - [ - html.tag('div', - { - [html.onlyIfContent]: true, - class: [ - 'nav-main-links', - 'nav-links-' + slots.navLinkStyle, - ], - }, - slots.navLinks?.map((cur, i) => { - let content; - - if (cur.html) { - content = cur.html; - } else { - let title; - let href; - - switch (cur.auto) { - case 'home': - title = data.wikiName; - href = to('localized.home'); - break; - case 'current': - title = slots.title; - href = ''; - break; - case null: - case undefined: - title = cur.title; - href = to(...cur.path); - break; - } - - content = html.tag('a', - {href}, - title); - } - - let className; - - if (cur.auto === 'current') { - className = 'current'; - } else if ( - slots.navLinkStyle === 'hierarchical' && - i === slots.navLinks.length - 1 - ) { - className = 'current'; - } - - return html.tag('span', - {class: className}, - [ - html.tag('span', - {class: 'nav-link-content'}, - content), - html.tag('span', - {[html.onlyIfContent]: true, class: 'nav-link-accent'}, - cur.accent), - ]); - })), - - html.tag('div', - {[html.onlyIfContent]: true, class: 'nav-bottom-row'}, - slots.navBottomRowContent), - - html.tag('div', - {[html.onlyIfContent]: true, class: 'nav-content'}, - slots.navContent), - ]) - - const generateSidebarHTML = (side, id) => { - const content = slots[side + 'Content']; - const multiple = slots[side + 'Multiple']; - const stickyMode = slots[side + 'StickyMode']; - const wide = slots[side + 'Wide']; - const collapse = slots[side + 'Collapse']; - - let sidebarClasses = []; - let sidebarContent = html.blank(); - - if (!html.isBlank(content)) { - sidebarClasses = ['sidebar']; - sidebarContent = content; - } else if (multiple) { - sidebarClasses = ['sidebar-multiple']; - sidebarContent = - multiple - .filter(Boolean) - .map(({content}) => - html.tag('div', - { - [html.onlyIfContent]: true, - class: 'sidebar', - }, - content)); - } + slots.navLinks?.map((cur, i) => { + let content; + + if (cur.html) { + content = cur.html; + } else { + let title; + let href; + + switch (cur.auto) { + case 'home': + title = data.wikiName; + href = to('localized.home'); + break; + case 'current': + title = slots.title; + href = ''; + break; + case null: + case undefined: + title = cur.title; + href = to(...cur.path); + break; + } - return html.tag('div', - { - [html.onlyIfContent]: true, - id, - class: [ - 'sidebar-column', - wide && 'wide', - !collapse && 'no-hide', - stickyMode !== 'static' && `sticky-${stickyMode}`, - ...sidebarClasses, - ], - }, - sidebarContent); - } - - const sidebarLeftHTML = generateSidebarHTML('leftSidebar', 'sidebar-left'); - const sidebarRightHTML = generateSidebarHTML('rightSidebar', 'sidebar-right'); - const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse; - - const imageOverlayHTML = html.tag('div', {id: 'image-overlay-container'}, - html.tag('div', {id: 'image-overlay-content-container'}, [ - html.tag('a', {id: 'image-overlay-image-container'}, [ - html.tag('img', {id: 'image-overlay-image'}), - html.tag('img', {id: 'image-overlay-image-thumb'}), - ]), - html.tag('div', {id: 'image-overlay-action-container'}, [ - html.tag('div', {id: 'image-overlay-action-content-without-size'}, - language.$('releaseInfo.viewOriginalFile', { - link: html.tag('a', {class: 'image-overlay-view-original'}, - language.$('releaseInfo.viewOriginalFile.link')), - })), - - html.tag('div', {id: 'image-overlay-action-content-with-size'}, [ - language.$('releaseInfo.viewOriginalFile.withSize', { - link: html.tag('a', {class: 'image-overlay-view-original'}, - language.$('releaseInfo.viewOriginalFile.link')), - size: html.tag('span', - {[html.joinChildren]: ''}, - [ - html.tag('span', {id: 'image-overlay-file-size-kilobytes'}, - language.$('count.fileSize.kilobytes', { - kilobytes: html.tag('span', {class: 'image-overlay-file-size-count'}), - })), - html.tag('span', {id: 'image-overlay-file-size-megabytes'}, - language.$('count.fileSize.megabytes', { - megabytes: html.tag('span', {class: 'image-overlay-file-size-count'}), - })), - ]), - }), - - html.tag('span', {id: 'image-overlay-file-size-warning'}, - language.$('releaseInfo.viewOriginalFile.sizeWarning')), - ]), - ]), - ])); + content = html.tag('a', + {href}, + title); + } + + let className; + + if (cur.auto === 'current') { + className = 'current'; + } else if ( + slots.navLinkStyle === 'hierarchical' && + i === slots.navLinks.length - 1 + ) { + className = 'current'; + } + + return html.tag('span', + {class: className}, + [ + html.tag('span', + {class: 'nav-link-content'}, + content), + html.tag('span', + {[html.onlyIfContent]: true, class: 'nav-link-accent'}, + cur.accent), + ]); + })), + + html.tag('div', + {[html.onlyIfContent]: true, class: 'nav-bottom-row'}, + slots.navBottomRowContent), + + html.tag('div', + {[html.onlyIfContent]: true, class: 'nav-content'}, + slots.navContent), + ]) + + const generateSidebarHTML = (side, id) => { + const content = slots[side + 'Content']; + const multiple = slots[side + 'Multiple']; + const stickyMode = slots[side + 'StickyMode']; + const wide = slots[side + 'Wide']; + const collapse = slots[side + 'Collapse']; + + let sidebarClasses = []; + let sidebarContent = html.blank(); + + if (!html.isBlank(content)) { + sidebarClasses = ['sidebar']; + sidebarContent = content; + } else if (multiple) { + sidebarClasses = ['sidebar-multiple']; + sidebarContent = + multiple + .filter(Boolean) + .map(({content}) => + html.tag('div', + { + [html.onlyIfContent]: true, + class: 'sidebar', + }, + content)); + } + + return html.tag('div', + { + [html.onlyIfContent]: true, + id, + class: [ + 'sidebar-column', + wide && 'wide', + !collapse && 'no-hide', + stickyMode !== 'static' && `sticky-${stickyMode}`, + ...sidebarClasses, + ], + }, + sidebarContent); + } + + const sidebarLeftHTML = generateSidebarHTML('leftSidebar', 'sidebar-left'); + const sidebarRightHTML = generateSidebarHTML('rightSidebar', 'sidebar-right'); + const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse; + + const imageOverlayHTML = html.tag('div', {id: 'image-overlay-container'}, + html.tag('div', {id: 'image-overlay-content-container'}, [ + html.tag('a', {id: 'image-overlay-image-container'}, [ + html.tag('img', {id: 'image-overlay-image'}), + html.tag('img', {id: 'image-overlay-image-thumb'}), + ]), + html.tag('div', {id: 'image-overlay-action-container'}, [ + html.tag('div', {id: 'image-overlay-action-content-without-size'}, + language.$('releaseInfo.viewOriginalFile', { + link: html.tag('a', {class: 'image-overlay-view-original'}, + language.$('releaseInfo.viewOriginalFile.link')), + })), - 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, + html.tag('div', {id: 'image-overlay-action-content-with-size'}, [ + language.$('releaseInfo.viewOriginalFile.withSize', { + link: html.tag('a', {class: 'image-overlay-view-original'}, + language.$('releaseInfo.viewOriginalFile.link')), + size: html.tag('span', + {[html.joinChildren]: ''}, + [ + html.tag('span', {id: 'image-overlay-file-size-kilobytes'}, + language.$('count.fileSize.kilobytes', { + kilobytes: html.tag('span', {class: 'image-overlay-file-size-count'}), + })), + html.tag('span', {id: 'image-overlay-file-size-megabytes'}, + language.$('count.fileSize.megabytes', { + megabytes: html.tag('span', {class: 'image-overlay-file-size-count'}), + })), + ]), + }), + + html.tag('span', {id: 'image-overlay-file-size-warning'}, + language.$('releaseInfo.viewOriginalFile.sizeWarning')), + ]), + ]), + ])); + + 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'); + + return html.tags([ + `<!DOCTYPE html>`, + html.tag('html', + { + lang: language.intlCode, + 'data-language-code': language.code, + + /* + 'data-url-key': 'localized.' + pagePath[0], + ...Object.fromEntries( + pagePath.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'), + }, + [ + // developersComment, + + html.tag('head', [ + html.tag('title', + (slots.showWikiNameInTitle + ? language.formatString('misc.pageTitle.withWikiName', { + title: slots.title, + wikiName: data.wikiName, + }) + : language.formatString('misc.pageTitle', { + title: slots.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, + }))), + + */ + + // slots.socialEmbed, + + html.tag('link', { + rel: 'stylesheet', + href: to('shared.staticFile', `site4.css?${cachebust}`), + }), + + html.tag('style', [ + (empty(slots.colorStyleRules) + ? relations.defaultColorStyleRules + : slots.colorStyleRules), + slots.additionalStyleRules, ]), - // banner.position === 'bottom' && bannerHTML, - footerHTML, - ].filter(Boolean).join('\n'); - const documentHTML = html.tags([ - `<!DOCTYPE html>`, - html.tag('html', - { - lang: language.intlCode, - 'data-language-code': language.code, - - /* - 'data-url-key': 'localized.' + pagePath[0], - ...Object.fromEntries( - pagePath.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'), - }, - [ - // developersComment, - - html.tag('head', [ - html.tag('title', - (slots.showWikiNameInTitle - ? language.formatString('misc.pageTitle.withWikiName', { - title: slots.title, - wikiName: data.wikiName, - }) - : language.formatString('misc.pageTitle', { - title: slots.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, - }))), - - */ - - // slots.socialEmbed, - - html.tag('link', { - rel: 'stylesheet', - href: to('shared.staticFile', `site4.css?${cachebust}`), - }), - - html.tag('style', [ - (empty(slots.colorStyleRules) - ? relations.defaultColorStyleRules - : slots.colorStyleRules), - slots.additionalStyleRules, - ]), + html.tag('script', { + src: to('shared.staticFile', `lazy-loading.js?${cachebust}`), + }), + ]), - html.tag('script', { - src: to('shared.staticFile', `lazy-loading.js?${cachebust}`), - }), + html.tag('body', + // {style: body.style || ''}, + [ + html.tag('div', {id: 'page-container'}, [ + // mainHTML && skippersHTML, + layoutHTML, ]), - html.tag('body', - // {style: body.style || ''}, - [ - html.tag('div', {id: 'page-container'}, [ - // mainHTML && skippersHTML, - layoutHTML, - ]), - - // infoCardHTML, - imageOverlayHTML, - - html.tag('script', { - type: 'module', - src: to('shared.staticFile', `client.js?${cachebust}`), - }), - ]), - ]) - ]); + // infoCardHTML, + imageOverlayHTML, - return documentHTML; - }, - }); + html.tag('script', { + type: 'module', + src: to('shared.staticFile', `client.js?${cachebust}`), + }), + ]), + ]) + ]); }, }; diff --git a/src/content/dependencies/generatePreviousNextLinks.js b/src/content/dependencies/generatePreviousNextLinks.js index 42b2c42b..5bdcc3ad 100644 --- a/src/content/dependencies/generatePreviousNextLinks.js +++ b/src/content/dependencies/generatePreviousNextLinks.js @@ -5,32 +5,26 @@ export default { extraDependencies: ['html', 'language'], - generate({html, language}) { - return html.template({ - annotation: `generatePreviousNextLinks`, - - slots: { - previousLink: {type: 'html'}, - nextLink: {type: 'html'}, - }, + slots: { + previousLink: {type: 'html'}, + nextLink: {type: 'html'}, + }, - content(slots) { - return [ - !html.isBlank(slots.previousLink) && - slots.previousLink.slots({ - tooltip: true, - attributes: {id: 'previous-button'}, - content: language.$('misc.nav.previous'), - }), + generate(slots, {html, language}) { + return html.tags([ + !html.isBlank(slots.previousLink) && + slots.previousLink.slots({ + tooltip: true, + attributes: {id: 'previous-button'}, + content: language.$('misc.nav.previous'), + }), - !html.isBlank(slots.nextLink) && - slots.nextLink?.slots({ - tooltip: true, - attributes: {id: 'next-button'}, - content: language.$('misc.nav.next'), - }), - ].filter(Boolean); - }, - }); + !html.isBlank(slots.nextLink) && + slots.nextLink?.slots({ + tooltip: true, + attributes: {id: 'next-button'}, + content: language.$('misc.nav.next'), + }), + ]); }, }; diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js index 2b342d09..78d3e506 100644 --- a/src/content/dependencies/generateReleaseInfoContributionsLine.js +++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js @@ -18,32 +18,26 @@ export default { }; }, - generate(relations, {html, language}) { - return html.template({ - annotation: `generateReleaseInfoContributionsLine`, + slots: { + stringKey: {type: 'string'}, - slots: { - stringKey: {type: 'string'}, - - showContribution: {type: 'boolean', default: true}, - showIcons: {type: 'boolean', default: true}, - }, + showContribution: {type: 'boolean', default: true}, + showIcons: {type: 'boolean', default: true}, + }, - content(slots) { - if (!relations.contributionLinks) { - return html.blank(); - } + generate(relations, slots, {html, language}) { + if (!relations.contributionLinks) { + return html.blank(); + } - return language.$(slots.stringKey, { - artists: - language.formatConjunctionList( - relations.contributionLinks.map(link => - link.slots({ - showContribution: slots.showContribution, - showIcons: slots.showIcons, - }))), - }); - }, + return language.$(slots.stringKey, { + artists: + language.formatConjunctionList( + relations.contributionLinks.map(link => + link.slots({ + showContribution: slots.showContribution, + showIcons: slots.showIcons, + }))), }); }, }; diff --git a/src/content/dependencies/generateStickyHeadingContainer.js b/src/content/dependencies/generateStickyHeadingContainer.js index 6602a2a3..e5f7cc5d 100644 --- a/src/content/dependencies/generateStickyHeadingContainer.js +++ b/src/content/dependencies/generateStickyHeadingContainer.js @@ -1,39 +1,33 @@ export default { extraDependencies: ['html'], - generate({html}) { - return html.template({ - annotation: `generateStickyHeadingContainer`, - - slots: { - title: {type: 'html'}, - cover: {type: 'html'}, - }, + slots: { + title: {type: 'html'}, + cover: {type: 'html'}, + }, - content(slots) { - const hasCover = !html.isBlank(slots.cover); + generate(slots, {html}) { + const hasCover = !html.isBlank(slots.cover); - return html.tag('div', - { - class: [ - 'content-sticky-heading-container', - hasCover && 'has-cover', - ], - }, - [ - html.tag('div', {class: 'content-sticky-heading-row'}, [ - html.tag('h1', slots.title), + return html.tag('div', + { + class: [ + 'content-sticky-heading-container', + hasCover && 'has-cover', + ], + }, + [ + html.tag('div', {class: 'content-sticky-heading-row'}, [ + html.tag('h1', slots.title), - hasCover && - html.tag('div', {class: 'content-sticky-heading-cover-container'}, - html.tag('div', {class: 'content-sticky-heading-cover'}, - slots.cover.slot('displayMode', 'thumbnail'))), - ]), + hasCover && + html.tag('div', {class: 'content-sticky-heading-cover-container'}, + html.tag('div', {class: 'content-sticky-heading-cover'}, + slots.cover.slot('displayMode', 'thumbnail'))), + ]), - html.tag('div', {class: 'content-sticky-subheading-row'}, - html.tag('h2', {class: 'content-sticky-subheading'})), - ]); - }, - }); + html.tag('div', {class: 'content-sticky-subheading-row'}, + html.tag('h2', {class: 'content-sticky-subheading'})), + ]); }, }; diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js index e2e9f48d..6688a33d 100644 --- a/src/content/dependencies/generateTrackList.js +++ b/src/content/dependencies/generateTrackList.js @@ -22,34 +22,28 @@ export default { }; }, - generate(relations, {html, language}) { - return html.template({ - annotation: `generateTrackList`, - - slots: { - showContribution: {type: 'boolean', default: false}, - showIcons: {type: 'boolean', default: false}, - }, + slots: { + showContribution: {type: 'boolean', default: false}, + showIcons: {type: 'boolean', default: false}, + }, - content(slots) { - return html.tag('ul', - relations.items.map(({trackLink, contributionLinks}) => - html.tag('li', - language.$('trackList.item.withArtists', { - track: trackLink, - by: - html.tag('span', {class: 'by'}, - language.$('trackList.item.withArtists.by', { - artists: - language.formatConjunctionList( - contributionLinks.map(link => - link.slots({ - showContribution: slots.showContribution, - showIcons: slots.showIcons, - }))), - })), - })))); - }, - }); + generate(relations, slots, {html, language}) { + return html.tag('ul', + relations.items.map(({trackLink, contributionLinks}) => + html.tag('li', + language.$('trackList.item.withArtists', { + track: trackLink, + by: + html.tag('span', {class: 'by'}, + language.$('trackList.item.withArtists.by', { + artists: + language.formatConjunctionList( + contributionLinks.map(link => + link.slots({ + showContribution: slots.showContribution, + showIcons: slots.showIcons, + }))), + })), + })))); }, }; diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index bd7898b1..2fbe1188 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -24,189 +24,181 @@ export default { return data; }, - generate(data, { + slots: { + src: {type: 'string'}, + + path: { + validate: v => v.validateArrayItems(v.isString), + }, + + thumb: {type: 'string'}, + + reveal: {type: 'boolean', default: true}, + link: {type: 'boolean', default: false}, + lazy: {type: 'boolean', default: false}, + square: {type: 'boolean', default: false}, + + id: {type: 'string'}, + class: {type: 'string'}, + alt: {type: 'string'}, + width: {type: 'number'}, + height: {type: 'number'}, + + missingSourceContent: {type: 'html'}, + }, + + generate(data, slots, { getSizeOfImageFile, html, language, thumb, to, }) { - return html.template({ - annotation: 'image', - - slots: { - src: { - type: 'string', - }, - - path: { - validate: v => v.validateArrayItems(v.isString), - }, - - thumb: {type: 'string'}, - - reveal: {type: 'boolean', default: true}, - link: {type: 'boolean', default: false}, - lazy: {type: 'boolean', default: false}, - square: {type: 'boolean', default: false}, - - id: {type: 'string'}, - class: {type: 'string'}, - alt: {type: 'string'}, - width: {type: 'number'}, - height: {type: 'number'}, - - missingSourceContent: {type: 'html'}, - }, - - content(slots) { - let originalSrc; - - if (slots.src) { - originalSrc = slots.src; - } else if (!empty(slots.path)) { - originalSrc = to(...slots.path); - } else { - originalSrc = ''; - } - - const thumbSrc = - originalSrc && - (slots.thumb - ? thumb[slots.thumb](originalSrc) - : originalSrc); - - const willLink = typeof slots.link === 'string' || slots.link; - - const willReveal = - slots.reveal && - originalSrc && - !empty(data.contentWarnings); - - const willSquare = slots.square; - - const idOnImg = willLink ? null : slots.id; - const idOnLink = willLink ? slots.id : null; - const classOnImg = willLink ? null : slots.class; - const classOnLink = willLink ? slots.class : null; - - if (!originalSrc) { - return prepare( - html.tag('div', {class: 'image-text-area'}, - slots.missingSourceContent)); - } - - let fileSize = null; - if (willLink) { - const mediaRoot = to('media.root'); - if (originalSrc.startsWith(mediaRoot)) { - fileSize = - getSizeOfImageFile( - originalSrc - .slice(mediaRoot.length) - .replace(/^\//, '')); - } - } - - let reveal = null; - if (willReveal) { - reveal = [ - language.$('misc.contentWarnings', { - warnings: language.formatUnitList(data.contentWarnings), + let originalSrc; + + if (slots.src) { + originalSrc = slots.src; + } else if (!empty(slots.path)) { + originalSrc = to(...slots.path); + } else { + originalSrc = ''; + } + + const thumbSrc = + originalSrc && + (slots.thumb + ? thumb[slots.thumb](originalSrc) + : originalSrc); + + const willLink = typeof slots.link === 'string' || slots.link; + + const willReveal = + slots.reveal && + originalSrc && + !empty(data.contentWarnings); + + const willSquare = slots.square; + + const idOnImg = willLink ? null : slots.id; + const idOnLink = willLink ? slots.id : null; + const classOnImg = willLink ? null : slots.class; + const classOnLink = willLink ? slots.class : null; + + if (!originalSrc) { + return prepare( + html.tag('div', {class: 'image-text-area'}, + slots.missingSourceContent)); + } + + let fileSize = null; + if (willLink) { + const mediaRoot = to('media.root'); + if (originalSrc.startsWith(mediaRoot)) { + fileSize = + getSizeOfImageFile( + originalSrc + .slice(mediaRoot.length) + .replace(/^\//, '')); + } + } + + let reveal = null; + if (willReveal) { + reveal = [ + language.$('misc.contentWarnings', { + warnings: language.formatUnitList(data.contentWarnings), + }), + html.tag('br'), + html.tag('span', {class: 'reveal-interaction'}, + language.$('misc.contentWarnings.reveal')), + ]; + } + + const imgAttributes = { + id: idOnImg, + class: classOnImg, + alt: slots.alt, + width: slots.width, + height: slots.height, + 'data-original-size': fileSize, + }; + + const nonlazyHTML = + originalSrc && + prepare( + html.tag('img', { + ...imgAttributes, + src: thumbSrc, + })); + + if (slots.lazy) { + return html.tags([ + html.tag('noscript', nonlazyHTML), + prepare( + html.tag('img', + { + ...imgAttributes, + class: 'lazy', + 'data-original': thumbSrc, }), - html.tag('br'), - html.tag('span', {class: 'reveal-interaction'}, - language.$('misc.contentWarnings.reveal')), - ]; - } - - const imgAttributes = { - id: idOnImg, - class: classOnImg, - alt: slots.alt, - width: slots.width, - height: slots.height, - 'data-original-size': fileSize, - }; - - const nonlazyHTML = - originalSrc && - prepare( - html.tag('img', { - ...imgAttributes, - src: thumbSrc, - })); - - if (slots.lazy) { - return html.tags([ - html.tag('noscript', nonlazyHTML), - prepare( - html.tag('img', - { - ...imgAttributes, - class: 'lazy', - 'data-original': thumbSrc, - }), - true), + true), + ]); + } + + return nonlazyHTML; + + function prepare(content, hide = false) { + let wrapped = content; + + wrapped = + html.tag('div', {class: 'image-container'}, + html.tag('div', {class: 'image-inner-area'}, + wrapped)); + + if (willReveal) { + wrapped = + html.tag('div', {class: 'reveal'}, [ + wrapped, + html.tag('span', {class: 'reveal-text-container'}, + html.tag('span', {class: 'reveal-text'}, + reveal)), ]); - } - - return nonlazyHTML; - - function prepare(content, hide = false) { - let wrapped = content; - - wrapped = - html.tag('div', {class: 'image-container'}, - html.tag('div', {class: 'image-inner-area'}, - wrapped)); - - if (willReveal) { - wrapped = - html.tag('div', {class: 'reveal'}, [ - wrapped, - html.tag('span', {class: 'reveal-text-container'}, - html.tag('span', {class: 'reveal-text'}, - reveal)), - ]); - } - - if (willSquare) { - wrapped = - html.tag('div', - { - class: [ - 'square', - hide && !willLink && 'js-hide' - ], - }, - - html.tag('div', {class: 'square-content'}, - wrapped)); - } - - if (willLink) { - wrapped = html.tag('a', - { - id: idOnLink, - class: [ - 'box', - 'image-link', - hide && 'js-hide', - classOnLink, - ], - - href: - (typeof slots.link === 'string' - ? slots.link - : originalSrc), - }, - wrapped); - } - - return wrapped; - } - }, - }); - } + } + + if (willSquare) { + wrapped = + html.tag('div', + { + class: [ + 'square', + hide && !willLink && 'js-hide' + ], + }, + + html.tag('div', {class: 'square-content'}, + wrapped)); + } + + if (willLink) { + wrapped = html.tag('a', + { + id: idOnLink, + class: [ + 'box', + 'image-link', + hide && 'js-hide', + classOnLink, + ], + + href: + (typeof slots.link === 'string' + ? slots.link + : originalSrc), + }, + wrapped); + } + + return wrapped; + } + }, }; diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js index 1d0e2d6a..cc0cb353 100644 --- a/src/content/dependencies/linkContribution.js +++ b/src/content/dependencies/linkContribution.js @@ -27,48 +27,39 @@ export default { return {contribution}; }, - generate(data, relations, { - html, - language, - }) { - return html.template({ - annotation: 'linkContribution', - - slots: { - showContribution: {type: 'boolean', default: false}, - showIcons: {type: 'boolean', default: false}, - }, + slots: { + showContribution: {type: 'boolean', default: false}, + showIcons: {type: 'boolean', default: false}, + }, - content(slots) { - const hasContributionPart = !!(slots.showContribution && data.contribution); - const hasExternalPart = !!(slots.showIcons && !empty(relations.artistIcons)); + generate(data, relations, slots, {html, language}) { + const hasContributionPart = !!(slots.showContribution && data.contribution); + const hasExternalPart = !!(slots.showIcons && !empty(relations.artistIcons)); - const externalLinks = hasExternalPart && - html.tag('span', - {[html.noEdgeWhitespace]: true, class: 'icons'}, - language.formatUnitList(relations.artistIcons)); + const externalLinks = hasExternalPart && + html.tag('span', + {[html.noEdgeWhitespace]: true, class: 'icons'}, + language.formatUnitList(relations.artistIcons)); - return ( - (hasContributionPart - ? (hasExternalPart - ? language.$('misc.artistLink.withContribution.withExternalLinks', { - artist: relations.artistLink, - contrib: data.contribution, - links: externalLinks, - }) - : language.$('misc.artistLink.withContribution', { - artist: relations.artistLink, - contrib: data.contribution, - })) - : (hasExternalPart - ? language.$('misc.artistLink.withExternalLinks', { - artist: relations.artistLink, - links: externalLinks, - }) - : language.$('misc.artistLink', { - artist: relations.artistLink, - })))); - }, - }); + return ( + (hasContributionPart + ? (hasExternalPart + ? language.$('misc.artistLink.withContribution.withExternalLinks', { + artist: relations.artistLink, + contrib: data.contribution, + links: externalLinks, + }) + : language.$('misc.artistLink.withContribution', { + artist: relations.artistLink, + contrib: data.contribution, + })) + : (hasExternalPart + ? language.$('misc.artistLink.withExternalLinks', { + artist: relations.artistLink, + links: externalLinks, + }) + : language.$('misc.artistLink', { + artist: relations.artistLink, + })))); }, }; diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index 9109ab50..98e2c8b9 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -8,66 +8,60 @@ export default { 'to', ], - generate({ + slots: { + href: {type: 'string'}, + path: {validate: v => v.validateArrayItems(v.isString)}, + hash: {type: 'string'}, + + tooltip: {validate: v => v.isString}, + attributes: {validate: v => v.isAttributes}, + color: {validate: v => v.isColor}, + content: {type: 'html'}, + }, + + generate(slots, { appendIndexHTML, getColors, html, to, }) { - return html.template({ - annotation: 'linkTemplate', - - slots: { - href: {type: 'string'}, - path: {validate: v => v.validateArrayItems(v.isString)}, - hash: {type: 'string'}, - - tooltip: {validate: v => v.isString}, - attributes: {validate: v => v.isAttributes}, - color: {validate: v => v.isColor}, - content: {type: 'html'}, - }, - - content(slots) { - let href = slots.href; - let style; - let title; + let href = slots.href; + let style; + let title; - if (!href && !empty(slots.path)) { - href = to(...slots.path); - } + if (!href && !empty(slots.path)) { + href = to(...slots.path); + } - if (appendIndexHTML) { - if ( - /^(?!https?:\/\/).+\/$/.test(href) && - href.endsWith('/') - ) { - href += 'index.html'; - } - } + if (appendIndexHTML) { + if ( + /^(?!https?:\/\/).+\/$/.test(href) && + href.endsWith('/') + ) { + href += 'index.html'; + } + } - if (slots.hash) { - href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash; - } + if (slots.hash) { + href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash; + } - if (slots.color) { - const {primary, dim} = getColors(slots.color); - style = `--primary-color: ${primary}; --dim-color: ${dim}`; - } + if (slots.color) { + const {primary, dim} = getColors(slots.color); + style = `--primary-color: ${primary}; --dim-color: ${dim}`; + } - if (slots.tooltip) { - title = slots.tooltip; - } + if (slots.tooltip) { + title = slots.tooltip; + } - return html.tag('a', - { - ...slots.attributes ?? {}, - href, - style, - title, - }, - slots.content); + return html.tag('a', + { + ...slots.attributes ?? {}, + href, + style, + title, }, - }); + slots.content); }, } diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index bf4233fd..57404bae 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -180,7 +180,14 @@ export default { }; }, - generate(data, relations, {html, language}) { + slots: { + mode: { + validate: v => v.is('inline', 'multiline', 'lyrics'), + default: 'multiline', + }, + }, + + generate(data, relations, slots, {html, language}) { let linkIndex = 0; // This array contains only straight text and link nodes, which are directly @@ -233,93 +240,80 @@ export default { return getPlaceholder(node, data.content); }); - return html.template({ - annotation: `transformContent`, + // In inline mode, no further processing is needed! - slots: { - mode: { - validate: v => v.is('inline', 'multiline', 'lyrics'), - default: 'multiline', - }, - }, + if (slots.mode === 'inline') { + return html.tags(contentFromNodes.map(node => node.data)); + } - content(slots) { - // In inline mode, no further processing is needed! + // Multiline mode has a secondary processing stage where it's passed... + // through marked! Rolling your own Markdown only gets you so far :D - if (slots.mode === 'inline') { - return html.tags(contentFromNodes.map(node => node.data)); - } - - // Multiline mode has a secondary processing stage where it's passed... - // through marked! Rolling your own Markdown only gets you so far :D - - const markedOptions = { - headerIds: false, - mangle: false, - }; - - // This is separated into its own function just since we're gonna reuse - // it in a minute if everything goes to heck in lyrics mode. - const transformMultiline = () => { - const markedInput = - contentFromNodes - .map(node => { - if (node.type === 'text') { - return node.data; - } else { - return node.data.toString(); - } - }) - .join('') - - // Compress multiple line breaks into single line breaks. - .replace(/\n{2,}/g, '\n') - // Expand line breaks which don't follow a list. - .replace(/(?<!^ *-.*)\n+/gm, '\n\n') - // Expand line breaks which are at the end of a list. - .replace(/(?<=^ *-.*)\n+(?!^ *-)/gm, '\n\n'); - - return marked.parse(markedInput, markedOptions); - } - - if (slots.mode === 'multiline') { - // Unfortunately, we kind of have to be super evil here and stringify - // the links, or else parse marked's output into html tags, which is - // very out of scope at the moment. - return transformMultiline(); - } - - // Lyrics mode goes through marked too, but line breaks are processed - // differently. Instead of having each line get its own paragraph, - // "adjacent" lines are joined together (with blank lines separating - // each verse/paragraph). - - if (slots.mode === 'lyrics') { - // If it looks like old data, using <br> instead of bunched together - // lines... then oh god... just use transformMultiline. Perishes. - if ( - contentFromNodes.some(node => - node.type === 'text' && - node.data.includes('<br')) - ) { - return transformMultiline(); - } + const markedOptions = { + headerIds: false, + mangle: false, + }; - // Lyrics mode is also evil for the same stringifying reasons as - // multiline. - return marked.parse( - contentFromNodes - .map(node => { - if (node.type === 'text') { - return node.data.replace(/\b\n\b/g, '<br>\n'); - } else { - return node.data.toString(); - } - }) - .join(''), - markedOptions); - } - }, - }); + // This is separated into its own function just since we're gonna reuse + // it in a minute if everything goes to heck in lyrics mode. + const transformMultiline = () => { + const markedInput = + contentFromNodes + .map(node => { + if (node.type === 'text') { + return node.data; + } else { + return node.data.toString(); + } + }) + .join('') + + // Compress multiple line breaks into single line breaks. + .replace(/\n{2,}/g, '\n') + // Expand line breaks which don't follow a list. + .replace(/(?<!^ *-.*)\n+/gm, '\n\n') + // Expand line breaks which are at the end of a list. + .replace(/(?<=^ *-.*)\n+(?!^ *-)/gm, '\n\n'); + + return marked.parse(markedInput, markedOptions); + } + + if (slots.mode === 'multiline') { + // Unfortunately, we kind of have to be super evil here and stringify + // the links, or else parse marked's output into html tags, which is + // very out of scope at the moment. + return transformMultiline(); + } + + // Lyrics mode goes through marked too, but line breaks are processed + // differently. Instead of having each line get its own paragraph, + // "adjacent" lines are joined together (with blank lines separating + // each verse/paragraph). + + if (slots.mode === 'lyrics') { + // If it looks like old data, using <br> instead of bunched together + // lines... then oh god... just use transformMultiline. Perishes. + if ( + contentFromNodes.some(node => + node.type === 'text' && + node.data.includes('<br')) + ) { + return transformMultiline(); + } + + // Lyrics mode is also evil for the same stringifying reasons as + // multiline. + return marked.parse( + contentFromNodes + .map(node => { + if (node.type === 'text') { + return node.data.replace(/\b\n\b/g, '<br>\n'); + } else { + return node.data.toString(); + } + }) + .join(''), + markedOptions); + } }, } |