diff options
| -rw-r--r-- | src/content/dependencies/generateAlbumReleaseInfo.js | 32 | ||||
| -rw-r--r-- | src/content/dependencies/generatePageLayout.js | 6 | ||||
| -rw-r--r-- | src/content/dependencies/generateReleaseInfoListenLine.js | 150 | ||||
| -rw-r--r-- | src/content/dependencies/generateTrackInfoPage.js | 12 | ||||
| -rw-r--r-- | src/content/dependencies/generateTrackReleaseInfo.js | 31 | ||||
| -rw-r--r-- | src/content/dependencies/listTracksWithLyrics.js | 2 | ||||
| -rw-r--r-- | src/data/yaml.js | 11 | ||||
| -rw-r--r-- | src/external-links.js | 27 | ||||
| -rw-r--r-- | src/static/css/site.css | 24 | ||||
| -rw-r--r-- | src/static/js/client/index.js | 2 | ||||
| -rw-r--r-- | src/static/js/client/lyrics-switcher.js | 70 | ||||
| -rw-r--r-- | src/static/js/rectangles.js | 42 | ||||
| -rw-r--r-- | src/strings-default.yaml | 7 | ||||
| -rw-r--r-- | src/urls-default.yaml | 2 | ||||
| -rw-r--r-- | src/write/build-modes/static-build.js | 58 |
15 files changed, 336 insertions, 140 deletions
diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js index 0abb412c..17390c9b 100644 --- a/src/content/dependencies/generateAlbumReleaseInfo.js +++ b/src/content/dependencies/generateAlbumReleaseInfo.js @@ -3,7 +3,7 @@ import {accumulateSum, empty} from '#sugar'; export default { contentDependencies: [ 'generateReleaseInfoContributionsLine', - 'linkExternal', + 'generateReleaseInfoListenLine', ], extraDependencies: ['html', 'language'], @@ -20,9 +20,8 @@ export default { relations.bannerArtistContributionsLine = relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs); - relations.externalLinks = - album.urls.map(url => - relation('linkExternal', url)); + relations.listenLine = + relation('generateReleaseInfoListenLine', album); return relations; }, @@ -87,21 +86,16 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, - language.$(capsule, 'listenOn', { - [language.onlyIfOptions]: ['links'], - - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => - link.slot('context', [ - 'album', - (data.numTracks === 0 - ? 'albumNoTracks' - : data.numTracks === 1 - ? 'albumOneTrack' - : 'albumMultipleTracks'), - ]))), + relations.listenLine.slots({ + context: [ + 'album', + + (data.numTracks === 0 + ? 'albumNoTracks' + : data.numTracks === 1 + ? 'albumOneTrack' + : 'albumMultipleTracks'), + ], })), ])), }; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 070c7c82..0acf401c 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -583,6 +583,11 @@ export default { ` background-image: url("${to('media.path', 'bg.jpg')}");\n` + `}`); + const goshFrigginDarnitStyleRule = + `.image-media-link::after {\n` + + ` mask-image: url("${to('staticMisc.path', 'image.svg')}");\n` + + `}`; + const numWallpaperParts = html.resolve(slots.styleRules, {normalize: 'string'}) .match(/\.wallpaper-part:nth-child/g) @@ -733,6 +738,7 @@ export default { .slot('color', slots.color ?? data.wikiColor), fallbackBackgroundStyleRule, + goshFrigginDarnitStyleRule, slots.styleRules, ]), diff --git a/src/content/dependencies/generateReleaseInfoListenLine.js b/src/content/dependencies/generateReleaseInfoListenLine.js new file mode 100644 index 00000000..f2a6dd29 --- /dev/null +++ b/src/content/dependencies/generateReleaseInfoListenLine.js @@ -0,0 +1,150 @@ +import {isExternalLinkContext} from '#external-links'; +import {empty, stitchArrays, unique} from '#sugar'; + +function getReleaseContext(urlString, { + _artistURLs, + albumArtistURLs, +}) { + const composerBandcampDomains = + albumArtistURLs + .filter(url => url.hostname.endsWith('.bandcamp.com')) + .map(url => url.hostname); + + const url = new URL(urlString); + + if (url.hostname === 'homestuck.bandcamp.com') { + return 'officialRelease'; + } + + if (composerBandcampDomains.includes(url.hostname)) { + return 'composerRelease'; + } + + return null; +} + +export default { + contentDependencies: ['linkExternal'], + extraDependencies: ['html', 'language'], + + query(thing) { + const query = {}; + + query.album = + (thing.album + ? thing.album + : thing); + + query.artists = + thing.artistContribs + .map(contrib => contrib.artist); + + query.artistGroups = + query.artists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + query.albumArtists = + query.album.artistContribs + .map(contrib => contrib.artist); + + query.albumArtistGroups = + query.albumArtists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + return query; + }, + + relations: (relation, _query, thing) => ({ + links: + thing.urls.map(url => relation('linkExternal', url)), + }), + + data(query, thing) { + const data = {}; + + data.name = thing.name; + + const artistURLs = + unique([ + ...query.artists.flatMap(artist => artist.urls), + ...query.artistGroups.flatMap(group => group.urls), + ]).map(url => new URL(url)); + + const albumArtistURLs = + unique([ + ...query.albumArtists.flatMap(artist => artist.urls), + ...query.albumArtistGroups.flatMap(group => group.urls), + ]).map(url => new URL(url)); + + const boundGetReleaseContext = urlString => + getReleaseContext(urlString, { + artistURLs, + albumArtistURLs, + }); + + let releaseContexts = + thing.urls.map(boundGetReleaseContext); + + const albumReleaseContexts = + query.album.urls.map(boundGetReleaseContext); + + const presentReleaseContexts = + unique(releaseContexts.filter(Boolean)); + + const presentAlbumReleaseContexts = + unique(albumReleaseContexts.filter(Boolean)); + + if ( + presentReleaseContexts.length <= 1 && + presentAlbumReleaseContexts.length <= 1 + ) { + releaseContexts = + thing.urls.map(() => null); + } + + data.releaseContexts = releaseContexts; + + return data; + }, + + slots: { + visibleWithoutLinks: { + type: 'boolean', + default: false, + }, + + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('releaseInfo.listenOn', capsule => + (empty(relations.links) && slots.visibleWithoutLinks + ? language.$(capsule, 'noLinks', { + name: + html.tag('i', data.name), + }) + + : language.$('releaseInfo.listenOn', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatDisjunctionList( + stitchArrays({ + link: relations.links, + releaseContext: data.releaseContexts, + }).map(({link, releaseContext}) => + link.slot('context', [ + ... + (Array.isArray(slots.context) + ? slots.context + : [slots.context]), + + releaseContext, + ]))), + }))), +}; diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index ca6f82b9..11d179ad 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -311,18 +311,6 @@ export default { relations.lyricsSection, - // html.tags([ - // relations.contentHeading.clone() - // .slots({ - // attributes: {id: 'lyrics'}, - // title: language.$('releaseInfo.lyrics'), - // }), - - // html.tag('blockquote', - // {[html.onlyIfContent]: true}, - // relations.lyrics.slot('mode', 'lyrics')), - // ]), - html.tags([ relations.contentHeading.clone() .slots({ diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js index 54e462c7..3298dcc4 100644 --- a/src/content/dependencies/generateTrackReleaseInfo.js +++ b/src/content/dependencies/generateTrackReleaseInfo.js @@ -1,9 +1,7 @@ -import {empty} from '#sugar'; - export default { contentDependencies: [ 'generateReleaseInfoContributionsLine', - 'linkExternal', + 'generateReleaseInfoListenLine', ], extraDependencies: ['html', 'language'], @@ -11,14 +9,11 @@ export default { relations(relation, track) { const relations = {}; - relations.artistContributionLinks = + relations.artistContributionsLine = relation('generateReleaseInfoContributionsLine', track.artistContribs); - if (!empty(track.urls)) { - relations.externalLinks = - track.urls.map(url => - relation('linkExternal', url)); - } + relations.listenLine = + relation('generateReleaseInfoListenLine', track); return relations; }, @@ -48,7 +43,7 @@ export default { {[html.joinChildren]: html.tag('br')}, [ - relations.artistContributionLinks.slots({ + relations.artistContributionsLine.slots({ stringKey: capsule + '.by', featuringStringKey: capsule + '.by.featuring', chronologyKind: 'track', @@ -66,17 +61,9 @@ export default { ]), html.tag('p', - language.encapsulate(capsule, 'listenOn', capsule => - (relations.externalLinks - ? language.$(capsule, { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => link.slot('context', 'track'))), - }) - : language.$(capsule, 'noLinks', { - name: - html.tag('i', data.name), - })))), + relations.listenLine.slots({ + visibleWithoutLinks: true, + context: ['track'], + })), ])), }; diff --git a/src/content/dependencies/listTracksWithLyrics.js b/src/content/dependencies/listTracksWithLyrics.js index a13a76f0..e6ab9d7d 100644 --- a/src/content/dependencies/listTracksWithLyrics.js +++ b/src/content/dependencies/listTracksWithLyrics.js @@ -2,7 +2,7 @@ export default { contentDependencies: ['listTracksWithExtra'], relations: (relation, spec) => - ({page: relation('listTracksWithExtra', spec, 'lyrics', 'truthy')}), + ({page: relation('listTracksWithExtra', spec, 'lyrics', 'array')}), generate: (relations) => relations.page, diff --git a/src/data/yaml.js b/src/data/yaml.js index 50317238..af1d5740 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1781,14 +1781,16 @@ export function flattenThingLayoutToDocumentOrder(layout) { } export function* splitDocumentsInYAMLSourceText(sourceText) { - const dividerRegex = /^-{3,}\n?/gm; + // Not multiline! + const dividerRegex = /(?:\r\n|\n|^)-{3,}(?:\r\n|\n|$)/g; + let previousDivider = ''; while (true) { const {lastIndex} = dividerRegex; const match = dividerRegex.exec(sourceText); if (match) { - const nextDivider = match[0].trim(); + const nextDivider = match[0]; yield { previousDivider, @@ -1799,11 +1801,12 @@ export function* splitDocumentsInYAMLSourceText(sourceText) { previousDivider = nextDivider; } else { const nextDivider = ''; + const lineBreak = previousDivider.match(/\r?\n/)?.[0] ?? ''; yield { previousDivider, nextDivider, - text: sourceText.slice(lastIndex).replace(/(?<!\n)$/, '\n'), + text: sourceText.slice(lastIndex).replace(/(?<!\n)$/, lineBreak), }; return; @@ -1829,7 +1832,7 @@ export function recombineDocumentsIntoYAMLSourceText(documents) { for (const document of documents) { if (sourceText) { - sourceText += divider + '\n'; + sourceText += divider; } sourceText += document.text; diff --git a/src/external-links.js b/src/external-links.js index 1055a391..daf57b41 100644 --- a/src/external-links.js +++ b/src/external-links.js @@ -32,6 +32,9 @@ export const externalLinkContexts = [ 'generic', 'group', 'track', + + 'composerRelease', + 'officialRelease', ]; export const isExternalLinkContext = @@ -257,6 +260,30 @@ export const externalLinkSpec = [ }, { + match: { + domain: '.bandcamp.com', + context: 'composerRelease', + }, + + platform: 'bandcamp.composerRelease', + handle: {domain: /^[^.]+/}, + + icon: 'bandcamp', + }, + + { + match: { + domain: '.bandcamp.com', + context: 'officialRelease', + }, + + platform: 'bandcamp.officialRelease', + handle: {domain: /^[^.]+/}, + + icon: 'bandcamp', + }, + + { match: {domain: '.bandcamp.com'}, platform: 'bandcamp', diff --git a/src/static/css/site.css b/src/static/css/site.css index 25d9ce47..8c53f877 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -931,7 +931,11 @@ a .normal-content { background-color: var(--primary-color); - mask-image: url(/static-4p1/misc/image.svg); + /* mask-image is set in content JavaScript, + * because we can't identify the correct nor + * absolute path to the file from CSS. + */ + mask-repeat: no-repeat; mask-position: calc(100% - 2px); vertical-align: text-bottom; @@ -1119,7 +1123,7 @@ a .normal-content { font-size: 0.9rem; } -li:not(:first-child:last-child) .tooltip, +li:not(:first-child:last-child) .tooltip:where(:not(.cover-artwork .tooltip)), .offset-tooltips > :not(:first-child:last-child) .tooltip { left: 14px; } @@ -1161,7 +1165,7 @@ li:not(:first-child:last-child) .tooltip, .thing-name-tooltip, .wiki-edits-tooltip { padding: 3px 4px 2px 2px; - left: -6px !important; + left: -6px; } .thing-name-tooltip .tooltip-content, @@ -1169,11 +1173,15 @@ li:not(:first-child:last-child) .tooltip, font-size: 0.85em; } -/* Terrifying? - * https://stackoverflow.com/a/64424759/4633828 - */ -.thing-name-tooltip { margin-right: -120px; } -.wiki-edits-tooltip { margin-right: -200px; } +.thing-name-tooltip .tooltip-content { + width: max-content; + max-width: 120px; +} + +.wiki-edits-tooltip .tooltip-content { + width: max-content; + max-width: 200px; +} .contribution-tooltip .tooltip-content { padding: 6px 2px 2px 2px; diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index b2343f07..81ea3415 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -15,7 +15,6 @@ import * as hoverableTooltipModule from './hoverable-tooltip.js'; import * as imageOverlayModule from './image-overlay.js'; import * as intrapageDotSwitcherModule from './intrapage-dot-switcher.js'; import * as liveMousePositionModule from './live-mouse-position.js'; -import * as lyricsSwitcherModule from './lyrics-switcher.js'; import * as quickDescriptionModule from './quick-description.js'; import * as scriptedLinkModule from './scripted-link.js'; import * as sidebarSearchModule from './sidebar-search.js'; @@ -38,7 +37,6 @@ export const modules = [ imageOverlayModule, intrapageDotSwitcherModule, liveMousePositionModule, - lyricsSwitcherModule, quickDescriptionModule, scriptedLinkModule, sidebarSearchModule, diff --git a/src/static/js/client/lyrics-switcher.js b/src/static/js/client/lyrics-switcher.js deleted file mode 100644 index b350ea50..00000000 --- a/src/static/js/client/lyrics-switcher.js +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-env browser */ - -import {stitchArrays} from '../../shared-util/sugar.js'; - -import {cssProp} from '../client-util.js'; - -export const info = { - id: 'lyricsSwitcherInfo', - - entries: null, - switchLinks: null, - currentLinks: null, -}; - -export function getPageReferences() { - const content = document.getElementById('content'); - - if (!content) return; - - const switcher = content.querySelector('.lyrics-switcher'); - - if (!switcher) return; - - info.entries = - Array.from(content.querySelectorAll('.lyrics-entry')); - - info.currentLinks = - Array.from(switcher.querySelectorAll('a.current')); - - info.switchLinks = - Array.from(switcher.querySelectorAll('a:not(.current)')); -} - -export function addPageListeners() { - if (!info.switchLinks) return; - - for (const {switchLink, entry} of stitchArrays({ - switchLink: info.switchLinks, - entry: info.entries, - })) { - switchLink.addEventListener('click', domEvent => { - domEvent.preventDefault(); - showLyricsEntry(entry); - }); - } -} - -function showLyricsEntry(entry) { - const entryToShow = entry; - - stitchArrays({ - entry: info.entries, - currentLink: info.currentLinks, - switchLink: info.switchLinks, - }).forEach(({ - entry, - currentLink, - switchLink, - }) => { - if (entry === entryToShow) { - cssProp(entry, 'display', null); - cssProp(currentLink, 'display', null); - cssProp(switchLink, 'display', 'none'); - } else { - cssProp(entry, 'display', 'none'); - cssProp(currentLink, 'display', 'none'); - cssProp(switchLink, 'display', null); - } - }); -} diff --git a/src/static/js/rectangles.js b/src/static/js/rectangles.js index cdab2cb8..b00ed98e 100644 --- a/src/static/js/rectangles.js +++ b/src/static/js/rectangles.js @@ -510,4 +510,46 @@ export class WikiRect extends DOMRect { height: this.height, }); } + + // Other utilities + + #display = null; + + display() { + if (!this.#display) { + this.#display = document.createElement('div'); + document.body.appendChild(this.#display); + } + + Object.assign(this.#display.style, { + position: 'fixed', + background: '#000c', + border: '3px solid var(--primary-color)', + borderRadius: '4px', + top: this.top + 'px', + left: this.left + 'px', + width: this.width + 'px', + height: this.height + 'px', + pointerEvents: 'none', + }); + + let i = 0; + const int = setInterval(() => { + i++; + if (i >= 3) clearInterval(int); + if (!this.#display) return; + + this.#display.style.display = 'none'; + setTimeout(() => { + this.#display.style.display = ''; + }, 200); + }, 600); + } + + hide() { + if (this.#display) { + this.#display.remove(); + this.#display = null; + } + } } diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 7a40bd0d..6b68cdb6 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -647,7 +647,12 @@ misc: amazonMusic: "Amazon Music" appleMusic: "Apple Music" artstation: "ArtStation" - bandcamp: "Bandcamp" + + bandcamp: + _: "Bandcamp" + + composerRelease: "Bandcamp (composer's release)" + officialRelease: "Bandcamp (official release)" bgreco: _: "bgreco.net" diff --git a/src/urls-default.yaml b/src/urls-default.yaml index c3bf89eb..7fcccae8 100644 --- a/src/urls-default.yaml +++ b/src/urls-default.yaml @@ -11,7 +11,7 @@ yamlAliases: # part of a build. This is so that multiple builds of a wiki can coexist # served from the same server / file system root: older builds' HTML files # refer to earlier values of STATIC_VERSION, avoiding name collisions. - - &staticVersion 4p1 + - &staticVersion 5r2 data: prefix: 'data/' diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 2baed816..3b69b066 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -4,6 +4,7 @@ import { copyFile, cp, mkdir, + readFile, stat, symlink, writeFile, @@ -95,6 +96,11 @@ export function getCLIOptions() { type: 'value', }, + 'paths': { + help: `Skip rest and build only pages matching paths in this text file`, + type: 'value', + }, + // NOT for neatly ena8ling or disa8ling specific features of the site! // This is only in charge of what general groups of files to write. // They're here to make development quicker when you're only working @@ -128,6 +134,7 @@ export async function go({ const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT; const appendIndexHTML = cliOptions['append-index-html'] ?? false; const writeOneLanguage = cliOptions['lang'] ?? null; + const pathsFromFile = cliOptions['paths'] ?? null; if (!outputPath) { logError`Expected ${'--out-path'} option or ${'HSMUSIC_OUT'} to be set`; @@ -147,6 +154,36 @@ export async function go({ logInfo`Writing all languages.`; } + let filterPaths = null; + if (pathsFromFile) { + let pathsText; + try { + pathsText = await readFile(pathsFromFile, 'utf8'); + } catch (error) { + logError`Failed to read file specified in ${'--paths'}:`; + logError`${error.code}: ${pathsFromFile}`; + return false; + } + + filterPaths = pathsText.split('\n').filter(Boolean); + + if (empty(filterPaths)) { + logWarn`Specified to build only paths in file ${'--paths'}:`; + logWarn`${pathsFromFile}`; + logWarn`But this file is totally empty...`; + } + + if (filterPaths.some(path => !path.startsWith('/'))) { + logError`All lines in ${'--paths'} file should start with slash ('${'/'}')`; + logError`These lines don't:`; + console.error(filterPaths.filter(path => !path.startsWith('/')).join('\n')); + logError`Please check file contents, or specified path, and try again.`; + return false; + } + + logInfo`Writing ${filterPaths.length} paths specified in: ${pathsFromFile} (${'--paths'})`; + } + const selectedPageFlags = Object.keys(cliOptions) .filter(key => pageFlags.includes(key)); @@ -225,6 +262,27 @@ export async function go({ // TODO: Validate each pathsForTargets entry } + if (!empty(filterPaths)) { + paths = + paths.filter(path => + filterPaths.includes( + (path.type === 'page' + ? '/' + + getPagePathname({ + baseDirectory: '', + pagePath: path.path, + urls, + }) + : path.type === 'redirect' + ? '/' + + getPagePathname({ + baseDirectory: '', + pagePath: path.fromPath, + urls, + }) + : null))); + } + paths = paths.filter(path => path.condition?.() ?? true); |