mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:47: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 = {
|
||||
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')
|
||||
};
|
||||
|
||||
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