mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 13:07:08 +00:00
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:
13
lib/www/server/lib/utils/FormatTimestamp.js
Normal file
13
lib/www/server/lib/utils/FormatTimestamp.js
Normal 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;
|
||||||
168
lib/www/server/lib/utils/deep.js
Normal file
168
lib/www/server/lib/utils/deep.js
Normal 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
|
||||||
|
};
|
||||||
47
lib/www/server/lib/utils/hsl.js
Normal file
47
lib/www/server/lib/utils/hsl.js
Normal 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
|
||||||
|
}
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
geometryAsString: require('./geometryAsString'),
|
...require('./deep'),
|
||||||
dms: require('./dms'),
|
dms: require('./dms'),
|
||||||
replaceMarkers: require('./replaceMarkers'),
|
...require('./flatEntries'),
|
||||||
flattenQCDefinitions: require('./flattenQCDefinitions'),
|
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'),
|
removeNulls: require('./removeNulls'),
|
||||||
logicalPath: require('./logicalPath'),
|
replaceMarkers: require('./replaceMarkers'),
|
||||||
ranges: require('./ranges'),
|
setContentDisposition: require('./setContentDisposition'),
|
||||||
|
throttle: require('./throttle'),
|
||||||
|
truncateText: require('./truncateText'),
|
||||||
unique: require('./unique'),
|
unique: require('./unique'),
|
||||||
setContentDisposition: require('./setContentDisposition')
|
unpack: require('./unpack'),
|
||||||
|
withParentProps: require('./withParentProps')
|
||||||
};
|
};
|
||||||
|
|||||||
11
lib/www/server/lib/utils/markdown.js
Normal file
11
lib/www/server/lib/utils/markdown.js
Normal 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 };
|
||||||
44
lib/www/server/lib/utils/preferencesλ.js
Normal file
44
lib/www/server/lib/utils/preferencesλ.js
Normal 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λ;
|
||||||
33
lib/www/server/lib/utils/throttle.js
Normal file
33
lib/www/server/lib/utils/throttle.js
Normal 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;
|
||||||
10
lib/www/server/lib/utils/truncateText.js
Normal file
10
lib/www/server/lib/utils/truncateText.js
Normal 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;
|
||||||
35
lib/www/server/lib/utils/unpack.js
Normal file
35
lib/www/server/lib/utils/unpack.js
Normal 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;
|
||||||
28
lib/www/server/lib/utils/withParentProps.js
Normal file
28
lib/www/server/lib/utils/withParentProps.js
Normal 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;
|
||||||
Reference in New Issue
Block a user