« get me outta code hell

find: kebab-fuzzy matches in content text - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2025-09-21 12:46:39 -0300
committer(quasar) nebula <qznebula@protonmail.com>2025-09-21 12:46:39 -0300
commit934494b5f3ee610d6473b3ca4c21c80731c213af (patch)
tree06bd69c1ca93d326e582df5cf303875eab341d56
parent72d9bf3ca4beaee26a59e5c68023d20ec1513f65 (diff)
find: kebab-fuzzy matches in content text
Outside content text, this doesn't cause miscapitalized references
to slip past, but it does stop them from being specially reported
i.e. highlighting which letters need to be updated or treating as
more than just "nothing matches for this reference" generic errors.

That's a TODO. Sorry! Gyeep!
-rw-r--r--src/common-util/wiki-data.js7
-rw-r--r--src/content/dependencies/transformContent.js22
-rw-r--r--src/data/checks.js10
-rw-r--r--src/find.js93
4 files changed, 99 insertions, 33 deletions
diff --git a/src/common-util/wiki-data.js b/src/common-util/wiki-data.js
index 0f6591c1..3fde2495 100644
--- a/src/common-util/wiki-data.js
+++ b/src/common-util/wiki-data.js
@@ -11,7 +11,7 @@ export {filterMultipleArrays} from './sugar.js';
 
 // Generic value operations
 
-export function getKebabCase(name) {
+export function getCaseSensitiveKebabCase(name) {
   return name
 
     // Spaces to dashes
@@ -53,9 +53,10 @@ export function getKebabCase(name) {
 
     // Trim dashes on boundaries
     .replace(/^-+|-+$/g, '')
+}
 
-    // Always lowercase
-    .toLowerCase();
+export function getKebabCase(name) {
+  return getCaseSensitiveKebabCase(name).toLowerCase();
 }
 
 // Specific data utilities
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index 06056ffc..3f738db2 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -79,7 +79,14 @@ export default {
   ],
 
   sprawl(wikiData, content) {
-    const find = bindFind(wikiData, {mode: 'quiet'});
+    const find =
+      bindFind(wikiData, {
+        mode: 'quiet',
+        fuzz: {
+          capitalization: true,
+          kebab: true,
+        },
+      });
 
     const {result: parsedNodes, error} =
       parseContentNodes(content ?? '', {errorMode: 'return'});
@@ -162,9 +169,16 @@ export default {
 
             data.label =
               enteredLabel ??
-                (transformName && data.thing.name
-                  ? transformName(data.thing.name, node, content)
-                  : null);
+
+              (transformName && data.thing.name &&
+               replacerKeyImplied && replacerValue === data.thing.name
+
+                ? transformName(data.thing.name, node, content)
+                : null) ??
+
+              (replacerKeyImplied
+                ? replacerValue
+                : null);
 
             data.hash = enteredHash ?? null;
 
diff --git a/src/data/checks.js b/src/data/checks.js
index 3fcb6d3b..5eba593b 100644
--- a/src/data/checks.js
+++ b/src/data/checks.js
@@ -637,7 +637,15 @@ export function reportContentTextErrors(wikiData, {
     }],
   ];
 
-  const boundFind = bindFind(wikiData, {mode: 'error'});
+  const boundFind =
+    bindFind(wikiData, {
+      mode: 'error',
+      fuzz: {
+        capitalization: true,
+        kebab: true,
+      },
+    });
+
   const findArtistOrAlias = bindFindArtistOrAlias(boundFind);
 
   function* processContent(input) {
diff --git a/src/find.js b/src/find.js
index 8f2170d4..b44c1bb2 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,7 +78,7 @@ export function processAvailableMatchesByName(data, {
         continue;
       }
 
-      const normalizedName = name.toLowerCase();
+      const normalizedName = fuzzName(name, fuzz);
 
       if (normalizedName in results) {
         if (normalizedName in multipleNameMatches) {
@@ -97,9 +125,9 @@ export function processAvailableMatchesByDirectory(data, {
   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);
@@ -150,19 +178,23 @@ function oopsNameCapitalizationMismatch(mode, {
     `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) {
-      if (name === match.name) {
-        return match.thing;
-      } else {
+      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, {
@@ -242,11 +274,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') {
@@ -257,19 +296,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:
@@ -279,7 +322,7 @@ function findHelper({
         }),
 
       matchByName:
-        prepareMatchByName(mode, {
+        prepareMatchByName(mode, fuzz, {
           byName,
           multipleNameMatches,
         }),
@@ -358,7 +401,7 @@ function findMixedHelper(config) {
       const multipleNameMatches = Object.create(null);
 
       for (const spec of specs) {
-        processAvailableMatchesByName(data, {
+        processAvailableMatchesByName(data, null, {
           ...spec,
 
           results: byName,
@@ -401,7 +444,7 @@ function findMixedHelper(config) {
       },
 
       matchByName:
-        prepareMatchByName(mode, {
+        prepareMatchByName(mode, null, {
           byName,
           multipleNameMatches,
         }),