From 8b81a3aa4e266548ef2c8083391f6bb859915133 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 Nov 2023 07:22:40 -0400 Subject: sugar: fix async decorateError not providing calling arguments --- src/util/sugar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/util') diff --git a/src/util/sugar.js b/src/util/sugar.js index 9646be37..3f0eb2ea 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -685,7 +685,7 @@ export function asyncAdaptiveDecorateError(fn, callback) { try { return await fn(...args); } catch (caughtError) { - throw callback(caughtError); + throw callback(caughtError, ...args); } }; -- cgit 1.3.0-6-gf8a5 From 8f17782a5f2adbafd031b269195879eb7f79e05f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 11:16:48 -0400 Subject: data, content: extract external link parsing into nicer interface --- src/util/external-links.js | 308 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 src/util/external-links.js (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js new file mode 100644 index 00000000..8e1c3ca9 --- /dev/null +++ b/src/util/external-links.js @@ -0,0 +1,308 @@ +import {empty, stitchArrays} from '#sugar'; + +import { + is, + isStringNonEmpty, + optional, + validateArrayItems, + validateInstanceOf, + validateProperties, +} from '#validators'; + +export const externalLinkStyles = [ + 'normal', + 'compact', + 'icon-id', +]; + +export const isExternalLinkStyle = is(...externalLinkStyles); + +// This might need to be adjusted for YAML importing... +const isExternalLinkSpecRegex = + 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), + }); + +export const isExternalLinkSpec = + validateArrayItems( + validateProperties({ + // TODO: Don't allow providing both of these, and require providing one + matchDomain: optional(isStringNonEmpty), + matchDomains: optional(validateArrayItems(isStringNonEmpty)), + + string: isStringNonEmpty, + + // TODO: Don't allow 'handle' options if handle isn't provided + normal: optional(is('domain', 'handle')), + compact: optional(is('domain', 'handle')), + icon: optional(isStringNonEmpty), + + handle: optional(isExternalLinkHandleSpec), + })); + +export const fallbackDescriptor = { + string: 'external', + + normal: 'domain', + compact: 'domain', + icon: 'globe', +}; + +// TODO: Define all this stuff in data as YAML! +export const externalLinkSpec = [ + { + matchDomain: 'bandcamp.com', + + string: 'bandcamp', + + compact: 'handle', + icon: 'bandcamp', + + handle: {domain: /^[^.]*/}, + }, + + { + matchDomains: ['bc.s3m.us', 'music.solatrux.com'], + + icon: 'bandcamp', + string: 'bandcamp', + + normal: 'domain', + compact: 'domain', + }, + + { + matchDomains: ['types.pl'], + + icon: 'mastodon', + string: 'mastodon', + + compact: 'domain', + }, + + { + matchDomains: ['youtube.com', 'youtu.be'], + + icon: 'youtube', + string: 'youtube', + + compact: 'handle', + + handle: { + pathname: /^(@.*?)\/?$/, + }, + }, + + { + matchDomain: 'soundcloud.com', + + icon: 'soundcloud', + string: 'soundcloud', + + compact: 'handle', + + handle: /[^/]*\/?$/, + }, + + { + matchDomain: 'tumblr.com', + + icon: 'tumblr', + string: 'tumblr', + + compact: 'handle', + + handle: {domain: /^[^.]*/}, + }, + + { + matchDomain: 'twitter.com', + + icon: 'twitter', + string: 'twitter', + + compact: 'handle', + + handle: { + prefix: '@', + pathname: /^@?.*\/?$/, + }, + }, + + { + matchDomain: 'deviantart.com', + + icon: 'deviantart', + string: 'deviantart', + }, + + { + matchDomain: 'instagram.com', + + icon: 'instagram', + string: 'instagram', + }, + + { + matchDomain: 'newgrounds.com', + + icon: 'newgrounds', + string: 'newgrounds', + }, +]; + +export function getMatchingDescriptorsForExternalLink(url, descriptors) { + const {hostname: domain} = new URL(url); + const compare = d => domain.includes(d); + + const matchingDescriptors = + descriptors.filter(spec => { + if (spec.matchDomain && compare(spec.matchDomain)) return true; + if (spec.matchDomains && spec.matchDomains.some(compare)) return true; + return false; + }); + + return [...matchingDescriptors, fallbackDescriptor]; +} + +export function getExternalLinkStringsFromDescriptor(url, descriptor, language) { + const prefix = 'misc.external'; + + const results = { + 'normal': null, + 'compact': null, + 'icon-id': null, + }; + + const {hostname: domain, pathname} = new URL(url); + + const place = language.$(prefix, descriptor.string); + + if (descriptor.icon) { + results['icon-id'] = descriptor.icon; + } + + if (descriptor.normal === 'domain') { + results['normal'] = language.$(prefix, 'withDomain', {place, domain}); + } + + if (descriptor.compact === 'domain') { + results['compact'] = language.sanitize(domain.replace(/^www\./, '')); + } + + let handle = null; + + if (descriptor.handle) { + let regexen = []; + let tests = []; + + let handlePrefix = ''; + + if (descriptor.handle instanceof RegExp) { + regexen.push(descriptor.handle); + tests.push(url); + } else { + for (const [key, value] of Object.entries(descriptor.handle)) { + switch (key) { + case 'prefix': + handlePrefix = value; + continue; + + case 'url': + tests.push(url); + break; + + case 'domain': + case 'hostname': + tests.push(domain); + break; + + case 'path': + case 'pathname': + tests.push(pathname.slice(1)); + break; + + default: + tests.push(''); + break; + } + + regexen.push(value); + } + } + + for (const {regex, test} of stitchArrays({ + regex: regexen, + test: tests, + })) { + const match = test.match(regex); + if (match) { + handle = handlePrefix + (match[1] ?? match[0]); + break; + } + } + } + + if (descriptor.compact === 'handle') { + results.compact = language.sanitize(handle); + } + + if (descriptor.normal === 'handle' && handle) { + results.normal = language.$(prefix, 'withHandle', {place, handle}); + } + + results.normal ??= language.$(prefix, descriptor.string); + + return results; +} + +export function getExternalLinkStringsFromDescriptors(url, descriptors, language) { + const results = { + 'normal': null, + 'compact': null, + 'icon-id': null, + }; + + const remainingKeys = + new Set(Object.keys(results)); + + const matchingDescriptors = + getMatchingDescriptorsForExternalLink(url, descriptors); + + for (const descriptor of matchingDescriptors) { + const descriptorResults = + getExternalLinkStringsFromDescriptor(url, descriptor, language); + + const descriptorKeys = + new Set( + Object.entries(descriptorResults) + .filter(entry => entry[1]) + .map(entry => entry[0])); + + for (const key of remainingKeys) { + if (descriptorKeys.has(key)) { + results[key] = descriptorResults[key]; + remainingKeys.delete(key); + } + } + + if (empty(remainingKeys)) { + return results; + } + } + + return results; +} -- cgit 1.3.0-6-gf8a5 From c5e02f9d314118a534fd0e942d87e74864674498 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 17:42:49 -0400 Subject: content: *mostly* port linkExternal to language.formatExternalLink --- src/util/external-links.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 8e1c3ca9..2047a720 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -12,6 +12,7 @@ import { export const externalLinkStyles = [ 'normal', 'compact', + 'platform', 'icon-id', ]; @@ -181,16 +182,15 @@ export function getMatchingDescriptorsForExternalLink(url, descriptors) { export function getExternalLinkStringsFromDescriptor(url, descriptor, language) { const prefix = 'misc.external'; - const results = { - 'normal': null, - 'compact': null, - 'icon-id': null, - }; + const results = + Object.fromEntries(externalLinkStyles.map(style => [style, null])); const {hostname: domain, pathname} = new URL(url); const place = language.$(prefix, descriptor.string); + results['platform'] = place; + if (descriptor.icon) { results['icon-id'] = descriptor.icon; } @@ -270,11 +270,8 @@ export function getExternalLinkStringsFromDescriptor(url, descriptor, language) } export function getExternalLinkStringsFromDescriptors(url, descriptors, language) { - const results = { - 'normal': null, - 'compact': null, - 'icon-id': null, - }; + const results = + Object.fromEntries(externalLinkStyles.map(style => [style, null])); const remainingKeys = new Set(Object.keys(results)); -- cgit 1.3.0-6-gf8a5 From 0ee5269cd196cd14f06aac6c586e7104159eac74 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 17:47:18 -0400 Subject: content: implement "local" links much more rudimentarily --- src/util/external-links.js | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 2047a720..7a34fa9e 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -64,6 +64,14 @@ export const fallbackDescriptor = { // TODO: Define all this stuff in data as YAML! export const externalLinkSpec = [ + { + matchDomain: 'hsmusic.wiki', + + string: 'local', + + icon: 'globe', + }, + { matchDomain: 'bandcamp.com', -- cgit 1.3.0-6-gf8a5 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/util/external-links.js | 263 +++++++++++++++++++++++++++++++++------------ 1 file changed, 196 insertions(+), 67 deletions(-) (limited to 'src/util') 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 From ba6c4e043b3364481ac3beff1e2a141d1bfcf6fb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 20:47:34 -0400 Subject: external-links: cleaner per-style logic --- src/util/external-links.js | 340 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 266 insertions(+), 74 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 07f46bd3..a0301c9c 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -2,6 +2,7 @@ import {empty, stitchArrays} from '#sugar'; import { is, + isObject, isStringNonEmpty, optional, validateArrayItems, @@ -33,13 +34,14 @@ export const isExternalLinkContext = is(...externalLinkContexts); const isRegExp = validateInstanceOf(RegExp); -export const isExternalLinkHandleSpec = +export const isExternalLinkExtractSpec = validateProperties({ prefix: optional(isStringNonEmpty), url: optional(isRegExp), domain: optional(isRegExp), pathname: optional(isRegExp), + query: optional(isRegExp), }); export const isExternalLinkSpec = @@ -63,12 +65,16 @@ export const isExternalLinkSpec = string: isStringNonEmpty, - // TODO: Don't allow 'handle' options if handle isn't provided - normal: optional(is('domain', 'handle')), - compact: optional(is('domain', 'handle')), + // TODO: Don't allow 'handle' or 'custom' options if the corresponding + // properties aren't provided + normal: optional(is('domain', 'handle', 'custom')), + compact: optional(is('domain', 'handle', 'custom')), icon: optional(isStringNonEmpty), - handle: optional(isExternalLinkHandleSpec), + handle: optional(isExternalLinkExtractSpec), + + // TODO: This should validate each value with isExternalLinkExtractSpec. + custom: optional(isObject), })); export const fallbackDescriptor = { @@ -145,6 +151,38 @@ export const externalLinkSpec = [ icon: 'external', }, + // This takes precedence over the secretPage match below. + { + match: { + context: 'flash', + domain: 'homestuck.com', + pathname: /^story\/[0-9]+\/?$/, + }, + + platform: 'homestuck', + string: 'homestuck.page', + icon: 'globe', + + normal: 'custom', + + custom: { + page: { + pathname: /[0-9]+/, + }, + }, + }, + + { + match: { + context: 'flash', + domain: 'homestuck.com', + pathname: /^story\/.+\/?$/, + }, + + string: 'homestuck.secretPage', + icon: 'globe', + }, + { match: { context: 'flash', @@ -277,6 +315,10 @@ function urlParts(url) { return {domain, pathname, query}; } +function createEmptyResults() { + return Object.fromEntries(externalLinkStyles.map(style => [style, null])); +} + export function getMatchingDescriptorsForExternalLink(url, descriptors, { context = 'generic', } = {}) { @@ -311,107 +353,257 @@ export function getMatchingDescriptorsForExternalLink(url, descriptors, { return [...matchingDescriptors, fallbackDescriptor]; } -export function getExternalLinkStringsFromDescriptor(url, descriptor, { - language, -}) { - const prefix = 'misc.external'; +export function extractPartFromExternalLink(url, extract) { + const {domain, pathname, query} = urlParts(url); - const results = - Object.fromEntries(externalLinkStyles.map(style => [style, null])); + let regexen = []; + let tests = []; + let prefix = ''; + + if (extract instanceof RegExp) { + regexen.push(descriptor.handle); + tests.push(url); + } else { + for (const [key, value] of Object.entries(extract)) { + switch (key) { + case 'prefix': + prefix = value; + continue; + + case 'url': + tests.push(url); + break; + + case 'domain': + tests.push(domain); + break; + + case 'pathname': + tests.push(pathname.slice(1)); + break; + + case 'query': + tests.push(query.slice(1)); + + default: + tests.push(''); + break; + } - const {domain, pathname, query} = urlParts(url); + regexen.push(value); + } + } - const place = language.$(prefix, descriptor.string); + for (const {regex, test} of stitchArrays({ + regex: regexen, + test: tests, + })) { + const match = test.match(regex); + if (match) { + return prefix + (match[1] ?? match[0]); + } + } - results['platform'] = place; + return null; +} - if (descriptor.icon) { - results['icon-id'] = descriptor.icon; +export function extractAllCustomPartsFromExternalLink(url, custom) { + const customParts = {}; + + // All or nothing: if one part doesn't match, all results are scrapped. + for (const [key, value] of Object.entries(custom)) { + customParts[key] = extractPartFromExternalLink(url, value); + if (!customParts[key]) return null; } - if (descriptor.normal === 'domain') { - results['normal'] = language.$(prefix, 'withDomain', {place, domain}); + return customParts; +} + +const prefix = 'misc.external'; + +export function getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}) { + function getPlatform() { + if (descriptor.custom) { + return null; + } + + return language.$(prefix, descriptor.string); } - if (descriptor.compact === 'domain') { - results['compact'] = language.sanitize(domain.replace(/^www\./, '')); + function getDomain() { + return urlParts(url).domain; } - let handle = null; + function getCustom() { + if (!descriptor.custom) { + return null; + } - if (descriptor.handle) { - let regexen = []; - let tests = []; + const customParts = + extractAllCustomPartsFromExternalLink(url, descriptor.custom); - let handlePrefix = ''; + if (!customParts) { + return null; + } - if (descriptor.handle instanceof RegExp) { - regexen.push(descriptor.handle); - tests.push(url); - } else { - for (const [key, value] of Object.entries(descriptor.handle)) { - switch (key) { - case 'prefix': - handlePrefix = value; - continue; - - case 'url': - tests.push(url); - break; - - case 'domain': - case 'hostname': - tests.push(domain); - break; - - case 'path': - case 'pathname': - tests.push(pathname.slice(1) + query); - break; - - default: - tests.push(''); - break; - } - - regexen.push(value); + return language.$(prefix, descriptor.string, customParts); + } + + function getHandle() { + if (!descriptor.handle) { + return null; + } + + return extractPartFromExternalLink(url, descriptor.handle); + } + + function getNormal() { + if (descriptor.custom) { + if (descriptor.normal === 'custom') { + return getCustom(); + } else { + return null; + } + } + + if (descriptor.normal === 'domain') { + const platform = getPlatform(); + const domain = getDomain(); + + if (!platform || !domain) { + return null; } + + return language.$(prefix, 'withDomain', {platform, domain}); } - for (const {regex, test} of stitchArrays({ - regex: regexen, - test: tests, - })) { - const match = test.match(regex); - if (match) { - handle = handlePrefix + (match[1] ?? match[0]); - break; + if (descriptor.normal === 'handle') { + const platform = getPlatform(); + const handle = getHandle(); + + if (!platform || !handle) { + return null; } + + return language.$(prefix, 'withHandle', {platform, handle}); } + + return language.$(prefix, descriptor.string); } - if (descriptor.compact === 'handle') { - results.compact = language.sanitize(handle); + function getCompact() { + if (descriptor.custom) { + if (descriptor.compact === 'custom') { + return getCustom(); + } else { + return null; + } + } + + if (descriptor.compact === 'domain') { + const domain = getDomain(); + + if (!domain) { + return null; + } + + return language.sanitize(domain.replace(/^www\./, '')); + } + + if (descriptor.compact === 'handle') { + const handle = getHandle(); + + if (!handle) { + return null; + } + + return language.sanitize(handle); + } + } + + function getIconId() { + return descriptor.icon ?? null; } - if (descriptor.normal === 'handle' && handle) { - results.normal = language.$(prefix, 'withHandle', {place, handle}); + switch (style) { + case 'normal': return getNormal(); + case 'compact': return getCompact(); + case 'platform': return getPlatform(); + case 'icon-id': return getIconId(); } +} - results.normal ??= language.$(prefix, descriptor.string); +export function couldDescriptorSupportStyle(descriptor, style) { + if (style === 'platform') { + return !descriptor.custom; + } - return results; + if (style === 'icon-id') { + return !!descriptor.icon; + } + + if (style === 'normal') { + if (descriptor.custom) { + return descriptor.normal === 'custom'; + } else { + return true; + } + } + + if (style === 'compact') { + if (descriptor.custom) { + return descriptor.compact === 'custom'; + } else { + return !!descriptor.compact; + } + } } -export function getExternalLinkStringsFromDescriptors(url, descriptors, { +export function getExternalLinkStringOfStyleFromDescriptors(url, style, descriptors, { language, context = 'generic', }) { - const results = - Object.fromEntries(externalLinkStyles.map(style => [style, null])); + const matchingDescriptors = + getMatchingDescriptorsForExternalLink(url, descriptors, {context}); + + console.log('match-filtered:', matchingDescriptors); + + const styleFilteredDescriptors = + matchingDescriptors.filter(descriptor => + couldDescriptorSupportStyle(descriptor, style)); + + console.log('style-filtered:', styleFilteredDescriptors); + + for (const descriptor of styleFilteredDescriptors) { + const descriptorResult = + getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}); - const remainingKeys = - new Set(Object.keys(results)); + if (descriptorResult) { + return descriptorResult; + } + } + + return null; +} + +export function getExternalLinkStringsFromDescriptor(url, descriptor, {language}) { + const getStyle = style => + getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}); + + return { + 'normal': getStyle('normal'), + 'compact': getStyle('compact'), + 'platform': getStyle('platform'), + 'icon-id': getStyle('icon-id'), + }; +} + +export function getExternalLinkStringsFromDescriptors(url, descriptors, { + language, + context = 'generic', +}) { + const results = createEmptyResults(); + const remainingKeys = new Set(Object.keys(results)); const matchingDescriptors = getMatchingDescriptorsForExternalLink(url, descriptors, {context}); -- cgit 1.3.0-6-gf8a5 From 841daeb4a29657485488ac55a743492b010658de Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 21:03:17 -0400 Subject: external-links: spec in terms of platform + substring --- src/util/external-links.js | 115 +++++++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 52 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index a0301c9c..c8cb1670 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -63,7 +63,8 @@ export const isExternalLinkSpec = context: optional(isExternalLinkContext), }), - string: isStringNonEmpty, + platform: isStringNonEmpty, + substring: optional(isStringNonEmpty), // TODO: Don't allow 'handle' or 'custom' options if the corresponding // properties aren't provided @@ -78,7 +79,7 @@ export const isExternalLinkSpec = })); export const fallbackDescriptor = { - string: 'external', + platform: 'external', normal: 'domain', compact: 'domain', @@ -96,7 +97,9 @@ export const externalLinkSpec = [ pathname: /^playlist/, }, - string: 'youtube.playlist', + platform: 'youtube', + substring: 'playlist', + icon: 'youtube', }, @@ -107,7 +110,9 @@ export const externalLinkSpec = [ pathname: /^watch/, }, - string: 'youtube.fullAlbum', + platform: 'youtube', + substring: 'fullAlbum', + icon: 'youtube', }, @@ -117,7 +122,9 @@ export const externalLinkSpec = [ domain: 'youtu.be', }, - string: 'youtube.fullAlbum', + platform: 'youtube', + substring: 'fullAlbum', + icon: 'youtube', }, @@ -129,10 +136,11 @@ export const externalLinkSpec = [ domains: ['youtube.com', 'youtu.be'], }, - string: 'youtube', - icon: 'youtube', + platform: 'youtube', + normal: 'handle', compact: 'handle', + icon: 'youtube', handle: { pathname: /^(@.*?)\/?$/, @@ -147,7 +155,9 @@ export const externalLinkSpec = [ domain: 'bgreco.net', }, - string: 'bgreco.flash', + platform: 'bgreco', + substring: 'flash', + icon: 'external', }, @@ -160,10 +170,10 @@ export const externalLinkSpec = [ }, platform: 'homestuck', - string: 'homestuck.page', - icon: 'globe', + substring: 'page', normal: 'custom', + icon: 'globe', custom: { page: { @@ -179,7 +189,9 @@ export const externalLinkSpec = [ pathname: /^story\/.+\/?$/, }, - string: 'homestuck.secretPage', + platform: 'homestuck', + substring: 'secretPage', + icon: 'globe', }, @@ -189,7 +201,9 @@ export const externalLinkSpec = [ domains: ['youtube.com', 'youtu.be'], }, - string: 'youtube.flash', + platform: 'youtube', + substring: 'flash', + icon: 'youtube', }, @@ -198,17 +212,17 @@ export const externalLinkSpec = [ { match: {domains: ['bc.s3m.us', 'music.solatrux.com']}, - icon: 'bandcamp', - string: 'bandcamp', + platform: 'bandcamp', normal: 'domain', compact: 'domain', + icon: 'bandcamp', }, { match: {domain: 'bandcamp.com'}, - string: 'bandcamp', + platform: 'bandcamp', compact: 'handle', icon: 'bandcamp', @@ -219,28 +233,31 @@ export const externalLinkSpec = [ { match: {domain: 'deviantart.com'}, - string: 'deviantart', + platform: 'deviantart', + icon: 'deviantart', }, { match: {domain: 'instagram.com'}, - string: 'instagram', + platform: 'instagram', + icon: 'instagram', }, { match: {domain: 'homestuck.com'}, - string: 'homestuck', - icon: 'globe', // The horror! + platform: 'homestuck', + + icon: 'globe', }, { match: {domain: 'hsmusic.wiki'}, - string: 'local', + platform: 'local', icon: 'globe', }, @@ -248,37 +265,38 @@ export const externalLinkSpec = [ { match: {domains: ['types.pl']}, - icon: 'mastodon', - string: 'mastodon', + platform: 'mastodon', compact: 'domain', + icon: 'mastodon', }, { match: {domain: 'newgrounds.com'}, - string: 'newgrounds', + platform: 'newgrounds', + icon: 'newgrounds', }, { match: {domain: 'soundcloud.com'}, - string: 'soundcloud', - icon: 'soundcloud', + platform: 'soundcloud', compact: 'handle', + icon: 'soundcloud', - handle: /[^/]*\/?$/, + handle: /([^/]*)\/?$/, }, { match: {domain: 'tumblr.com'}, - string: 'tumblr', - icon: 'tumblr', + platform: 'tumblr', compact: 'handle', + icon: 'tumblr', handle: {domain: /^[^.]*/}, }, @@ -286,10 +304,10 @@ export const externalLinkSpec = [ { match: {domain: 'twitter.com'}, - string: 'twitter', - icon: 'twitter', + platform: 'twitter', compact: 'handle', + icon: 'twitter', handle: { prefix: '@', @@ -300,7 +318,8 @@ export const externalLinkSpec = [ { match: {domains: ['youtube.com', 'youtu.be']}, - string: 'youtube', + platform: 'youtube', + icon: 'youtube', }, ]; @@ -419,15 +438,11 @@ export function extractAllCustomPartsFromExternalLink(url, custom) { return customParts; } -const prefix = 'misc.external'; - export function getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}) { - function getPlatform() { - if (descriptor.custom) { - return null; - } + const prefix = 'misc.external'; - return language.$(prefix, descriptor.string); + function getPlatform() { + return language.$(prefix, descriptor.platform); } function getDomain() { @@ -446,7 +461,7 @@ export function getExternalLinkStringOfStyleFromDescriptor(url, style, descripto return null; } - return language.$(prefix, descriptor.string, customParts); + return language.$(prefix, descriptor.platform, descriptor.substring, customParts); } function getHandle() { @@ -488,7 +503,7 @@ export function getExternalLinkStringOfStyleFromDescriptor(url, style, descripto return language.$(prefix, 'withHandle', {platform, handle}); } - return language.$(prefix, descriptor.string); + return language.$(prefix, descriptor.platform, descriptor.substring); } function getCompact() { @@ -534,14 +549,6 @@ export function getExternalLinkStringOfStyleFromDescriptor(url, style, descripto } export function couldDescriptorSupportStyle(descriptor, style) { - if (style === 'platform') { - return !descriptor.custom; - } - - if (style === 'icon-id') { - return !!descriptor.icon; - } - if (style === 'normal') { if (descriptor.custom) { return descriptor.normal === 'custom'; @@ -557,6 +564,14 @@ export function couldDescriptorSupportStyle(descriptor, style) { return !!descriptor.compact; } } + + if (style === 'platform') { + return true; + } + + if (style === 'icon-id') { + return !!descriptor.icon; + } } export function getExternalLinkStringOfStyleFromDescriptors(url, style, descriptors, { @@ -566,14 +581,10 @@ export function getExternalLinkStringOfStyleFromDescriptors(url, style, descript const matchingDescriptors = getMatchingDescriptorsForExternalLink(url, descriptors, {context}); - console.log('match-filtered:', matchingDescriptors); - const styleFilteredDescriptors = matchingDescriptors.filter(descriptor => couldDescriptorSupportStyle(descriptor, style)); - console.log('style-filtered:', styleFilteredDescriptors); - for (const descriptor of styleFilteredDescriptors) { const descriptorResult = getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}); -- cgit 1.3.0-6-gf8a5 From db786c25a9fafc4cac37b108b4ea433019741c07 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 21:33:15 -0400 Subject: content, external-links: minor fixes --- src/util/external-links.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index c8cb1670..07a83bc1 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -133,7 +133,7 @@ export const externalLinkSpec = [ { match: { context: 'artist', - domains: ['youtube.com', 'youtu.be'], + domain: 'youtube.com', }, platform: 'youtube', @@ -158,7 +158,7 @@ export const externalLinkSpec = [ platform: 'bgreco', substring: 'flash', - icon: 'external', + icon: 'globe', }, // This takes precedence over the secretPage match below. -- cgit 1.3.0-6-gf8a5 From 803a17296249e1521089451c9d077cc524b4acf5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 24 Nov 2023 13:43:34 -0400 Subject: external-links: minor code fixes --- src/util/external-links.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 07a83bc1..dee65cc5 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -380,7 +380,7 @@ export function extractPartFromExternalLink(url, extract) { let prefix = ''; if (extract instanceof RegExp) { - regexen.push(descriptor.handle); + regexen.push(extract); tests.push(url); } else { for (const [key, value] of Object.entries(extract)) { @@ -403,6 +403,7 @@ export function extractPartFromExternalLink(url, extract) { case 'query': tests.push(query.slice(1)); + break; default: tests.push(''); -- cgit 1.3.0-6-gf8a5 From ad1ae12ab182dd50cf3ca6ec653d371d77b5fabb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 24 Nov 2023 14:14:27 -0400 Subject: external-links: quick spec tweaks --- src/util/external-links.js | 77 +++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 22 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index dee65cc5..0a4a77cf 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -4,6 +4,7 @@ import { is, isObject, isStringNonEmpty, + oneOf, optional, validateArrayItems, validateInstanceOf, @@ -60,7 +61,10 @@ export const isExternalLinkSpec = query: optional(isRegExp), queries: optional(validateArrayItems(isRegExp)), - context: optional(isExternalLinkContext), + context: + optional(oneOf( + isExternalLinkContext, + validateArrayItems(isExternalLinkContext))), }), platform: isStringNonEmpty, @@ -130,6 +134,21 @@ export const externalLinkSpec = [ // Special handling for artist links + { + match: { + domain: 'patreon.com', + context: 'artist', + }, + + platform: 'patreon', + + normal: 'handle', + compact: 'handle', + icon: 'globe', + + handle: /([^/]*)\/?$/, + }, + { match: { context: 'artist', @@ -210,7 +229,7 @@ export const externalLinkSpec = [ // Generic domains, sorted alphabetically (by string) { - match: {domains: ['bc.s3m.us', 'music.solatrux.com']}, + match: {domains: ['bc.s3m.us', 'music.solatrus.com']}, platform: 'bandcamp', @@ -220,7 +239,7 @@ export const externalLinkSpec = [ }, { - match: {domain: 'bandcamp.com'}, + match: {domain: '.bandcamp.com'}, platform: 'bandcamp', @@ -232,53 +251,56 @@ export const externalLinkSpec = [ { match: {domain: 'deviantart.com'}, - platform: 'deviantart', - icon: 'deviantart', }, - { - match: {domain: 'instagram.com'}, - - platform: 'instagram', - - icon: 'instagram', - }, - { match: {domain: 'homestuck.com'}, - platform: 'homestuck', - icon: 'globe', }, { match: {domain: 'hsmusic.wiki'}, - platform: 'local', - icon: 'globe', }, + { + match: {domain: 'instagram.com'}, + platform: 'instagram', + icon: 'instagram', + }, + { match: {domains: ['types.pl']}, platform: 'mastodon', + normal: 'domain', compact: 'domain', icon: 'mastodon', }, { match: {domain: 'newgrounds.com'}, - platform: 'newgrounds', - icon: 'newgrounds', }, + { + match: {domain: 'patreon.com'}, + platform: 'patreon', + icon: 'globe', + }, + + { + match: {domain: 'poetryfoundation.org'}, + platform: 'poetryFoundation', + icon: 'globe', + }, + { match: {domain: 'soundcloud.com'}, @@ -291,7 +313,13 @@ export const externalLinkSpec = [ }, { - match: {domain: 'tumblr.com'}, + match: {domain: 'spotify.com'}, + platform: 'spotify', + icon: 'globe', + }, + + { + match: {domain: '.tumblr.com'}, platform: 'tumblr', @@ -316,10 +344,14 @@ export const externalLinkSpec = [ }, { - match: {domains: ['youtube.com', 'youtu.be']}, + match: {domain: 'wikipedia.org'}, + platform: 'wikipedia', + icon: 'misc', + }, + { + match: {domains: ['youtube.com', 'youtu.be']}, platform: 'youtube', - icon: 'youtube', }, ]; @@ -355,6 +387,7 @@ export function getMatchingDescriptorsForExternalLink(url, descriptors, { return false; }) .filter(({match}) => { + if (Array.isArray(match.context)) return match.context.includes(context); if (match.context) return context === match.context; return true; }) -- cgit 1.3.0-6-gf8a5