« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/find.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/find.js')
-rw-r--r--src/find.js166
1 files changed, 134 insertions, 32 deletions
diff --git a/src/find.js b/src/find.js
index e7f5cda1..7b605e97 100644
--- a/src/find.js
+++ b/src/find.js
@@ -4,6 +4,7 @@ import {colors, logWarn} from '#cli';
 import {compareObjects, stitchArrays, typeAppearance} from '#sugar';
 import thingConstructors from '#things';
 import {isFunction, validateArrayItems} from '#validators';
+import {getCaseSensitiveKebabCase} from '#wiki-data';
 
 import * as fr from './find-reverse.js';
 
@@ -30,7 +31,34 @@ function warnOrThrow(mode, message) {
 export const keyRefRegex =
   new RegExp(String.raw`^(?:(?<key>[a-z-]*):(?=\S))?(?<ref>.*)$`);
 
-export function processAvailableMatchesByName(data, {
+function getFuzzHash(fuzz = {}) {
+  if (!fuzz) {
+    return 0;
+  }
+
+  return (
+    fuzz.capitalization << 0 +
+    fuzz.kebab << 1
+  );
+}
+
+export function fuzzName(name, fuzz = {}) {
+  if (!fuzz) {
+    return name;
+  }
+
+  if (fuzz.capitalization) {
+    name = name.toLowerCase();
+  }
+
+  if (fuzz.kebab) {
+    name = getCaseSensitiveKebabCase(name);
+  }
+
+  return name;
+}
+
+export function processAvailableMatchesByName(data, fuzz, {
   include = _thing => true,
 
   getMatchableNames = thing =>
@@ -50,17 +78,20 @@ export function processAvailableMatchesByName(data, {
         continue;
       }
 
-      const normalizedName = name.toLowerCase();
+      const normalizedName = fuzzName(name, fuzz);
 
       if (normalizedName in results) {
         if (normalizedName in multipleNameMatches) {
           multipleNameMatches[normalizedName].push(thing);
         } else {
-          multipleNameMatches[normalizedName] = [results[normalizedName], thing];
+          multipleNameMatches[normalizedName] = [
+            results[normalizedName].thing,
+            thing,
+          ];
           results[normalizedName] = null;
         }
       } else {
-        results[normalizedName] = thing;
+        results[normalizedName] = {thing, name};
       }
     }
   }
@@ -87,16 +118,16 @@ export function processAvailableMatchesByDirectory(data, {
         continue;
       }
 
-      results[directory] = thing;
+      results[directory] = {thing, directory};
     }
   }
 
   return {results};
 }
 
-export function processAllAvailableMatches(data, spec) {
+export function processAllAvailableMatches(data, fuzz, spec) {
   const {results: byName, multipleNameMatches} =
-    processAvailableMatchesByName(data, spec);
+    processAvailableMatchesByName(data, fuzz, spec);
 
   const {results: byDirectory} =
     processAvailableMatchesByDirectory(data, spec);
@@ -109,21 +140,69 @@ function oopsMultipleNameMatches(mode, {
   normalizedName,
   multipleNameMatches,
 }) {
+  try {
+    return warnOrThrow(mode,
+      `Multiple matches for reference "${name}". Please resolve:\n` +
+      multipleNameMatches[normalizedName]
+        .map(match => `- ${inspect(match)}\n`)
+        .join('') +
+      `Returning null for this reference.`);
+  } catch (caughtError) {
+    throw Object.assign(caughtError, {
+      [Symbol.for('hsmusic.find.multipleNameMatches')]:
+        multipleNameMatches[normalizedName],
+    });
+  }
+}
+
+function oopsNameCapitalizationMismatch(mode, {
+  matchingName,
+  matchedName,
+}) {
+  if (matchingName.length === matchedName.length) {
+    let a = '', b = '';
+    for (let i = 0; i < matchingName.length; i++) {
+      if (
+        matchingName[i] === matchedName[i] ||
+        matchingName[i].toLowerCase() !== matchingName[i].toLowerCase()
+      ) {
+        a += matchingName[i];
+        b += matchedName[i];
+      } else {
+        a += colors.bright(colors.red(matchingName[i]));
+        b += colors.bright(colors.green(matchedName[i]));
+      }
+    }
+
+    matchingName = a;
+    matchedName = b;
+  }
+
   return warnOrThrow(mode,
-    `Multiple matches for reference "${name}". Please resolve:\n` +
-    multipleNameMatches[normalizedName]
-      .map(match => `- ${inspect(match)}\n`)
-      .join('') +
+    `Provided capitalization differs from the matched name. Please resolve:\n` +
+    `- provided: ${matchingName}\n` +
+    `- should be: ${matchedName}\n` +
     `Returning null for this reference.`);
 }
 
-export function prepareMatchByName(mode, {byName, multipleNameMatches}) {
+export function prepareMatchByName(mode, fuzz, {byName, multipleNameMatches}) {
   return (name) => {
-    const normalizedName = name.toLowerCase();
+    const normalizedName = fuzzName(name, fuzz);
     const match = byName[normalizedName];
 
     if (match) {
-      return match;
+      if (
+        !fuzz?.capitalization &&
+        name !== match.name &&
+        name.toLowerCase === match.name.toLowerCase()
+      ) {
+        return oopsNameCapitalizationMismatch(mode, {
+          matchingName: name,
+          matchedName: match.name,
+        });
+      } else {
+        return match.thing;
+      }
     } else if (multipleNameMatches[normalizedName]) {
       return oopsMultipleNameMatches(mode, {
         name,
@@ -154,7 +233,13 @@ export function prepareMatchByDirectory(mode, {referenceTypes, byDirectory}) {
       });
     }
 
-    return byDirectory[directory];
+    const match = byDirectory[directory];
+
+    if (match) {
+      return match.thing;
+    } else {
+      return null;
+    }
   };
 }
 
@@ -196,11 +281,18 @@ function findHelper({
   // hasn't changed!
   const cache = new WeakMap();
 
-  // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws
-  // errors for null matches (with details about the error), while 'warn' and
-  // 'quiet' both return null, with 'warn' logging details directly to the
-  // console.
-  return (fullRef, data, {mode = 'warn'} = {}) => {
+  return (fullRef, data, {
+    // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws
+    // errors for null matches (with details about the error), while 'warn' and
+    // 'quiet' both return null, with 'warn' logging details directly to the
+    // console.
+    mode = 'warn',
+
+    fuzz = {
+      capitalization: false,
+      kebab: false,
+    },
+  } = {}) => {
     if (!fullRef) return null;
 
     if (typeof fullRef !== 'string') {
@@ -211,19 +303,23 @@ function findHelper({
       throw new TypeError(`Expected data to be present`);
     }
 
-    let subcache = cache.get(data);
-    if (!subcache) {
-      subcache =
-        processAllAvailableMatches(data, {
+    let dataSubcache = cache.get(data);
+    if (!dataSubcache) {
+      cache.set(data, dataSubcache = new Map());
+    }
+
+    const fuzzHash = getFuzzHash(fuzz);
+    let fuzzSubcache = dataSubcache.get(fuzzHash);
+    if (!fuzzSubcache) {
+      dataSubcache.set(fuzzHash, fuzzSubcache =
+        processAllAvailableMatches(data, fuzz, {
           include,
           getMatchableNames,
           getMatchableDirectories,
-        });
-
-      cache.set(data, subcache);
+        }));
     }
 
-    const {byDirectory, byName, multipleNameMatches} = subcache;
+    const {byDirectory, byName, multipleNameMatches} = fuzzSubcache;
 
     return matchHelper(fullRef, mode, {
       matchByDirectory:
@@ -233,7 +329,7 @@ function findHelper({
         }),
 
       matchByName:
-        prepareMatchByName(mode, {
+        prepareMatchByName(mode, fuzz, {
           byName,
           multipleNameMatches,
         }),
@@ -312,7 +408,7 @@ function findMixedHelper(config) {
       const multipleNameMatches = Object.create(null);
 
       for (const spec of specs) {
-        processAvailableMatchesByName(data, {
+        processAvailableMatchesByName(data, null, {
           ...spec,
 
           results: byName,
@@ -345,11 +441,17 @@ function findMixedHelper(config) {
           });
         }
 
-        return byDirectory[referenceType][directory];
+        const match = byDirectory[referenceType][directory];
+
+        if (match) {
+          return match.thing;
+        } else {
+          return null;
+        }
       },
 
       matchByName:
-        prepareMatchByName(mode, {
+        prepareMatchByName(mode, null, {
           byName,
           multipleNameMatches,
         }),