« 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.js37
-rw-r--r--src/util/find.js2
-rw-r--r--src/util/sugar.js200
3 files changed, 191 insertions, 48 deletions
diff --git a/src/util/cli.js b/src/util/cli.js
index 7f84be7c..b6335726 100644
--- a/src/util/cli.js
+++ b/src/util/cli.js
@@ -3,18 +3,47 @@
 // A 8unch of these depend on process.stdout 8eing availa8le, so they won't
 // work within the 8rowser.
 
+const { process } = globalThis;
+
+export const ENABLE_COLOR = process && (
+    (process.env.CLICOLOR_FORCE && process.env.CLICOLOR_FORCE === '1')
+    ?? (process.env.CLICOLOR && process.env.CLICOLOR === '1' && process.stdout.hasColors && process.stdout.hasColors())
+    ?? (process.stdout.hasColors ? process.stdout.hasColors() : true));
+
+const C = n => (ENABLE_COLOR
+    ? text => `\x1b[${n}m${text}\x1b[0m`
+    : text => text);
+
+export const color = {
+    bright: C('1'),
+    dim: C('2'),
+    black: C('30'),
+    red: C('31'),
+    green: C('32'),
+    yellow: C('33'),
+    blue: C('34'),
+    magenta: C('35'),
+    cyan: C('36'),
+    white: C('37')
+};
+
 const logColor = color => (literals, ...values) => {
     const w = s => process.stdout.write(s);
-    w(`\x1b[${color}m`);
+    const wc = text => {
+        if (ENABLE_COLOR) w(text);
+    };
+
+    wc(`\x1b[${color}m`);
     for (let i = 0; i < literals.length; i++) {
         w(literals[i]);
         if (values[i] !== undefined) {
-            w(`\x1b[1m`);
+            wc(`\x1b[1m`);
             w(String(values[i]));
-            w(`\x1b[0;${color}m`);
+            wc(`\x1b[0;${color}m`);
         }
     }
-    w(`\x1b[0m\n`);
+    wc(`\x1b[0m`);
+    w('\n');
 };
 
 export const logInfo = logColor(2);
