« get me outta code hell

find: always read matches from pre-cached hash map - 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:
author(quasar) nebula <qznebula@protonmail.com>2023-10-02 10:37:10 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-10-02 10:37:10 -0300
commit7c938421035502484ed0e15b11064421bd7fcfce (patch)
treee737a640784b611ae9927d55e316b5adbe0466ff /src/find.js
parentd30bc62956358637522d636b4454aee39e7b3d03 (diff)
find: always read matches from pre-cached hash map
Diffstat (limited to 'src/find.js')
-rw-r--r--src/find.js331
1 files changed, 115 insertions, 216 deletions
diff --git a/src/find.js b/src/find.js
index 2959ed5..7877545 100644
--- a/src/find.js
+++ b/src/find.js
@@ -1,6 +1,7 @@
 import {inspect} from 'node:util';
 
 import {colors, logWarn} from '#cli';
+import {typeAppearance} from '#sugar';
 
 function warnOrThrow(mode, message) {
   if (mode === 'error') {
@@ -14,253 +15,151 @@ function warnOrThrow(mode, message) {
   return null;
 }
 
-function findHelper(keys, findFns = {}) {
+export function processAllAvailableMatches(data, {
+  getMatchableNames = thing => [thing.name],
+} = {}) {
+  const byName = Object.create(null);
+  const byDirectory = Object.create(null);
+  const multipleNameMatches = Object.create(null);
+
+  for (const thing of data) {
+    for (const name of getMatchableNames(thing)) {
+      const normalizedName = name.toLowerCase();
+      if (normalizedName in byName) {
+        byName[normalizedName] = null;
+        if (normalizedName in multipleNameMatches) {
+          multipleNameMatches[normalizedName].push(thing);
+        } else {
+          multipleNameMatches[normalizedName] = [thing];
+        }
+      } else {
+        byName[normalizedName] = thing;
+      }
+    }
+
+    byDirectory[thing.directory] = thing;
+  }
+
+  return {byName, byDirectory, multipleNameMatches};
+}
+
+function findHelper({
+  referenceTypes,
+
+  getMatchableNames = undefined,
+}) {
+  const keyRefRegex =
+    new RegExp(String.raw`^(?:(${referenceTypes.join('|')}):(?=\S))?(.*)$`);
+
   // Note: This cache explicitly *doesn't* support mutable data arrays. If the
   // data array is modified, make sure it's actually a new array object, not
   // the original, or the cache here will break and act as though the data
   // hasn't changed!
   const cache = new WeakMap();
 
-  const byDirectory = findFns.byDirectory || matchDirectory;
-  const byName = findFns.byName || matchName;
-
-  const keyRefRegex = new RegExp(String.raw`^(?:(${keys.join('|')}):(?=\S))?(.*)$`);
-
   // 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, {mode = 'warn'}) => {
     if (!fullRef) return null;
-
-    if (typeof fullRef !== 'string' && !Array.isArray(fullRef)) {
-      throw new Error(`Got a reference that is ${typeof fullRef}, not string or array: ${fullRef}`);
+    if (typeof fullRef !== 'string') {
+      throw new TypeError(`Expected a string, got ${typeAppearance(fullRef)}`);
     }
 
     if (!data) {
-      throw new Error(`Expected data to be present`);
+      throw new TypeError(`Expected data to be present`);
     }
 
-    let cacheForThisData = cache.get(data);
-    if (!cacheForThisData) {
-      cacheForThisData = Object.create(null);
-      cache.set(data, cacheForThisData);
-    }
-
-    const parseFullRef = fullRef => {
-      const regexMatch = fullRef.match(keyRefRegex);
-      if (!regexMatch) {
-        warnOrThrow(mode, `Malformed link reference: "${fullRef[i]}"`);
-        return {error: true, key: null, ref: null};
-      }
-
-      const key = regexMatch[1];
-      const ref = regexMatch[2];
-
-      return {error: false, key, ref};
-    };
+    let subcache = cache.get(data);
+    if (!subcache) {
+      subcache =
+        processAllAvailableMatches(data, {
+          getMatchableNames,
+        });
 
-    if (typeof fullRef === 'string') {
-      const cachedMatch = cacheForThisData[fullRef];
-      if (cachedMatch) return cachedMatch;
-
-      const {error: regexError, key, ref} = parseFullRef(fullRef);
-      if (regexError) return null;
-
-      const match =
-        (key
-          ? byDirectory(ref, data, mode)
-          : byName(ref, data, mode));
-
-      if (!match) {
-        warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`);
-      }
-
-      cacheForThisData[fullRef] = match;
-
-      return match;
+      cache.set(data, subcache);
     }
 
-    const fullRefList = fullRef;
-    if (Array.isArray(fullRefList)) {
-      const byDirectoryUncachedIndices = [];
-      const byDirectoryUncachedRefs = [];
-      const byNameUncachedIndices = [];
-      const byNameUncachedRefs = [];
-
-      for (let index = 0; index < fullRefList.length; index++) {
-        const cachedMatch = cacheForThisData[fullRefList[index]];
-        if (cachedMatch) return cachedMatch;
-
-        const {error: regexError, key, ref} = parseFullRef(fullRefList[index]);
-        if (regexError) return null;
-
-        if (key) {
-          byDirectoryUncachedIndices.push(index);
-          byDirectoryUncachedRefs.push(ref);
-        } else {
-          byNameUncachedIndices.push(index);
-          byNameUncachedRefs.push(ref);
-        }
-      }
-
-      const byDirectoryMatches = byDirectory(byDirectoryUncachedRefs, data, mode);
-      const byNameMatches = byName(byNameUncachedRefs, data, mode);
-
-      const results = [];
-
-      const processMatch = (match, sourceIndex) => {
-        if (match) {
-          cacheForThisData[fullRefList[sourceIndex]] = match;
-          results[sourceIndex] = match;
-        } else {
-          // TODO: Aggregate errors
-          warnOrThrow(mode, `Didn't match anything for ${fullRefList[sourceIndex]}`);
-          results[sourceIndex] = null;
-        }
-      };
-
-      for (let index = 0; index < byDirectoryMatches.length; index++) {
-        const sourceIndex = byDirectoryUncachedIndices[index];
-        const match = byDirectoryMatches[index];
-        processMatch(match, sourceIndex);
-      }
-
-      for (let index = 0; index < byNameMatches.length; index++) {
-        const sourceIndex = byNameUncachedIndices[index];
-        const match = byNameMatches[index];
-        processMatch(match, sourceIndex);
-      }
-
-      return results;
+    const regexMatch = fullRef.match(keyRefRegex);
+    if (!regexMatch) {
+      warnOrThrow(mode, `Malformed link reference: "${fullRef}"`);
     }
-  };
-}
-
-function matchDirectory(ref, data) {
-  if (typeof ref === 'string') {
-    return data.find(({directory}) => directory === ref);
-  }
-
-  const refList = ref;
-  if (Array.isArray(refList)) {
-    const refSet = new Set(refList);
-    const refMap = new Map();
 
-    for (const thing of data) {
-      const {directory} = thing;
-      if (refSet.has(directory)) {
-        refMap.set(directory, thing);
+    const typePart = regexMatch[1];
+    const refPart = regexMatch[2];
+
+    const match =
+      (typePart
+        ? subcache.byDirectory[refPart]
+        : subcache.byName[refPart.toLowerCase()]);
+
+    if (!match && !typePart) {
+      if (subcache.multipleNameMatches[refPart]) {
+        return warnOrThrow(mode,
+          `Multiple matches for reference "${fullRef}". Please resolve:\n` +
+          subcache.multipleNameMatches[refPart]
+            .map(match => `- ${inspect(match)}\n`)
+            .join('') +
+          `Returning null for this reference.`);
       }
     }
 
-    return refList.map(ref => refMap.get(ref) ?? null);
-  }
-}
-
-function matchName(ref, data, mode) {
-  if (typeof ref === 'string') {
-    const matches =
-      data
-        .filter(({name}) => name.toLowerCase() === ref.toLowerCase())
-        .filter(thing =>
-          (Object.hasOwn(thing, 'alwaysReferenceByDirectory')
-            ? !thing.alwaysReferenceByDirectory
-            : true));
-
-    if (matches.length > 1) {
-      return warnOrThrow(mode,
-        `Multiple matches for reference "${ref}". Please resolve:\n` +
-        matches.map(match => `- ${inspect(match)}\n`).join('') +
-        `Returning null for this reference.`);
-    }
-
-    if (matches.length === 0) {
+    if (!match) {
+      warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`);
       return null;
     }
 
-    const match = matches[0];
-
-    if (ref !== match.name) {
-      warnOrThrow(mode,
-        `Bad capitalization: ${colors.red(ref)} -> ${colors.green(match.name)}`);
-    }
-
     return match;
-  }
-
-  const refList = ref;
-  if (Array.isArray(refList)) {
-    const refSet = new Set(refList.map(name => name.toLowerCase()));
-    const refMap = new Map();
-    const multipleMatchesMap = new Map();
-
-    for (const thing of data) {
-      if (thing.alwaysReferenceByDirectory) continue;
-      const name = thing.name.toLowerCase();
-      if (refSet.has(name)) {
-        if (refMap.has(name)) {
-          refMap.set(name, null); // .has() will still return true
-          if (multipleMatchesMap.has(name)) {
-            multipleMatchesMap.get(name).push(thing);
-          } else {
-            multipleMatchesMap.set(name, [thing]);
-          }
-        } else {
-          refMap.set(name, thing);
-        }
-      }
-    }
-
-    // TODO: Aggregate errors
-    for (const [name, matches] of multipleMatchesMap.entries()) {
-      warnOrThrow(mode,
-        `Multiple matches for reference "${ref}". Please resolve:\n` +
-        matches.map(match => `- ${inspect(match)}\n`).join('') +
-        `Returning null for this reference.`);
-    }
-
-    return refList.map(ref => {
-      const match = refMap.get(ref);
-      if (!match) return null;
-
-      // TODO: Aggregate errors
-      if (ref !== match.name) {
-        warnOrThrow(mode,
-          `Bad capitalization: ${colors.red(ref)} -> ${colors.green(match.name)}`);
-      }
-
-      return match;
-    });
-  }
-}
-
-function matchTagName(ref, data, mode) {
-  if (typeof ref === 'string') {
-    return matchName(
-      ref.startsWith('cw: ') ? ref.slice(4) : ref,
-      data,
-      mode);
-  }
-
-  if (Array.isArray(ref)) {
-    return matchName(
-      ref.map(ref => ref.startsWith('cw: ') ? ref.slice(4) : ref),
-      data,
-      mode);
-  }
+  };
 }
 
 const find = {
-  album: findHelper(['album', 'album-commentary', 'album-gallery']),
-  artist: findHelper(['artist', 'artist-gallery']),
-  artTag: findHelper(['tag'], {byName: matchTagName}),
-  flash: findHelper(['flash']),
-  group: findHelper(['group', 'group-gallery']),
-  listing: findHelper(['listing']),
-  newsEntry: findHelper(['news-entry']),
-  staticPage: findHelper(['static']),
-  track: findHelper(['track']),
+  album: findHelper({
+    referenceTypes: ['album', 'album-commentary', 'album-gallery'],
+  }),
+
+  artist: findHelper({
+    referenceTypes: ['artist', 'artist-gallery'],
+  }),
+
+  artTag: findHelper({
+    referenceTypes: ['tag'],
+
+    getMatchableNames: tag =>
+      (tag.isContentWarning
+        ? [`cw: ${tag.name}`]
+        : [tag.name]),
+  }),
+
+  flash: findHelper({
+    referenceTypes: ['flash'],
+  }),
+
+  group: findHelper({
+    referenceTypes: ['group', 'group-gallery'],
+  }),
+
+  listing: findHelper({
+    referenceTypes: ['listing'],
+  }),
+
+  newsEntry: findHelper({
+    referenceTypes: ['news-entry'],
+  }),
+
+  staticPage: findHelper({
+    referenceTypes: ['static'],
+  }),
+
+  track: findHelper({
+    referenceTypes: ['track'],
+
+    getMatchableNames: track =>
+      (track.alwaysReferenceByDirectory
+        ? []
+        : [track.name]),
+  }),
 };
 
 export default find;