« 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/util
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/util
parent8c69ef2b14c4719fa0cd0c7daca27c613167b7ca (diff)
external-links: cleaner per-style logic
Diffstat (limited to 'src/util')
-rw-r--r--src/util/external-links.js340
1 files changed, 266 insertions, 74 deletions
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});