From cf08893d48db6f8082a176f54d0d92cb82716b3a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 18:50:59 -0400 Subject: external-links: general support for page-contextual formatting --- src/data/things/language.js | 16 ++- src/strings-default.yaml | 25 +++-- src/util/external-links.js | 263 +++++++++++++++++++++++++++++++++----------- 3 files changed, 224 insertions(+), 80 deletions(-) (limited to 'src') diff --git a/src/data/things/language.js b/src/data/things/language.js index 185488e2..f83b4218 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -3,6 +3,7 @@ import {Tag} from '#html'; import { getExternalLinkStringsFromDescriptors, + isExternalLinkContext, isExternalLinkSpec, isExternalLinkStyle, } from '#external-links'; @@ -312,17 +313,22 @@ export class Language extends Thing { : duration; } - formatExternalLink(url, {style = 'normal'} = {}) { + formatExternalLink(url, { + style = 'normal', + context = 'generic', + } = {}) { if (!this.externalLinkSpec) { throw new TypeError(`externalLinkSpec unavailable`); } - if (style !== 'all') { - isExternalLinkStyle(style); - } + if (style !== 'all') isExternalLinkStyle(style); + isExternalLinkContext(context); const results = - getExternalLinkStringsFromDescriptors(url, this.externalLinkSpec, this); + getExternalLinkStringsFromDescriptors(url, this.externalLinkSpec, { + language: this, + context, + }); if (style === 'all') { return results; diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 9fdf0182..698e3c9f 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -413,8 +413,17 @@ misc: "{PLACE} ({HANDLE})" local: "Wiki Archive (local upload)" + + bandcamp: "Bandcamp" + + bgreco: + _: "bgreco.net" + flash: "bgreco.net (high quality audio)" + deviantart: "DeviantArt" + homestuck: "Homestuck" instagram: "Instagram" + mastodon: "Mastodon" newgrounds: "Newgrounds" patreon: "Patreon" poetryFoundation: "Poetry Foundation" @@ -423,20 +432,20 @@ misc: tumblr: "Tumblr" twitter: "Twitter" wikipedia: "Wikipedia" - bandcamp: "Bandcamp" - mastodon: "Mastodon" youtube: _: "YouTube" + flash: "YouTube (on any device)" playlist: "YouTube (playlist)" fullAlbum: "YouTube (full album)" - flash: - bgreco: "{LINK} (HQ Audio)" - youtube: "{LINK} (on any device)" - homestuck: - page: "{LINK} (page {PAGE})" - secret: "{LINK} (secret page)" + # flashLink: + # Flashes can be positioned by page! They're accented with this + # information, if available. + + flashLink: + page: "{LINK} (page {PAGE})" + secret: "{LINK} (secret page)" # missingImage: # Fallback text displayed in an image when it's sourced to a file diff --git a/src/util/external-links.js b/src/util/external-links.js index 7a34fa9e..07f46bd3 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -18,31 +18,48 @@ export const externalLinkStyles = [ export const isExternalLinkStyle = is(...externalLinkStyles); +export const externalLinkContexts = [ + 'album', + 'artist', + 'flash', + 'generic', + 'group', + 'track', +]; + +export const isExternalLinkContext = is(...externalLinkContexts); + // This might need to be adjusted for YAML importing... -const isExternalLinkSpecRegex = +const isRegExp = validateInstanceOf(RegExp); export const isExternalLinkHandleSpec = validateProperties({ prefix: optional(isStringNonEmpty), - url: optional(isExternalLinkSpecRegex), - - // TODO: Don't allow specifying both of these (they're aliases) - domain: optional(isExternalLinkSpecRegex), - hostname: optional(isExternalLinkSpecRegex), - - // TODO: Don't allow specifying both of these (they're aliases) - path: optional(isExternalLinkSpecRegex), - pathname: optional(isExternalLinkSpecRegex), + url: optional(isRegExp), + domain: optional(isRegExp), + pathname: optional(isRegExp), }); export const isExternalLinkSpec = validateArrayItems( validateProperties({ - // TODO: Don't allow providing both of these, and require providing one - matchDomain: optional(isStringNonEmpty), - matchDomains: optional(validateArrayItems(isStringNonEmpty)), + match: validateProperties({ + // TODO: Don't allow providing both of these, and require providing one + domain: optional(isStringNonEmpty), + domains: optional(validateArrayItems(isStringNonEmpty)), + + // TODO: Don't allow providing both of these + pathname: optional(isRegExp), + pathnames: optional(validateArrayItems(isRegExp)), + + // TODO: Don't allow providing both of these + query: optional(isRegExp), + queries: optional(validateArrayItems(isRegExp)), + + context: optional(isExternalLinkContext), + }), string: isStringNonEmpty, @@ -64,27 +81,84 @@ export const fallbackDescriptor = { // TODO: Define all this stuff in data as YAML! export const externalLinkSpec = [ + // Special handling for album links + { - matchDomain: 'hsmusic.wiki', + match: { + context: 'album', + domain: 'youtube.com', + pathname: /^playlist/, + }, - string: 'local', + string: 'youtube.playlist', + icon: 'youtube', + }, - icon: 'globe', + { + match: { + context: 'album', + domain: 'youtube.com', + pathname: /^watch/, + }, + + string: 'youtube.fullAlbum', + icon: 'youtube', }, { - matchDomain: 'bandcamp.com', + match: { + context: 'album', + domain: 'youtu.be', + }, - string: 'bandcamp', + string: 'youtube.fullAlbum', + icon: 'youtube', + }, + + // Special handling for artist links + + { + match: { + context: 'artist', + domains: ['youtube.com', 'youtu.be'], + }, + + string: 'youtube', + icon: 'youtube', compact: 'handle', - icon: 'bandcamp', - handle: {domain: /^[^.]*/}, + handle: { + pathname: /^(@.*?)\/?$/, + }, + }, + + // Special handling for flash links + + { + match: { + context: 'flash', + domain: 'bgreco.net', + }, + + string: 'bgreco.flash', + icon: 'external', + }, + + { + match: { + context: 'flash', + domains: ['youtube.com', 'youtu.be'], + }, + + string: 'youtube.flash', + icon: 'youtube', }, + // Generic domains, sorted alphabetically (by string) + { - matchDomains: ['bc.s3m.us', 'music.solatrux.com'], + match: {domains: ['bc.s3m.us', 'music.solatrux.com']}, icon: 'bandcamp', string: 'bandcamp', @@ -94,7 +168,47 @@ export const externalLinkSpec = [ }, { - matchDomains: ['types.pl'], + match: {domain: 'bandcamp.com'}, + + string: 'bandcamp', + + compact: 'handle', + icon: 'bandcamp', + + handle: {domain: /^[^.]*/}, + }, + + { + match: {domain: 'deviantart.com'}, + + string: 'deviantart', + icon: 'deviantart', + }, + + { + match: {domain: 'instagram.com'}, + + string: 'instagram', + icon: 'instagram', + }, + + { + match: {domain: 'homestuck.com'}, + + string: 'homestuck', + icon: 'globe', // The horror! + }, + + { + match: {domain: 'hsmusic.wiki'}, + + string: 'local', + + icon: 'globe', + }, + + { + match: {domains: ['types.pl']}, icon: 'mastodon', string: 'mastodon', @@ -103,23 +217,17 @@ export const externalLinkSpec = [ }, { - matchDomains: ['youtube.com', 'youtu.be'], - - icon: 'youtube', - string: 'youtube', - - compact: 'handle', + match: {domain: 'newgrounds.com'}, - handle: { - pathname: /^(@.*?)\/?$/, - }, + string: 'newgrounds', + icon: 'newgrounds', }, { - matchDomain: 'soundcloud.com', + match: {domain: 'soundcloud.com'}, - icon: 'soundcloud', string: 'soundcloud', + icon: 'soundcloud', compact: 'handle', @@ -127,10 +235,10 @@ export const externalLinkSpec = [ }, { - matchDomain: 'tumblr.com', + match: {domain: 'tumblr.com'}, - icon: 'tumblr', string: 'tumblr', + icon: 'tumblr', compact: 'handle', @@ -138,62 +246,80 @@ export const externalLinkSpec = [ }, { - matchDomain: 'twitter.com', + match: {domain: 'twitter.com'}, - icon: 'twitter', string: 'twitter', + icon: 'twitter', compact: 'handle', handle: { prefix: '@', - pathname: /^@?.*\/?$/, + pathname: /^@?([a-zA-Z0-9_]*)\/?$/, }, }, { - matchDomain: 'deviantart.com', + match: {domains: ['youtube.com', 'youtu.be']}, - icon: 'deviantart', - string: 'deviantart', + string: 'youtube', + icon: 'youtube', }, +]; - { - matchDomain: 'instagram.com', - - icon: 'instagram', - string: 'instagram', - }, +function urlParts(url) { + const { + hostname: domain, + pathname, + search: query, + } = new URL(url); - { - matchDomain: 'newgrounds.com', + return {domain, pathname, query}; +} - icon: 'newgrounds', - string: 'newgrounds', - }, -]; +export function getMatchingDescriptorsForExternalLink(url, descriptors, { + context = 'generic', +} = {}) { + const {domain, pathname, query} = urlParts(url); -export function getMatchingDescriptorsForExternalLink(url, descriptors) { - const {hostname: domain} = new URL(url); - const compare = d => domain.includes(d); + const compareDomain = string => domain.includes(string); + const comparePathname = regex => regex.test(pathname.slice(1)); + const compareQuery = regex => regex.test(query.slice(1)); const matchingDescriptors = - descriptors.filter(spec => { - if (spec.matchDomain && compare(spec.matchDomain)) return true; - if (spec.matchDomains && spec.matchDomains.some(compare)) return true; - return false; - }); + descriptors + .filter(({match}) => { + if (match.domain) return compareDomain(match.domain); + if (match.domains) return match.domains.some(compareDomain); + return false; + }) + .filter(({match}) => { + if (match.context) return context === match.context; + return true; + }) + .filter(({match}) => { + if (match.pathname) return comparePathname(match.pathname); + if (match.pathnames) return match.pathnames.some(comparePathname); + return true; + }) + .filter(({match}) => { + if (match.query) return compareQuery(match.query); + if (match.queries) return match.quieries.some(compareQuery); + return true; + }); return [...matchingDescriptors, fallbackDescriptor]; } -export function getExternalLinkStringsFromDescriptor(url, descriptor, language) { +export function getExternalLinkStringsFromDescriptor(url, descriptor, { + language, +}) { const prefix = 'misc.external'; const results = Object.fromEntries(externalLinkStyles.map(style => [style, null])); - const {hostname: domain, pathname} = new URL(url); + const {domain, pathname, query} = urlParts(url); const place = language.$(prefix, descriptor.string); @@ -240,7 +366,7 @@ export function getExternalLinkStringsFromDescriptor(url, descriptor, language) case 'path': case 'pathname': - tests.push(pathname.slice(1)); + tests.push(pathname.slice(1) + query); break; default: @@ -277,7 +403,10 @@ export function getExternalLinkStringsFromDescriptor(url, descriptor, language) return results; } -export function getExternalLinkStringsFromDescriptors(url, descriptors, language) { +export function getExternalLinkStringsFromDescriptors(url, descriptors, { + language, + context = 'generic', +}) { const results = Object.fromEntries(externalLinkStyles.map(style => [style, null])); @@ -285,11 +414,11 @@ export function getExternalLinkStringsFromDescriptors(url, descriptors, language new Set(Object.keys(results)); const matchingDescriptors = - getMatchingDescriptorsForExternalLink(url, descriptors); + getMatchingDescriptorsForExternalLink(url, descriptors, {context}); for (const descriptor of matchingDescriptors) { const descriptorResults = - getExternalLinkStringsFromDescriptor(url, descriptor, language); + getExternalLinkStringsFromDescriptor(url, descriptor, {language}); const descriptorKeys = new Set( -- cgit 1.3.0-6-gf8a5