« get me outta code hell

Merge branch 'preview' into listing-tweaks - 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-09 14:42:24 -0400
committer(quasar) nebula <qznebula@protonmail.com>2023-11-09 14:42:24 -0400
commitaa30c888ea2307931c555db474d709f520c551a8 (patch)
treeb23042b5b575862d83f401b5fa21f8b45f7988ff /src/util
parente71230340181a3b7b38ff05ba23504b264f5b26c (diff)
parentb62622d3cd8ffe1ed517ceb873d9352943c4a601 (diff)
Merge branch 'preview' into listing-tweaks
Diffstat (limited to 'src/util')
-rw-r--r--src/util/cli.js31
-rw-r--r--src/util/html.js4
-rw-r--r--src/util/sugar.js157
3 files changed, 155 insertions, 37 deletions
diff --git a/src/util/cli.js b/src/util/cli.js
index 4c08c085..973fef19 100644
--- a/src/util/cli.js
+++ b/src/util/cli.js
@@ -340,3 +340,34 @@ export function fileIssue({
   console.error(colors.red(`- https://hsmusic.wiki/feedback/`));
   console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`));
 }
+
+export async function logicalCWD() {
+  if (process.env.PWD) {
+    return process.env.PWD;
+  }
+
+  const {exec} = await import('node:child_process');
+  const {stat} = await import('node:fs/promises');
+
+  try {
+    await stat('/bin/sh');
+  } catch (error) {
+    // Not logical, so sad.
+    return process.cwd();
+  }
+
+  const proc = exec('/bin/pwd -L');
+
+  let output = '';
+  proc.stdout.on('data', buf => { output += buf; });
+
+  await new Promise(resolve => proc.on('exit', resolve));
+
+  return output.trim();
+}
+
+export async function logicalPathTo(target) {
+  const {relative} = await import('node:path');
+  const cwd = await logicalCWD();
+  return relative(cwd, target);
+}
diff --git a/src/util/html.js b/src/util/html.js
index 282a52da..5b6743e0 100644
--- a/src/util/html.js
+++ b/src/util/html.js
@@ -181,6 +181,10 @@ export function tags(content, attributes = null) {
   return new Tag(null, attributes, content);
 }
 
+export function normalize(content) {
+  return Tag.normalize(content);
+}
+
 export class Tag {
   #tagName = '';
   #content = null;
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 3e39e98f..9646be37 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -411,6 +411,18 @@ export function aggregateThrows(errorClass) {
   return {[openAggregate.errorClassSymbol]: errorClass};
 }
 
+// Helper function for allowing both (fn, aggregateOpts) and (aggregateOpts, fn)
+// in aggregate utilities.
+function _reorganizeAggregateArguments(arg1, arg2) {
+  if (typeof arg1 === 'function') {
+    return {fn: arg1, opts: arg2 ?? {}};
+  } else if (typeof arg2 === 'function') {
+    return {fn: arg2, opts: arg1 ?? {}};
+  } else {
+    throw new Error(`Expected a function`);
+  }
+}
+
 // Performs an ordinary array map with the given function, collating into a
 // results array (with errored inputs filtered out) and an error aggregate.
 //
@@ -420,15 +432,15 @@ export function aggregateThrows(errorClass) {
 // Note the aggregate property is the result of openAggregate(), still unclosed;
 // use aggregate.close() to throw the error. (This aggregate may be passed to a
 // parent aggregate: `parent.call(aggregate.close)`!)
-export function mapAggregate(array, fn, aggregateOpts) {
-  return _mapAggregate('sync', null, array, fn, aggregateOpts);
+export function mapAggregate(array, arg1, arg2) {
+  const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2);
+  return _mapAggregate('sync', null, array, fn, opts);
 }
 
-export function mapAggregateAsync(array, fn, {
-  promiseAll = Promise.all.bind(Promise),
-  ...aggregateOpts
-} = {}) {
-  return _mapAggregate('async', promiseAll, array, fn, aggregateOpts);
+export function mapAggregateAsync(array, arg1, arg2) {
+  const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2);
+  const {promiseAll = Promise.all.bind(Promise), ...remainingOpts} = opts;
+  return _mapAggregate('async', promiseAll, array, fn, remainingOpts);
 }
 
 // Helper function for mapAggregate which holds code common between sync and
@@ -462,15 +474,15 @@ export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) {
 // inputs to a particular output.
 //
 // As with mapAggregate, the returned aggregate property is not yet closed.
-export function filterAggregate(array, fn, aggregateOpts) {
-  return _filterAggregate('sync', null, array, fn, aggregateOpts);
+export function filterAggregate(array, arg1, arg2) {
+  const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2);
+  return _filterAggregate('sync', null, array, fn, opts);
 }
 
-export async function filterAggregateAsync(array, fn, {
-  promiseAll = Promise.all.bind(Promise),
-  ...aggregateOpts
-} = {}) {
-  return _filterAggregate('async', promiseAll, array, fn, aggregateOpts);
+export async function filterAggregateAsync(array, arg1, arg2) {
+  const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2);
+  const {promiseAll = Promise.all.bind(Promise), ...remainingOpts} = opts;
+  return _filterAggregate('async', promiseAll, array, fn, remainingOpts);
 }
 
 // Helper function for filterAggregate which holds code common between sync and
@@ -530,20 +542,17 @@ function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) {
 // Totally sugar function for opening an aggregate, running the provided
 // function with it, then closing the function and returning the result (if
 // there's no throw).
-export function withAggregate(aggregateOpts, fn) {
-  return _withAggregate('sync', aggregateOpts, fn);
+export function withAggregate(arg1, arg2) {
+  const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2);
+  return _withAggregate('sync', opts, fn);
 }
 
-export function withAggregateAsync(aggregateOpts, fn) {
-  return _withAggregate('async', aggregateOpts, fn);
+export function withAggregateAsync(arg1, arg2) {
+  const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2);
+  return _withAggregate('async', opts, fn);
 }
 
 export function _withAggregate(mode, aggregateOpts, fn) {
-  if (typeof aggregateOpts === 'function') {
-    fn = aggregateOpts;
-    aggregateOpts = {};
-  }
-
   const aggregate = openAggregate(aggregateOpts);
 
   if (mode === 'sync') {
@@ -628,27 +637,101 @@ export function showAggregate(topError, {
   }
 }
 
-export function decorateErrorWithIndex(fn) {
-  return (x, index, array) => {
+export function annotateError(error, ...callbacks) {
+  for (const callback of callbacks) {
+    error = callback(error) ?? error;
+  }
+
+  return error;
+}
+
+export function annotateErrorWithIndex(error, index) {
+  return Object.assign(error, {
+    [Symbol.for('hsmusic.annotateError.indexInSourceArray')]:
+      index,
+
+    message:
+      `(${colors.yellow(`#${index + 1}`)}) ` +
+      error.message,
+  });
+}
+
+export function annotateErrorWithFile(error, file) {
+  return Object.assign(error, {
+    [Symbol.for('hsmusic.annotateError.file')]:
+      file,
+
+    message:
+      error.message +
+      (error.message.includes('\n') ? '\n' : ' ') +
+      `(file: ${colors.bright(colors.blue(file))})`,
+  });
+}
+
+export function asyncAdaptiveDecorateError(fn, callback) {
+  if (typeof callback !== 'function') {
+    throw new Error(`Expected callback to be a function, got ${typeAppearance(callback)}`);
+  }
+
+  const syncDecorated = function (...args) {
     try {
-      return fn(x, index, array);
-    } catch (error) {
-      error.message = `(${colors.yellow(`#${index + 1}`)}) ${error.message}`;
-      error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index;
-      throw error;
+      return fn(...args);
+    } catch (caughtError) {
+      throw callback(caughtError, ...args);
     }
   };
-}
 
-export function decorateErrorWithCause(fn, cause) {
-  return (...args) => {
+  const asyncDecorated = async function(...args) {
     try {
-      return fn(...args);
-    } catch (error) {
-      error.cause = cause;
-      throw error;
+      return await fn(...args);
+    } catch (caughtError) {
+      throw callback(caughtError);
     }
   };
+
+  syncDecorated.async = asyncDecorated;
+
+  return syncDecorated;
+}
+
+export function decorateError(fn, callback) {
+  return asyncAdaptiveDecorateError(fn, callback);
+}
+
+export function asyncDecorateError(fn, callback) {
+  return asyncAdaptiveDecorateError(fn, callback).async;
+}
+
+export function decorateErrorWithAnnotation(fn, ...annotationCallbacks) {
+  return asyncAdaptiveDecorateError(fn,
+    (caughtError, ...args) =>
+      annotateError(caughtError,
+        ...annotationCallbacks
+          .map(callback => error => callback(error, ...args))));
+}
+
+export function decorateErrorWithIndex(fn) {
+  return decorateErrorWithAnnotation(fn,
+    (caughtError, _value, index) =>
+      annotateErrorWithIndex(caughtError, index));
+}
+
+export function decorateErrorWithCause(fn, cause) {
+  return asyncAdaptiveDecorateError(fn,
+    (caughtError) =>
+      Object.assign(caughtError, {cause}));
+}
+
+export function asyncDecorateErrorWithAnnotation(fn, ...annotationCallbacks) {
+  return decorateErrorWithAnnotation(fn, ...annotationCallbacks).async;
+}
+
+export function asyncDecorateErrorWithIndex(fn) {
+  return decorateErrorWithIndex(fn).async;
+}
+
+export function asyncDecorateErrorWithCause(fn, cause) {
+  return decorateErrorWithCause(fn, cause).async;
 }
 
 export function conditionallySuppressError(conditionFn, callbackFn) {