This repository has been archived by the owner on Feb 7, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathfmt.mjs
137 lines (128 loc) · 4.54 KB
/
fmt.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* notion-enhancer: api
* (c) 2021 dragonwocky <[email protected]> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
'use strict';
/**
* helpers for formatting or parsing text
* @namespace fmt
*/
import { fs } from './index.mjs';
/**
* transform a heading into a slug (a lowercase alphanumeric string separated by hyphens),
* e.g. for use as an anchor id
* @param {string} heading - the original heading to be slugified
* @param {Set<string>=} slugs - a list of pre-generated slugs to avoid duplicates
* @returns {string} the generated slug
*/
export const slugger = (heading, slugs = new Set()) => {
heading = heading
.replace(/\s/g, '-')
.replace(/[^A-Za-z0-9-_]/g, '')
.toLowerCase();
let i = 0,
slug = heading;
while (slugs.has(slug)) {
i++;
slug = `${heading}-${i}`;
}
return slug;
};
/**
* generate a reasonably random uuidv4 string. uses crypto implementation if available
* (from https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid)
* @returns {string} a uuidv4
*/
export const uuidv4 = () => {
if (crypto?.randomUUID) return crypto.randomUUID();
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
};
/**
* log-based shading of an rgb color, from
* https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
* @param {number} shade - a decimal amount to shade the color.
* 1 = white, 0 = the original color, -1 = black
* @param {string} color - the rgb color
* @returns {string} the shaded color
*/
export const rgbLogShade = (shade, color) => {
const int = parseInt,
round = Math.round,
[a, b, c, d] = color.split(','),
t = shade < 0 ? 0 : shade * 255 ** 2,
p = shade < 0 ? 1 + shade : 1 - shade;
return (
'rgb' +
(d ? 'a(' : '(') +
round((p * int(a[3] == 'a' ? a.slice(5) : a.slice(4)) ** 2 + t) ** 0.5) +
',' +
round((p * int(b) ** 2 + t) ** 0.5) +
',' +
round((p * int(c) ** 2 + t) ** 0.5) +
(d ? ',' + d : ')')
);
};
/**
* pick a contrasting color e.g. for text on a variable color background
* using the hsp (perceived brightness) constants from http://alienryderflex.com/hsp.html
* @param {number} r - red (0-255)
* @param {number} g - green (0-255)
* @param {number} b - blue (0-255)
* @returns {string} the contrasting rgb color, white or black
*/
export const rgbContrast = (r, g, b) => {
return Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)) > 165.75
? 'rgb(0,0,0)'
: 'rgb(255,255,255)';
};
const patterns = {
alphanumeric: /^[\w\.-]+$/,
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
semver:
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i,
email:
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i,
url: /^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,64}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i,
color: /^(?:#|0x)(?:[a-f0-9]{3}|[a-f0-9]{6})\b|(?:rgb|hsl)a?\([^\)]*\)$/i,
};
function test(str, pattern) {
const match = str.match(pattern);
return !!(match && match.length);
}
/**
* test the type of a value. unifies builtin, regex, and environment/api checks
* @param {unknown} value - the value to check
* @param {string|string[]} type - the type the value should be or a list of allowed values
* @returns {boolean} whether or not the value matches the type
*/
export const is = async (value, type, { extension = '' } = {}) => {
extension = !value || !value.endsWith || value.endsWith(extension);
if (Array.isArray(type)) {
return type.includes(value);
}
switch (type) {
case 'array':
return Array.isArray(value);
case 'object':
return value && typeof value === 'object' && !Array.isArray(value);
case 'undefined':
case 'boolean':
case 'number':
return typeof value === type && extension;
case 'string':
return typeof value === type && extension;
case 'alphanumeric':
case 'uuid':
case 'semver':
case 'email':
case 'url':
case 'color':
return typeof value === 'string' && test(value, patterns[type]) && extension;
case 'file':
return typeof value === 'string' && value && (await fs.isFile(value)) && extension;
}
return false;
};