« get me outta code hell

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:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/cli.js8
-rw-r--r--src/util/html.js6
-rw-r--r--src/util/replacer.js5
-rw-r--r--src/util/sugar.js97
-rw-r--r--src/util/urls.js21
-rw-r--r--src/util/wiki-data.js70
6 files changed, 142 insertions, 65 deletions
diff --git a/src/util/cli.js b/src/util/cli.js
index e8c8c79f..4c08c085 100644
--- a/src/util/cli.js
+++ b/src/util/cli.js
@@ -17,7 +17,7 @@ export const ENABLE_COLOR =
 const C = (n) =>
   ENABLE_COLOR ? (text) => `\x1b[${n}m${text}\x1b[0m` : (text) => text;
 
-export const color = {
+export const colors = {
   bright: C('1'),
   dim: C('2'),
   normal: C('22'),
@@ -335,8 +335,8 @@ export function fileIssue({
   topMessage = `This shouldn't happen.`,
 } = {}) {
   if (topMessage) {
-    console.error(color.red(`${topMessage} Please let the HSMusic developers know:`));
+    console.error(colors.red(`${topMessage} Please let the HSMusic developers know:`));
   }
-  console.error(color.red(`- https://hsmusic.wiki/feedback/`));
-  console.error(color.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`));
+  console.error(colors.red(`- https://hsmusic.wiki/feedback/`));
+  console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`));
 }
diff --git a/src/util/html.js b/src/util/html.js
index f0c7bfdf..282a52da 100644
--- a/src/util/html.js
+++ b/src/util/html.js
@@ -2,7 +2,7 @@
 
 import {inspect} from 'node:util';
 
-import {empty} from '#sugar';
+import {empty, typeAppearance} from '#sugar';
 import * as commonValidators from '#validators';
 
 // COMPREHENSIVE!
@@ -242,7 +242,7 @@ export class Tag {
       this.selfClosing &&
       !(value === null ||
         value === undefined ||
-        !Boolean(value) ||
+        !value ||
         Array.isArray(value) && value.filter(Boolean).length === 0)
     ) {
       throw new Error(`Tag <${this.tagName}> is self-closing but got content`);
@@ -633,7 +633,7 @@ export class Template {
 
   static validateDescription(description) {
     if (typeof description !== 'object') {
-      throw new TypeError(`Expected object, got ${typeof description}`);
+      throw new TypeError(`Expected object, got ${typeAppearance(description)}`);
     }
 
     if (description === null) {
diff --git a/src/util/replacer.js b/src/util/replacer.js
index c5289cc5..095ee060 100644
--- a/src/util/replacer.js
+++ b/src/util/replacer.js
@@ -5,9 +5,8 @@
 // function, which converts nodes parsed here into actual HTML, links, etc
 // for embedding in a wiki webpage.
 
-import {logError, logWarn} from '#cli';
 import * as html from '#html';
-import {escapeRegex} from '#sugar';
+import {escapeRegex, typeAppearance} from '#sugar';
 
 // Syntax literals.
 const tagBeginning = '[[';
@@ -408,7 +407,7 @@ export function postprocessHeadings(inputNodes) {
 
 export function parseInput(input) {
   if (typeof input !== 'string') {
-    throw new TypeError(`Expected input to be string, got ${input}`);
+    throw new TypeError(`Expected input to be string, got ${typeAppearance(input)}`);
   }
 
   try {
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 5b1f3193..2e724bae 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -6,7 +6,7 @@
 // It will likely only do exactly what I want it to, and only in the cases I
 // decided were relevant enough to 8other handling.
 
-import {color} from './cli.js';
+import {colors} from './cli.js';
 
 // Apparently JavaScript doesn't come with a function to split an array into
 // chunks! Weird. Anyway, this is an awesome place to use a generator, even
@@ -82,7 +82,7 @@ export function stitchArrays(keyToArray) {
   for (const [key, value] of Object.entries(keyToArray)) {
     if (value === null) continue;
     if (Array.isArray(value)) continue;
-    errors.push(new TypeError(`(${key}) Expected array or null, got ${value}`));
+    errors.push(new TypeError(`(${key}) Expected array or null, got ${typeAppearance(value)}`));
   }
 
   if (!empty(errors)) {
@@ -168,12 +168,24 @@ export function setIntersection(set1, set2) {
   return intersection;
 }
 
-export function filterProperties(obj, properties) {
-  const set = new Set(properties);
-  return Object.fromEntries(
-    Object
-      .entries(obj)
-      .filter(([key]) => set.has(key)));
+export function filterProperties(object, properties) {
+  if (typeof object !== 'object' || object === null) {
+    throw new TypeError(`Expected object to be an object, got ${typeAppearance(object)}`);
+  }
+
+  if (!Array.isArray(properties)) {
+    throw new TypeError(`Expected properties to be an array, got ${typeAppearance(properties)}`);
+  }
+
+  const filteredObject = {};
+
+  for (const property of properties) {
+    if (Object.hasOwn(object, property)) {
+      filteredObject[property] = object[property];
+    }
+  }
+
+  return filteredObject;
 }
 
 export function queue(array, max = 50) {
@@ -218,6 +230,16 @@ export function escapeRegex(string) {
   return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
 }
 
+// Gets the "look" of some arbitrary value. It's like typeof, but smarter.
+// Don't use this for actually validating types - it's only suitable for
+// inclusion in error messages.
+export function typeAppearance(value) {
+  if (value === null) return 'null';
+  if (value === undefined) return 'undefined';
+  if (Array.isArray(value)) return 'array';
+  return typeof value;
+}
+
 // Binds default values for arguments in a {key: value} type function argument
 // (typically the second argument, but may be overridden by providing a
 // [bindOpts.bindIndex] argument). Typically useful for preparing a function for
@@ -532,15 +554,17 @@ export function showAggregate(topError, {
   print = true,
 } = {}) {
   const recursive = (error, {level}) => {
-    let header = showTraces
+    let headerPart = showTraces
       ? `[${error.constructor.name || 'unnamed'}] ${
           error.message || '(no message)'
         }`
       : error instanceof AggregateError
       ? `[${error.message || '(no message)'}]`
       : error.message || '(no message)';
+
     if (showTraces) {
       const stackLines = error.stack?.split('\n');
+
       const stackLine = stackLines?.find(
         (line) =>
           line.trim().startsWith('at') &&
@@ -548,38 +572,44 @@ export function showAggregate(topError, {
           !line.includes('node:') &&
           !line.includes('<anonymous>')
       );
+
       const tracePart = stackLine
         ? '- ' +
           stackLine
             .trim()
             .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match))
         : '(no stack trace)';
-      header += ` ${color.dim(tracePart)}`;
-    }
-    const bar = level % 2 === 0 ? '\u2502' : color.dim('\u254e');
-    const head = level % 2 === 0 ? '\u257f' : color.dim('\u257f');
-
-    if (error instanceof AggregateError) {
-      return (
-        header +
-        '\n' +
-        error.errors
-          .map((error) => recursive(error, {level: level + 1}))
-          .flatMap((str) => str.split('\n'))
-          .map((line, i) => i === 0 ? ` ${head} ${line}` : ` ${bar} ${line}`)
-          .join('\n')
-      );
-    } else {
-      return header;
+
+      headerPart += ` ${colors.dim(tracePart)}`;
     }
+
+    const head1 = level % 2 === 0 ? '\u21aa' : colors.dim('\u21aa');
+    const bar1 = ' ';
+
+    const causePart =
+      (error.cause
+        ? recursive(error.cause, {level: level + 1})
+            .split('\n')
+            .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`)
+            .join('\n')
+        : '');
+
+    const head2 = level % 2 === 0 ? '\u257f' : colors.dim('\u257f');
+    const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e');
+
+    const aggregatePart =
+      (error instanceof AggregateError
+        ? error.errors
+            .map(error => recursive(error, {level: level + 1}))
+            .flatMap(str => str.split('\n'))
+            .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`)
+            .join('\n')
+        : '');
+
+    return [headerPart, causePart, aggregatePart].filter(Boolean).join('\n');
   };
 
-  const message =
-    (topError instanceof AggregateError
-      ? recursive(topError, {level: 0})
-      : (showTraces
-          ? topError.stack
-          : topError.toString()));
+  const message = recursive(topError, {level: 0});
 
   if (print) {
     console.error(message);
@@ -593,7 +623,8 @@ export function decorateErrorWithIndex(fn) {
     try {
       return fn(x, index, array);
     } catch (error) {
-      error.message = `(${color.yellow(`#${index + 1}`)}) ${error.message}`;
+      error.message = `(${colors.yellow(`#${index + 1}`)}) ${error.message}`;
+      error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index;
       throw error;
     }
   };
diff --git a/src/util/urls.js b/src/util/urls.js
index d2b303e9..11b9b8b0 100644
--- a/src/util/urls.js
+++ b/src/util/urls.js
@@ -237,27 +237,6 @@ export function getPagePathname({
     : to('localized.' + pagePath[0], ...pagePath.slice(1)));
 }
 
-export function getPagePathnameAcrossLanguages({
-  defaultLanguage,
-  languages,
-  pagePath,
-  urls,
-}) {
-  return withEntries(languages, entries => entries
-    .filter(([key, language]) => key !== 'default' && !language.hidden)
-    .map(([_key, language]) => [
-      language.code,
-      getPagePathname({
-        baseDirectory:
-          (language === defaultLanguage
-            ? ''
-            : language.code),
-        pagePath,
-        urls,
-      }),
-    ]));
-}
-
 // Needed for the rare path arguments which themselves contains one or more
 // slashes, e.g. for listings, with arguments like 'albums/by-name'.
 export function getPageSubdirectoryPrefix({
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index ad2f82fb..ac652b27 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -1,6 +1,6 @@
 // Utility functions for interacting with wiki data.
 
-import {accumulateSum, empty, stitchArrays, unique} from './sugar.js';
+import {accumulateSum, empty, unique} from './sugar.js';
 
 // Generic value operations
 
@@ -874,3 +874,71 @@ export function filterItemsForCarousel(items) {
     .filter(item => item.artTags.every(tag => !tag.isContentWarning))
     .slice(0, maxCarouselLayoutItems + 1);
 }
+
+// Ridiculous caching support nonsense
+
+export class TupleMap {
+  static maxNestedTupleLength = 25;
+
+  #store = [undefined, null, null, null];
+
+  #lifetime(value) {
+    if (Array.isArray(value) && value.length <= TupleMap.maxNestedTupleLength) {
+      return 'tuple';
+    } else if (
+      typeof value === 'object' && value !== null ||
+      typeof value === 'function'
+    ) {
+      return 'weak';
+    } else {
+      return 'strong';
+    }
+  }
+
+  #getSubstoreShallow(value, store) {
+    const lifetime = this.#lifetime(value);
+    const mapIndex = {weak: 1, strong: 2, tuple: 3}[lifetime];
+
+    let map = store[mapIndex];
+    if (map === null) {
+      map = store[mapIndex] =
+        (lifetime === 'weak' ? new WeakMap()
+       : lifetime === 'strong' ? new Map()
+       : lifetime === 'tuple' ? new TupleMap()
+       : null);
+    }
+
+    if (map.has(value)) {
+      return map.get(value);
+    } else {
+      const substore = [undefined, null, null, null];
+      map.set(value, substore);
+      return substore;
+    }
+  }
+
+  #getSubstoreDeep(tuple, store = this.#store) {
+    if (tuple.length === 0) {
+      return store;
+    } else {
+      const [first, ...rest] = tuple;
+      return this.#getSubstoreDeep(rest, this.#getSubstoreShallow(first, store));
+    }
+  }
+
+  get(tuple) {
+    const store = this.#getSubstoreDeep(tuple);
+    return store[0];
+  }
+
+  has(tuple) {
+    const store = this.#getSubstoreDeep(tuple);
+    return store[0] !== undefined;
+  }
+
+  set(tuple, value) {
+    const store = this.#getSubstoreDeep(tuple);
+    store[0] = value;
+    return value;
+  }
+}