Bring all the lib/utils from the frontend to the backend.

The idea being that eventually we will symlink the lib/utils
directory so that the same routines are available on both the
frontend and the backend.
This commit is contained in:
D. Berge
2023-11-13 18:11:58 +01:00
parent 09fb653812
commit 313e9687bd
10 changed files with 406 additions and 6 deletions

View File

@@ -0,0 +1,13 @@
function FormatTimestamp (str) {
const d = new Date(str);
if (isNaN(d)) {
return str;
} else {
// Get rid of milliseconds
return d.toISOString().substring(0,19)+"Z";
}
}
module.exports = FormatTimestamp;

View File

@@ -0,0 +1,168 @@
/** Compare two possibly complex values for
* loose equality, going as deep as required in the
* case of complex objects.
*/
function deepCompare (a, b) {
if (typeof a == "object" && typeof b == "object") {
return !Object.entries(a).some( ([k, v]) => !deepCompare(v, b[k])) &&
!Object.entries(b).some( ([k, v]) => !deepCompare(v, a[k]));
} else {
return a == b;
}
}
/** Compare two possibly complex values for
* strict equality.
*/
function deepEqual (a, b) {
if (typeof a === "object" && typeof b === "object") {
return !Object.entries(a).some( ([k, v]) => !deepEqual(v, b[k])) &&
!Object.entries(b).some( ([k, v]) => !deepEqual(v, a[k]));
} else {
return a === b;
}
}
/** Traverses an object and sets a nested value.
*
* Example:
*
* const obj = {a: {b: {c: "X"} } }
* deepSet(obj, ["a", "b", "c"], "d")
* → {a: {b: {c: "d"} } }
*
* This would be the equivalent of:
*
* obj?.a?.b?.c = "d";
*
* Except that the above is not a legal expression.
*
* If a non-leaf property does not exist, this function
* creates it as an empty object ({}) and keeps traversing.
*
* The last member of `path` may be `null`, in which case,
* if the object pointed to by the next to last member is
* an array, an insert operation will take place.
*
*/
function deepSet (obj, path, value) {
const key = path.shift();
if (!path.length) {
if (key === null && Array.isArray(obj)) {
obj.push(value);
} else {
obj[key] = value;
}
} else {
if (!Object.hasOwn(obj, key)) {
obj[key] = {};
}
deepSet(obj[key], path, value);
}
}
/** Returns a nested property.
*
* Example:
*
* const obj = {a: {b: {c: "d"} } }
* deepSet(obj, ["a", "b", "c"])
* → "d"
*
* If `path` is known in advance, this is effectively
* the same as:
*
* obj?.a?.b?.c
*
* This might be useful when `path` is dynamic.
*/
function deepValue (obj, path) {
if (obj !== undefined) {
const key = path.shift();
if (!path.length) {
if (key === undefined) {
return obj;
} else {
return obj[key];
}
} else {
return deepValue(obj[key], path);
}
}
}
// Copied from:
// https://gomakethings.com/how-to-deep-merge-arrays-and-objects-with-javascript/
/*!
* Deep merge two or more objects or arrays.
* (c) 2023 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {*} ...objs The arrays or objects to merge
* @returns {*} The merged arrays or objects
*/
function deepMerge (...objs) {
/**
* Get the object type
* @param {*} obj The object
* @return {String} The object type
*/
function getType (obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
/**
* Deep merge two objects
* @return {Object}
*/
function mergeObj (clone, obj) {
for (let [key, value] of Object.entries(obj)) {
let type = getType(value);
if (clone[key] !== undefined && getType(clone[key]) === type && ['array', 'object'].includes(type)) {
clone[key] = deepMerge(clone[key], value);
} else {
clone[key] = structuredClone(value);
}
}
}
// Create a clone of the first item in the objs array
let clone = structuredClone(objs.shift());
// Loop through each item
for (let obj of objs) {
// Get the object type
let type = getType(obj);
// If the current item isn't the same type as the clone, replace it
if (getType(clone) !== type) {
clone = structuredClone(obj);
continue;
}
// Otherwise, merge
if (type === 'array') {
// Replace old array with new
clone = [...structuredClone(obj)];
} else if (type === 'object') {
mergeObj(clone, obj);
} else {
clone = obj;
}
}
return clone;
}
module.exports = {
deepCompare,
deepEqual,
deepSet,
deepValue,
deepMerge
};

View File

@@ -0,0 +1,47 @@
/** Return an HSL colour as a function of an input value
* `str`.
*
* Consider using as getHSL.bind(this) in Vue components
* in order to get access to the Vuetify theme configuration.
*/
function getHSL (str, saturation = 1, lightness = 0.25, offset = 0) {
function getHash (v) {
if (typeof (v??false)[Symbol.iterator] != "function") {
// Not an iterable, make it one
v = String(v);
}
return Math.abs([...v, ..." "].reduce( (acc, cur) => String(cur).charCodeAt(0) + ((acc << 5) - acc), 0 ));
}
const h = (getHash(str) + offset) % 360;
const s = saturation * 100;
const l = this?.$vuetify?.theme?.isDark
? (1-lightness) * 100
: lightness * 100;
return {h, s, l};
}
/** Return a CSS hsl() or hsla() colour
* representation as a function of an input value.
*
* Consider using as getHSLColourFor.bind(this) See
* note for getHSL() above.
*/
function getHSLColourFor (str, opacity = 1, saturation, lightness, offset) {
const _getHSL = getHSL.bind(this);
const {h, s, l} = _getHSL(str, saturation, lightness, offset);
if (opacity == 1) {
return `hsl(${h},${s}%,${l}%)`;
} else {
return `hsla(${h},${s}%,${l}%, ${opacity})`;
}
}
module.exports = {
getHSL,
getHSLColourFor
}

View File

@@ -1,13 +1,24 @@
module.exports = {
geometryAsString: require('./geometryAsString'),
...require('./deep'),
dms: require('./dms'),
replaceMarkers: require('./replaceMarkers'),
...require('./flatEntries'),
flattenQCDefinitions: require('./flattenQCDefinitions'),
deepMerge: require('./deepMerge'),
FormatTimestamp: require('./FormatTimestamp'),
geometryAsString: require('./geometryAsString'),
...require('./hsl'),
...require('./logicalPath'), // FIXME Breaking change (used to be logicalPath.…)
logicalPath: require('./logicalPath'), // NOTE For compatibility, see above
...require('./markdown'),
preferencesλ: require('./preferencesλ'),
...require('./ranges'), // FIXME Breaking change (used to be ranges.…)
ranges: require('./ranges'), // NOTE For compatibility, see above.
removeNulls: require('./removeNulls'),
logicalPath: require('./logicalPath'),
ranges: require('./ranges'),
replaceMarkers: require('./replaceMarkers'),
setContentDisposition: require('./setContentDisposition'),
throttle: require('./throttle'),
truncateText: require('./truncateText'),
unique: require('./unique'),
setContentDisposition: require('./setContentDisposition')
unpack: require('./unpack'),
withParentProps: require('./withParentProps')
};

View File

@@ -0,0 +1,11 @@
const { marked, parseInline } = require('marked');
function markdown (str) {
return marked(String(str));
}
function markdownInline (str) {
return parseInline(String(str));
}
module.exports = { markdown, markdownInline };

View File

@@ -0,0 +1,44 @@
/** Extract preferences by prefix.
*
* This function returns a lambda which, given
* a key or a prefix, extracts the relevant
* preferences from the designated preferences
* store.
*
* For instance, assume preferences = {
* "a.b.c.d": 1,
* "a.b.e.f": 2,
* "g.h": 3
* }
*
* And λ = preferencesλ(preferences). Then:
*
* λ("a.b") → { "a.b.c.d": 1, "a.b.e.f": 2 }
* λ("a.b.e.f") → { "a.b.e.f": 2 }
* λ("g.x", {"g.x.": 99}) → { "g.x.": 99 }
* λ("a.c", {"g.x.": 99}) → { "g.x.": 99 }
*
* Note from the last two examples that a default value
* may be provided and will be returned if a key does
* not exist or is not searched for.
*/
function preferencesλ (preferences) {
return function (key, defaults={}) {
const keys = Object.keys(preferences).filter(str => str.startsWith(key+".") || str == key);
const settings = {...defaults};
for (const str of keys) {
const k = str == key ? str : str.substring(key.length+1);
const v = preferences[str];
settings[k] = v;
}
return settings;
}
}
module.exports = preferencesλ;

View File

@@ -0,0 +1,33 @@
/**
* Throttle a function call.
*
* It delays `callback` by `delay` ms and ignores any
* repeated calls from `caller` within at most `maxWait`
* milliseconds.
*
* Used to react to server events in cases where we get
* a separate notification for each row of a bulk update.
*/
function throttle (callback, caller, delay = 100, maxWait = 500) {
const schedule = async () => {
caller.triggeredAt = Date.now();
caller.timer = setTimeout(async () => {
await callback();
caller.timer = null;
}, delay);
}
if (!caller.timer) {
schedule();
} else {
const elapsed = Date.now() - caller.triggeredAt;
if (elapsed > maxWait) {
cancelTimeout(caller.timer);
schedule();
}
}
}
module.exports = throttle;

View File

@@ -0,0 +1,10 @@
function truncateText (text, length=20) {
if (text?.length <= length) {
return text;
} else {
return text.slice(0, length/2)+"…"+text.slice(-(length/2));
}
}
module.exports = truncateText;

View File

@@ -0,0 +1,35 @@
/** Unpacks attributes from array items.
*
* At it simplest, given an array of objects,
* the call unpack(rows, "x") returns an array
* of the "x" attribute of every item in rows.
*
* `key` may also be:
*
* - a function with the signature
* (Object) => any
* the result of applying the function to
* the object will be used as the unpacked
* value.
*
* - an array of strings, functions or other
* arrays. In this case, it does a recursive
* fold operation. NOTE: it mutates `key`.
*
*/
function unpack(rows, key) {
if (typeof key === "function") {
return rows && rows.map( row => key(row) );
} else if (Array.isArray(key)) {
const car = key.shift();
if (key.length) {
return unpack(unpack(rows, car), key);
} else {
return unpack(rows, car);
}
} else {
return rows && rows.map( row => row?.[key] );
}
};
module.exports = unpack;

View File

@@ -0,0 +1,28 @@
function withParentProps(item, parent, childrenKey, prop, currentValue) {
if (!Array.isArray(parent)) {
return;
}
let currentPropValue = currentValue || parent[prop];
for (const entry of parent) {
if (entry[prop]) {
currentPropValue = entry[prop];
}
if (entry === item) {
return [item, currentPropValue];
}
if (entry[childrenKey]) {
const res = withParentProps(item, entry[childrenKey], childrenKey, prop, currentPropValue);
if (res[1]) {
return res;
}
}
}
return [];
}
module.exports = withParentProps;