diff --git a/src/util/find.js b/src/util/find.js
index 5f69bbec..423046b3 100644
--- a/src/util/find.js
+++ b/src/util/find.js
@@ -7,7 +7,7 @@ function findHelper(keys, dataProp, findFns = {}) {
     const byDirectory = findFns.byDirectory || matchDirectory;
     const byName = findFns.byName || matchName;
 
-    const keyRefRegex = new RegExp(`^((${keys.join('|')}):)?(.*)$`);
+    const keyRefRegex = new RegExp(`^((${keys.join('|')}):(?:\S))?(.*)$`);
 
     return (fullRef, {wikiData}) => {
         if (!fullRef) return null;
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 38c8047f..219c3eec 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -6,6 +6,8 @@
 // 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';
+
 // 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
 // though we don't really make use of the 8enefits of generators any time we
@@ -33,11 +35,6 @@ export const unique = arr => Array.from(new Set(arr));
 // Stolen from jq! Which pro8a8ly stole the concept from other places. Nice.
 export const withEntries = (obj, fn) => Object.fromEntries(fn(Object.entries(obj)));
 
-// Nothin' more to it than what it says. Runs a function in-place. Provides an
-// altern8tive syntax to the usual IIFEs (e.g. (() => {})()) when you want to
-// open a scope and run some statements while inside an existing expression.
-export const call = fn => fn();
-
 export function queue(array, max = 50) {
     if (max === 0) {
         return array.map(fn => fn());
@@ -133,14 +130,33 @@ export function openAggregate({
         }
     };
 
+    aggregate.wrapAsync = fn => (...args) => {
+        return fn(...args).then(
+            value => value,
+            error => {
+                errors.push(error);
+                return (typeof returnOnFail === 'function'
+                    ? returnOnFail(...args)
+                    : returnOnFail);
+            });
+    };
+
     aggregate.call = (fn, ...args) => {
         return aggregate.wrap(fn)(...args);
     };
 
+    aggregate.callAsync = (fn, ...args) => {
+        return aggregate.wrapAsync(fn)(...args);
+    };
+
     aggregate.nest = (...args) => {
         return aggregate.call(() => withAggregate(...args));
     };
 
+    aggregate.nestAsync = (...args) => {
+        return aggregate.callAsync(() => withAggregateAsync(...args));
+    };
+
     aggregate.map = (...args) => {
         const parent = aggregate;
         const { result, aggregate: child } = mapAggregate(...args);
@@ -148,6 +164,13 @@ export function openAggregate({
         return result;
     };
 
+    aggregate.mapAsync = async (...args) => {
+        const parent = aggregate;
+        const { result, aggregate: child } = await mapAggregateAsync(...args);
+        parent.call(child.close);
+        return result;
+    };
+
     aggregate.filter = (...args) => {
         const parent = aggregate;
         const { result, aggregate: child } = filterAggregate(...args);
@@ -183,6 +206,19 @@ export function aggregateThrows(errorClass) {
 // 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 mapAggregateAsync(array, fn, {
+    promiseAll = Promise.all,
+    ...aggregateOpts
+} = {}) {
+    return _mapAggregate('async', promiseAll, array, fn, aggregateOpts);
+}
+
+// Helper function for mapAggregate which holds code common between sync and
+// async versions.
+export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) {
     const failureSymbol = Symbol();
 
     const aggregate = openAggregate({
@@ -190,10 +226,16 @@ export function mapAggregate(array, fn, aggregateOpts) {
         ...aggregateOpts
     });
 
-    const result = array.map(aggregate.wrap(fn))
-        .filter(value => value !== failureSymbol);
-
-    return {result, aggregate};
+    if (mode === 'sync') {
+        const result = array.map(aggregate.wrap(fn))
+            .filter(value => value !== failureSymbol);
+        return {result, aggregate};
+    } else {
+        return promiseAll(array.map(aggregate.wrapAsync(fn))).then(values => {
+            const result = values.filter(value => value !== failureSymbol);
+            return {result, aggregate};
+        });
+    }
 }
 
 // Performs an ordinary array filter with the given function, collating into a
@@ -204,6 +246,19 @@ export function mapAggregate(array, fn, aggregateOpts) {
 //
 // 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 async function filterAggregateAsync(array, fn, {
+    promiseAll = Promise.all,
+    ...aggregateOpts
+} = {}) {
+    return _filterAggregate('async', promiseAll, array, fn, aggregateOpts);
+}
+
+// Helper function for filterAggregate which holds code common between sync and
+// async versions.
+function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) {
     const failureSymbol = Symbol();
 
     const aggregate = openAggregate({
@@ -211,62 +266,121 @@ export function filterAggregate(array, fn, aggregateOpts) {
         ...aggregateOpts
     });
 
-    const result = array.map(aggregate.wrap((x, ...rest) => ({
-        input: x,
-        output: fn(x, ...rest)
-    })))
-        .filter(value => {
-            // Filter out results which match the failureSymbol, i.e. errored
-            // inputs.
-            if (value === failureSymbol) return false;
-
-            // Always keep results which match the overridden returnOnFail
-            // value, if provided.
-            if (value === aggregateOpts.returnOnFail) return true;
-
-            // Otherwise, filter according to the returned value of the wrapped
-            // function.
-            return value.output;
-        })
-        .map(value => {
-            // Then turn the results back into their corresponding input, or, if
-            // provided, the overridden returnOnFail value.
-            return (value === aggregateOpts.returnOnFail
-                ? value
-                : value.input);
-        });
+    function filterFunction(value) {
+        // Filter out results which match the failureSymbol, i.e. errored
+        // inputs.
+        if (value === failureSymbol) return false;
+
+        // Always keep results which match the overridden returnOnFail
+        // value, if provided.
+        if (value === aggregateOpts.returnOnFail) return true;
+
+        // Otherwise, filter according to the returned value of the wrapped
+        // function.
+        return value.output;
+    }
+
+    function mapFunction(value) {
+        // Then turn the results back into their corresponding input, or, if
+        // provided, the overridden returnOnFail value.
+        return (value === aggregateOpts.returnOnFail
+            ? value
+            : value.input);
+    }
+
+    function wrapperFunction(x, ...rest) {
+        return {
+            input: x,
+            output: fn(x, ...rest)
+        };
+    }
 
-    return {result, aggregate};
+    if (mode === 'sync') {
+        const result = array
+            .map(aggregate.wrap((input, index, array) => {
+                const output = fn(input, index, array);
+                return {input, output};
+            }))
+            .filter(filterFunction)
+            .map(mapFunction);
+
+        return {result, aggregate};
+    } else {
+        return promiseAll(array.map(aggregate.wrapAsync(async (input, index, array) => {
+            const output = await fn(input, index, array);
+            return {input, output};
+        }))).then(values => {
+            const result = values
+                .filter(filterFunction)
+                .map(mapFunction);
+
+            return {result, aggregate};
+        });
+    }
 }
 
 // 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 withAggregateAsync(aggregateOpts, fn) {
+    return _withAggregate('async', aggregateOpts, fn);
+}
+
+export function _withAggregate(mode, aggregateOpts, fn) {
     if (typeof aggregateOpts === 'function') {
         fn = aggregateOpts;
         aggregateOpts = {};
     }
 
     const aggregate = openAggregate(aggregateOpts);
-    const result = fn(aggregate);
-    aggregate.close();
-    return result;
+
+    if (mode === 'sync') {
+        const result = fn(aggregate);
+        aggregate.close();
+        return result;
+    } else {
+        return fn(aggregate).then(result => {
+            aggregate.close();
+            return result;
+        });
+    }
 }
 
-export function showAggregate(topError) {
-    const recursive = error => {
-        const header = `[${error.constructor.name || 'unnamed'}] ${error.message || '(no message)'}`;
+export function showAggregate(topError, {pathToFile = p => p} = {}) {
+    const recursive = (error, {level}) => {
+        const stackLines = error.stack?.split('\n');
+        const stackLine = stackLines?.find(line =>
+            line.trim().startsWith('at')
+            && !line.includes('sugar')
+            && !line.includes('node:internal'));
+        const tracePart = (stackLine
+            ? '- ' + stackLine.trim().replace(/file:\/\/(.*\.js)/, (match, pathname) => pathToFile(pathname))
+            : '(no stack trace)');
+
+        const header = `[${error.constructor.name || 'unnamed'}] ${error.message || '(no message)'} ${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(recursive)
+                .map(error => recursive(error, {level: level + 1}))
                 .flatMap(str => str.split('\n'))
-                .map(line => ` | ` + line)
+                .map((line, i, lines) => (i === 0
+                    ? ` ${head} ${line}`
+                    : ` ${bar} ${line}`))
                 .join('\n'));
         } else {
             return header;
         }
     };
 
-    console.log(recursive(topError));
+    console.error(recursive(topError, {level: 0}));
 }