From 0202375db8ccd03d98ed6c2ffbb800b67c026639 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 09:16:23 -0400 Subject: content, css: vertical tooltips + basic external parsing --- src/content/dependencies/linkExternalAsIcon.js | 265 +++++++++++++++++++++---- 1 file changed, 231 insertions(+), 34 deletions(-) (limited to 'src/content/dependencies/linkExternalAsIcon.js') diff --git a/src/content/dependencies/linkExternalAsIcon.js b/src/content/dependencies/linkExternalAsIcon.js index cd168992..d3ed9122 100644 --- a/src/content/dependencies/linkExternalAsIcon.js +++ b/src/content/dependencies/linkExternalAsIcon.js @@ -1,6 +1,202 @@ -// TODO: Define these as extra dependencies and pass them somewhere -const BANDCAMP_DOMAINS = ['bc.s3m.us', 'music.solatrux.com']; -const MASTODON_DOMAINS = ['types.pl']; +import {stitchArrays} from '#sugar'; + +const fallbackDescriptor = { + icon: 'globe', + string: 'external', + + normal: 'domain', + compact: 'domain', +}; + +// TODO: Define all this stuff in data! +const externalSpec = [ + { + matchDomain: 'bandcamp.com', + + icon: 'bandcamp', + string: 'bandcamp', + + compact: 'handle', + + 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', + }, +]; + +function determineLinkText(url, descriptor, {language}) { + const prefix = 'misc.external'; + + const { + hostname: domain, + pathname, + } = new URL(url); + + let normal = null; + let compact = null; + + const place = language.$(prefix, descriptor.string); + + if (descriptor.normal === 'domain') { + normal = language.$(prefix, 'withDomain', {place, domain}); + } + + if (descriptor.compact === 'domain') { + compact = 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') { + compact = handle; + } + + if (normal === 'handle' && handle) { + normal = language.$(prefix, 'withHandle', {place, handle}); + } + + normal ??= language.$(prefix, descriptor.string); + + return {normal, compact}; +} export default { extraDependencies: ['html', 'language', 'to'], @@ -9,38 +205,39 @@ export default { return {url}; }, - generate(data, {html, language, to}) { - const domain = new URL(data.url).hostname; - const [id, msg] = ( - domain.includes('bandcamp.com') - ? ['bandcamp', language.$('misc.external.bandcamp')] - : BANDCAMP_DOMAINS.includes(domain) - ? ['bandcamp', language.$('misc.external.bandcamp.domain', {domain})] - : MASTODON_DOMAINS.includes(domain) - ? ['mastodon', language.$('misc.external.mastodon.domain', {domain})] - : domain.includes('youtu') - ? ['youtube', language.$('misc.external.youtube')] - : domain.includes('soundcloud') - ? ['soundcloud', language.$('misc.external.soundcloud')] - : domain.includes('tumblr.com') - ? ['tumblr', language.$('misc.external.tumblr')] - : domain.includes('twitter.com') - ? ['twitter', language.$('misc.external.twitter')] - : domain.includes('deviantart.com') - ? ['deviantart', language.$('misc.external.deviantart')] - : domain.includes('instagram.com') - ? ['instagram', language.$('misc.external.bandcamp')] - : domain.includes('newgrounds.com') - ? ['newgrounds', language.$('misc.external.newgrounds')] - : ['globe', language.$('misc.external.domain', {domain})]); + slots: { + withText: {type: 'boolean'}, + }, + + generate(data, slots, {html, language, to}) { + const {hostname: domain} = new URL(data.url); + + const descriptor = + externalSpec.find(({matchDomain, matchDomains}) => { + const compare = d => domain.includes(d); + if (matchDomain && compare(matchDomain)) return true; + if (matchDomains && matchDomains.some(compare)) return true; + return false; + }) ?? fallbackDescriptor; + + const {normal: normalText, compact: compactText} = + determineLinkText(data.url, descriptor, {language}); return html.tag('a', - {href: data.url, class: 'icon'}, - html.tag('svg', [ - html.tag('title', msg), - html.tag('use', { - href: to('shared.staticIcon', id), - }), - ])); + {href: data.url, class: ['icon', slots.withText && 'has-text']}, + [ + html.tag('svg', [ + !slots.withText && + html.tag('title', normalText), + + html.tag('use', { + href: to('shared.staticIcon', descriptor.icon), + }), + ]), + + slots.withText && + html.tag('span', {class: 'icon-text'}, + compactText ?? normalText), + ]); }, }; -- 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/content/dependencies/linkExternalAsIcon.js | 223 +------------------------ 1 file changed, 7 insertions(+), 216 deletions(-) (limited to 'src/content/dependencies/linkExternalAsIcon.js') diff --git a/src/content/dependencies/linkExternalAsIcon.js b/src/content/dependencies/linkExternalAsIcon.js index d3ed9122..58bd896d 100644 --- a/src/content/dependencies/linkExternalAsIcon.js +++ b/src/content/dependencies/linkExternalAsIcon.js @@ -1,237 +1,28 @@ -import {stitchArrays} from '#sugar'; - -const fallbackDescriptor = { - icon: 'globe', - string: 'external', - - normal: 'domain', - compact: 'domain', -}; - -// TODO: Define all this stuff in data! -const externalSpec = [ - { - matchDomain: 'bandcamp.com', - - icon: 'bandcamp', - string: 'bandcamp', - - compact: 'handle', - - 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', - }, -]; - -function determineLinkText(url, descriptor, {language}) { - const prefix = 'misc.external'; - - const { - hostname: domain, - pathname, - } = new URL(url); - - let normal = null; - let compact = null; - - const place = language.$(prefix, descriptor.string); - - if (descriptor.normal === 'domain') { - normal = language.$(prefix, 'withDomain', {place, domain}); - } - - if (descriptor.compact === 'domain') { - compact = 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') { - compact = handle; - } - - if (normal === 'handle' && handle) { - normal = language.$(prefix, 'withHandle', {place, handle}); - } - - normal ??= language.$(prefix, descriptor.string); - - return {normal, compact}; -} - export default { extraDependencies: ['html', 'language', 'to'], - data(url) { - return {url}; - }, + data: (url) => ({url}), slots: { withText: {type: 'boolean'}, }, generate(data, slots, {html, language, to}) { - const {hostname: domain} = new URL(data.url); - - const descriptor = - externalSpec.find(({matchDomain, matchDomains}) => { - const compare = d => domain.includes(d); - if (matchDomain && compare(matchDomain)) return true; - if (matchDomains && matchDomains.some(compare)) return true; - return false; - }) ?? fallbackDescriptor; + const {url} = data; - const {normal: normalText, compact: compactText} = - determineLinkText(data.url, descriptor, {language}); + const normalText = language.formatExternalLink(url, {style: 'normal'}); + const compactText = language.formatExternalLink(url, {style: 'compact'}); + const iconId = language.formatExternalLink(url, {style: 'icon-id'}); return html.tag('a', - {href: data.url, class: ['icon', slots.withText && 'has-text']}, + {href: url, class: ['icon', slots.withText && 'has-text']}, [ html.tag('svg', [ !slots.withText && html.tag('title', normalText), html.tag('use', { - href: to('shared.staticIcon', descriptor.icon), + href: to('shared.staticIcon', iconId), }), ]), -- cgit 1.3.0-6-gf8a5 From 8c69ef2b14c4719fa0cd0c7daca27c613167b7ca Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 18:52:04 -0400 Subject: content: contextual external links --- src/content/dependencies/linkExternalAsIcon.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'src/content/dependencies/linkExternalAsIcon.js') diff --git a/src/content/dependencies/linkExternalAsIcon.js b/src/content/dependencies/linkExternalAsIcon.js index 58bd896d..357c835c 100644 --- a/src/content/dependencies/linkExternalAsIcon.js +++ b/src/content/dependencies/linkExternalAsIcon.js @@ -1,21 +1,32 @@ +import {isExternalLinkContext} from '#external-links'; + export default { extraDependencies: ['html', 'language', 'to'], data: (url) => ({url}), slots: { + context: { + // This awkward syntax is because the slot descriptor validator can't + // differentiate between a function that returns a validator (the usual + // syntax) and a function that is itself a validator. + validate: () => isExternalLinkContext, + default: 'generic', + }, + withText: {type: 'boolean'}, }, generate(data, slots, {html, language, to}) { - const {url} = data; + const format = style => + language.formatExternalLink(data.url, {style, context: slots.context}); - const normalText = language.formatExternalLink(url, {style: 'normal'}); - const compactText = language.formatExternalLink(url, {style: 'compact'}); - const iconId = language.formatExternalLink(url, {style: 'icon-id'}); + const normalText = format('normal'); + const compactText = format('compact'); + const iconId = format('icon-id'); return html.tag('a', - {href: url, class: ['icon', slots.withText && 'has-text']}, + {href: data.url, class: ['icon', slots.withText && 'has-text']}, [ html.tag('svg', [ !slots.withText && -- cgit 1.3.0-6-gf8a5