« get me outta code hell

external-links: cleaner per-style logic - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-11-23 20:47:34 -0400
committer(quasar) nebula <qznebula@protonmail.com>2023-11-24 13:45:35 -0400
commitba6c4e043b3364481ac3beff1e2a141d1bfcf6fb (patch)
tree0ec66b67837bc72a4a726243fe2d65e1c584424f /src
parent8c69ef2b14c4719fa0cd0c7daca27c613167b7ca (diff)
external-links: cleaner per-style logic
Diffstat (limited to 'src')
-rw-r--r--src/data/things/language.js16
-rw-r--r--src/strings-default.yaml19
-rw-r--r--src/util/external-links.js340
3 files changed, 282 insertions, 93 deletions
diff --git a/src/data/things/language.js b/src/data/things/language.js
index f83b421..7048129 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -2,6 +2,7 @@ import {isLanguageCode} from '#validators';
 import {Tag} from '#html';
 
 import {
+  getExternalLinkStringOfStyleFromDescriptors,
   getExternalLinkStringsFromDescriptors,
   isExternalLinkContext,
   isExternalLinkSpec,
@@ -321,20 +322,19 @@ export class Language extends Thing {
       throw new TypeError(`externalLinkSpec unavailable`);
     }
 
-    if (style !== 'all') isExternalLinkStyle(style);
     isExternalLinkContext(context);
 
-    const results =
-      getExternalLinkStringsFromDescriptors(url, this.externalLinkSpec, {
+    if (style === 'all') {
+      return getExternalLinkStringsFromDescriptors(url, this.externalLinkSpec, {
         language: this,
         context,
       });
-
-    if (style === 'all') {
-      return results;
-    } else {
-      return results[style];
     }
+
+    return getExternalLinkStringOfStyleFromDescriptors(url, style, this.externalLinkSpec, {
+      language: this,
+      context,
+    });
   }
 
   formatIndex(value) {
diff --git a/src/strings-default.yaml b/src/strings-default.yaml
index 698e3c9..d0d4699 100644
--- a/src/strings-default.yaml
+++ b/src/strings-default.yaml
@@ -407,10 +407,10 @@ misc:
     external: "External"
 
     withDomain:
-      "{PLACE} ({DOMAIN})"
+      "{PLATFORM} ({DOMAIN})"
 
     withHandle:
-      "{PLACE} ({HANDLE})"
+      "{PLATFORM} ({HANDLE})"
 
     local: "Wiki Archive (local upload)"
 
@@ -421,7 +421,12 @@ misc:
       flash: "bgreco.net (high quality audio)"
 
     deviantart: "DeviantArt"
-    homestuck: "Homestuck"
+
+    homestuck:
+      _: "Homestuck"
+      page: "Homestuck (page {PAGE})"
+      secretPage: "Homestuck (secret page)"
+
     instagram: "Instagram"
     mastodon: "Mastodon"
     newgrounds: "Newgrounds"
@@ -439,14 +444,6 @@ misc:
       playlist: "YouTube (playlist)"
       fullAlbum: "YouTube (full album)"
 
-  # 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
   #   that isn't available under the wiki's media directory. While it
diff --git a/src/util/external-links.js b/src/util/external-links.js
index 07f46bd..a0301c9 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});