@jarenjs/core 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +800 -0
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/types/array.d.ts +28 -0
- package/dist/types/bigint.d.ts +5 -0
- package/dist/types/dates.d.ts +79 -0
- package/dist/types/float.d.ts +32 -0
- package/dist/types/function.d.ts +22 -0
- package/dist/types/index.d.ts +119 -0
- package/dist/types/integer.d.ts +24 -0
- package/dist/types/math/float64.d.ts +121 -0
- package/dist/types/math/index.d.ts +5 -0
- package/dist/types/math/int32.d.ts +41 -0
- package/dist/types/math/vec2f64.d.ts +346 -0
- package/dist/types/math/vec2i32.d.ts +43 -0
- package/dist/types/math/vec3f64.d.ts +61 -0
- package/dist/types/number.d.ts +39 -0
- package/dist/types/object.d.ts +44 -0
- package/dist/types/scan.d.ts +64 -0
- package/dist/types/string.d.ts +65 -0
- package/dist/types/text/base64.d.ts +4 -0
- package/dist/types/text/basic.d.ts +5 -0
- package/dist/types/text/email.d.ts +3 -0
- package/dist/types/text/host.d.ts +14 -0
- package/dist/types/text/i18n.d.ts +13 -0
- package/dist/types/text/identifiers.d.ts +5 -0
- package/dist/types/text/index.d.ts +8 -0
- package/dist/types/text/iregexp.d.ts +36 -0
- package/dist/types/text/misc.d.ts +4 -0
- package/dist/types/text/punycode.d.ts +86 -0
- package/package.json +101 -0
- package/src/array.js +57 -0
- package/src/bigint.js +30 -0
- package/src/dates.js +371 -0
- package/src/float.js +107 -0
- package/src/function.js +56 -0
- package/src/index.js +223 -0
- package/src/integer.js +77 -0
- package/src/math/float64.js +316 -0
- package/src/math/index.js +5 -0
- package/src/math/int32.js +235 -0
- package/src/math/vec2f64.js +706 -0
- package/src/math/vec2i32.js +250 -0
- package/src/math/vec3f64.js +225 -0
- package/src/number.js +63 -0
- package/src/object.js +240 -0
- package/src/scan.js +96 -0
- package/src/string.js +194 -0
- package/src/text/base64.js +54 -0
- package/src/text/basic.js +23 -0
- package/src/text/email.js +61 -0
- package/src/text/host.js +335 -0
- package/src/text/i18n.js +294 -0
- package/src/text/identifiers.js +27 -0
- package/src/text/index.js +11 -0
- package/src/text/iregexp.js +308 -0
- package/src/text/misc.js +19 -0
- package/src/text/punycode.js +407 -0
package/src/object.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isFn,
|
|
5
|
+
isScalarType,
|
|
6
|
+
isBooleanType,
|
|
7
|
+
isTypedArray,
|
|
8
|
+
} from './index.js';
|
|
9
|
+
|
|
10
|
+
const hasOwn = Object.hasOwn;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Deep equality comparison for arbitrary values.
|
|
14
|
+
*
|
|
15
|
+
* Generic JavaScript equality: understands Maps, Sets, RegExps,
|
|
16
|
+
* functions, typed arrays and class instances (constructors must
|
|
17
|
+
* match). Not the same as `equalsJson`, which compares JSON values
|
|
18
|
+
* only and is the hot-path variant — keep both.
|
|
19
|
+
* @param {any} target
|
|
20
|
+
* @param {any} source
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function equalsDeep(target, source) {
|
|
24
|
+
if (target === source) return true;
|
|
25
|
+
if (target == null) return false;
|
|
26
|
+
if (source == null) return false;
|
|
27
|
+
if (isBooleanType(target)) return false;
|
|
28
|
+
if (isBooleanType(source)) return false;
|
|
29
|
+
|
|
30
|
+
if (isFn(target))
|
|
31
|
+
return target.toString() === source.toString();
|
|
32
|
+
|
|
33
|
+
if (isScalarType(target))
|
|
34
|
+
return false;
|
|
35
|
+
|
|
36
|
+
if (target.constructor !== source.constructor)
|
|
37
|
+
return false;
|
|
38
|
+
|
|
39
|
+
if (target.constructor === Object) {
|
|
40
|
+
const tks = Object.keys(target);
|
|
41
|
+
const sks = Object.keys(source);
|
|
42
|
+
if (tks.length !== sks.length)
|
|
43
|
+
return false;
|
|
44
|
+
for (let i = 0; i < tks.length; ++i) {
|
|
45
|
+
const key = tks[i];
|
|
46
|
+
if (!equalsDeep(target[key], source[key]))
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (target.constructor === Map) {
|
|
53
|
+
if (target.size !== source.size)
|
|
54
|
+
return false;
|
|
55
|
+
for (const [key, value] of target) {
|
|
56
|
+
if (source.has(key) === false)
|
|
57
|
+
return false;
|
|
58
|
+
if (!equalsDeep(value, source.get(key)))
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (target.constructor === Array) {
|
|
65
|
+
if (target.length !== source.length)
|
|
66
|
+
return false;
|
|
67
|
+
for (let i = 0; i < target.length; ++i) {
|
|
68
|
+
if (!equalsDeep(target[i], source[i]))
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (target.constructor === Set) {
|
|
75
|
+
if (target.size !== source.size)
|
|
76
|
+
return false;
|
|
77
|
+
for (const value of target) {
|
|
78
|
+
if (source.has(value) === false)
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (target.constructor === RegExp) {
|
|
85
|
+
return target.toString() === source.toString();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isTypedArray(target)) {
|
|
89
|
+
if (target.length !== source.length)
|
|
90
|
+
return false;
|
|
91
|
+
for (let i = 0; i < target.length; ++i) {
|
|
92
|
+
if (target[i] !== source[i])
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// we could test for instance of Array, Map and Set in order
|
|
99
|
+
// to differentiate between types of equality.. but we dont.
|
|
100
|
+
const tkeys = Object.keys(target);
|
|
101
|
+
const skeys = Object.keys(source);
|
|
102
|
+
if (tkeys.length !== skeys.length) return false;
|
|
103
|
+
if (tkeys.length === 0) return true;
|
|
104
|
+
for (let i = 0; i < tkeys.length; ++i) {
|
|
105
|
+
const key = tkeys[i];
|
|
106
|
+
if (!equalsDeep(target[key], source[key]))
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Structural equality of two JSON values per RFC 9535 section 2.3.5.2.2.
|
|
114
|
+
*
|
|
115
|
+
* JSON-only equality: objects compare by own enumerable keys, arrays by
|
|
116
|
+
* index, primitives by `===` (so `1 === 1.0`, and `NaN` is never equal).
|
|
117
|
+
* Anything a JSON value cannot be — Map, Set, RegExp, function, class
|
|
118
|
+
* instance — compares by identity only. Deliberately not the same as
|
|
119
|
+
* `equalsDeep`, the generic JavaScript variant: this is the hot-path
|
|
120
|
+
* comparator of the JSONPath engine — do not merge the two.
|
|
121
|
+
* @param {any} a
|
|
122
|
+
* @param {any} b
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
export function equalsJson(a, b) {
|
|
126
|
+
if (a === b)
|
|
127
|
+
return true;
|
|
128
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
129
|
+
return false;
|
|
130
|
+
const aIsArray = Array.isArray(a);
|
|
131
|
+
if (aIsArray !== Array.isArray(b))
|
|
132
|
+
return false;
|
|
133
|
+
if (aIsArray) {
|
|
134
|
+
const alen = a.length;
|
|
135
|
+
if (alen !== b.length)
|
|
136
|
+
return false;
|
|
137
|
+
for (let i = 0; i < alen; i++) {
|
|
138
|
+
if (!equalsJson(a[i], b[i]))
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
let count = 0;
|
|
144
|
+
for (const key in a) {
|
|
145
|
+
if (!hasOwn(a, key))
|
|
146
|
+
continue;
|
|
147
|
+
if (!hasOwn(b, key) || !equalsJson(a[key], b[key]))
|
|
148
|
+
return false;
|
|
149
|
+
count++;
|
|
150
|
+
}
|
|
151
|
+
for (const key in b) {
|
|
152
|
+
if (hasOwn(b, key))
|
|
153
|
+
count--;
|
|
154
|
+
}
|
|
155
|
+
return count === 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if all items in an array are unique using deep equality
|
|
160
|
+
* @param {any[]} arr - The array to check
|
|
161
|
+
* @returns {boolean} True if all items are unique
|
|
162
|
+
*/
|
|
163
|
+
export function isUniqueDeepArray(arr) {
|
|
164
|
+
if (!Array.isArray(arr) || arr.length < 2) return true;
|
|
165
|
+
|
|
166
|
+
// Small all-scalar arrays: pairwise === beats allocating a Set.
|
|
167
|
+
if (arr.length <= 8) {
|
|
168
|
+
let allScalar = true;
|
|
169
|
+
for (let i = 0; i < arr.length; i++) {
|
|
170
|
+
const item = arr[i];
|
|
171
|
+
if ((typeof item === 'object' && item !== null) || typeof item === 'function') {
|
|
172
|
+
allScalar = false;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (allScalar) {
|
|
177
|
+
for (let i = 0; i < arr.length; i++) {
|
|
178
|
+
for (let j = i + 1; j < arr.length; j++) {
|
|
179
|
+
if (arr[i] === arr[j]) return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Primitives are compared with === by equalsDeep and can never deep-equal
|
|
187
|
+
// an object, so they dedupe in O(n) through a Set (SameValueZero); only
|
|
188
|
+
// objects, arrays and functions need the pairwise deep comparison.
|
|
189
|
+
let seen = null;
|
|
190
|
+
let complex = null;
|
|
191
|
+
for (let i = 0; i < arr.length; i++) {
|
|
192
|
+
const item = arr[i];
|
|
193
|
+
if ((typeof item === 'object' && item !== null) || typeof item === 'function') {
|
|
194
|
+
if (complex === null) complex = [item];
|
|
195
|
+
else complex.push(item);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// NaN !== NaN under equalsDeep; keep NaN values always unique
|
|
199
|
+
if (typeof item === 'number' && item !== item) continue;
|
|
200
|
+
if (seen === null) seen = new Set();
|
|
201
|
+
else if (seen.has(item)) return false;
|
|
202
|
+
seen.add(item);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (complex !== null && complex.length > 1) {
|
|
207
|
+
for (let i = 0; i < complex.length; i++) {
|
|
208
|
+
for (let j = i + 1; j < complex.length; j++) {
|
|
209
|
+
if (equalsDeep(complex[i], complex[j])) return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
*
|
|
218
|
+
* @param {Map<any, any>} map
|
|
219
|
+
* @param {...Map<any, any>} iterables
|
|
220
|
+
*/
|
|
221
|
+
export function mergeMap(map, ...iterables) {
|
|
222
|
+
for (const iterable of iterables) {
|
|
223
|
+
for (const item of iterable) {
|
|
224
|
+
map.set(...item);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
*
|
|
231
|
+
* @param {Set<any>} set
|
|
232
|
+
* @param {...Array<Set<any>>} iterables
|
|
233
|
+
*/
|
|
234
|
+
export function mergeSet(set, ...iterables) {
|
|
235
|
+
for (const iterable of iterables) {
|
|
236
|
+
for (const item of iterable) {
|
|
237
|
+
set.add(item);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/scan.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
// Scanner primitives shared by the char-code-level recursive-descent
|
|
4
|
+
// parsers (JSONPath in json/path.js, I-Regexp in text/iregexp.js).
|
|
5
|
+
// Constants and pure predicates only — stateful token readers belong
|
|
6
|
+
// to the parser that owns the cursor.
|
|
7
|
+
|
|
8
|
+
//#region char codes
|
|
9
|
+
|
|
10
|
+
export const CC_TAB = 0x09;
|
|
11
|
+
export const CC_LF = 0x0A;
|
|
12
|
+
export const CC_CR = 0x0D;
|
|
13
|
+
export const CC_SPACE = 0x20;
|
|
14
|
+
export const CC_BANG = 0x21;
|
|
15
|
+
export const CC_DQUOTE = 0x22;
|
|
16
|
+
export const CC_HASH = 0x23;
|
|
17
|
+
export const CC_DOLLAR = 0x24;
|
|
18
|
+
export const CC_PERCENT = 0x25;
|
|
19
|
+
export const CC_AMP = 0x26;
|
|
20
|
+
export const CC_SQUOTE = 0x27;
|
|
21
|
+
export const CC_LPAREN = 0x28;
|
|
22
|
+
export const CC_RPAREN = 0x29;
|
|
23
|
+
export const CC_STAR = 0x2A;
|
|
24
|
+
export const CC_COMMA = 0x2C;
|
|
25
|
+
export const CC_MINUS = 0x2D;
|
|
26
|
+
export const CC_DOT = 0x2E;
|
|
27
|
+
export const CC_SLASH = 0x2F;
|
|
28
|
+
export const CC_0 = 0x30;
|
|
29
|
+
export const CC_1 = 0x31;
|
|
30
|
+
export const CC_9 = 0x39;
|
|
31
|
+
export const CC_COLON = 0x3A;
|
|
32
|
+
export const CC_LT = 0x3C;
|
|
33
|
+
export const CC_EQ = 0x3D;
|
|
34
|
+
export const CC_GT = 0x3E;
|
|
35
|
+
export const CC_QUESTION = 0x3F;
|
|
36
|
+
export const CC_AT = 0x40;
|
|
37
|
+
export const CC_LBRACKET = 0x5B;
|
|
38
|
+
export const CC_BACKSLASH = 0x5C;
|
|
39
|
+
export const CC_RBRACKET = 0x5D;
|
|
40
|
+
export const CC_UNDERSCORE = 0x5F;
|
|
41
|
+
export const CC_PIPE = 0x7C;
|
|
42
|
+
export const CC_TILDE = 0x7E;
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
|
|
46
|
+
//#region predicates
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Checks if a char code is an ASCII digit (0-9).
|
|
50
|
+
* @param {number} c - The char code
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
export function isDigitCode(c) {
|
|
54
|
+
return c >= CC_0 && c <= CC_9;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Checks if a char code is an ASCII hexadecimal digit (0-9, A-F, a-f).
|
|
59
|
+
* @param {number} c - The char code
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
export function isHexDigitCode(c) {
|
|
63
|
+
return (c >= CC_0 && c <= CC_9)
|
|
64
|
+
|| (c >= 0x41 && c <= 0x46) // A-F
|
|
65
|
+
|| (c >= 0x61 && c <= 0x66); // a-f
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Checks if a char code is blank space per RFC 9535 (space, tab,
|
|
70
|
+
* line feed or carriage return).
|
|
71
|
+
* @param {number} c - The char code
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function isWhitespaceCode(c) {
|
|
75
|
+
return c === CC_SPACE || c === CC_TAB || c === CC_LF || c === CC_CR;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks if a char code is an ASCII lowercase letter (a-z).
|
|
80
|
+
* @param {number} c - The char code
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
export function isAsciiLowerCode(c) {
|
|
84
|
+
return c >= 0x61 && c <= 0x7A;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checks if a char code is an ASCII uppercase letter (A-Z).
|
|
89
|
+
* @param {number} c - The char code
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
export function isAsciiUpperCode(c) {
|
|
93
|
+
return c >= 0x41 && c <= 0x5A;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
package/src/string.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isStringType,
|
|
5
|
+
isObjectOfClass,
|
|
6
|
+
} from './index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string | null | undefined} data
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isStringEmpty(data) {
|
|
13
|
+
return typeof data === 'string' && !data;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string | null | undefined} data
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function isStringWhiteSpace(data) {
|
|
21
|
+
return data == null || /^\s*$/.test(data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} str
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
export function isStringUpperCase(str) {
|
|
29
|
+
return str === str.toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} str
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
export function isStringLowerCase(str) {
|
|
37
|
+
return str === str.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {unknown} data
|
|
42
|
+
* @returns {data is RegExp}
|
|
43
|
+
*/
|
|
44
|
+
export function isRegExpType(data) {
|
|
45
|
+
return isObjectOfClass(data, RegExp);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string | RegExp | null | undefined } data
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
export function isStringRegExp(data) {
|
|
54
|
+
try {
|
|
55
|
+
return createRegExp(data) != null;
|
|
56
|
+
}
|
|
57
|
+
// eslint-disable-next-line no-unused-vars
|
|
58
|
+
catch (e) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string | RegExp | null | undefined } pattern
|
|
65
|
+
* @returns {RegExp | undefined}
|
|
66
|
+
*/
|
|
67
|
+
export function createRegExp(pattern) {
|
|
68
|
+
if (pattern == null)
|
|
69
|
+
return undefined;
|
|
70
|
+
|
|
71
|
+
if (isRegExpType(pattern))
|
|
72
|
+
return pattern;
|
|
73
|
+
|
|
74
|
+
if (isStringType(pattern)) {
|
|
75
|
+
if (pattern[0] === '/') {
|
|
76
|
+
const e = pattern.lastIndexOf('/');
|
|
77
|
+
if (e >= 0) {
|
|
78
|
+
const r = pattern.substring(1, e);
|
|
79
|
+
const g = pattern.substring(e + 1);
|
|
80
|
+
// Add unicode flag if not already present
|
|
81
|
+
return g.includes('u') ? new RegExp(r, g) : new RegExp(r, g + 'u');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
return new RegExp(pattern, 'u');
|
|
86
|
+
}
|
|
87
|
+
// TODO: should we return false instead?
|
|
88
|
+
throw new Error(`Unknown Regular Expression Pattern Type: ${pattern}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @type {Intl.Segmenter | null}
|
|
93
|
+
*/
|
|
94
|
+
let segmenterCache = null;
|
|
95
|
+
export function getSegmenter() {
|
|
96
|
+
if (segmenterCache === null) {
|
|
97
|
+
segmenterCache = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
98
|
+
}
|
|
99
|
+
return segmenterCache;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} str
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
export function isAsciiString(str) {
|
|
107
|
+
const len = str.length;
|
|
108
|
+
for (let i = 0; i < len; i++) {
|
|
109
|
+
if (str.charCodeAt(i) > 127) return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Characters that can merge with neighbours into a multi-codepoint grapheme
|
|
115
|
+
// cluster: CR (CRLF), combining marks, ZWJ/ZWNJ, variation selectors,
|
|
116
|
+
// emoji modifiers, regional indicators (flags), conjoining Hangul jamo,
|
|
117
|
+
// prepended concatenation marks and tag characters. When none of these are
|
|
118
|
+
// present, grapheme count equals code point count and the (expensive)
|
|
119
|
+
// Intl.Segmenter can be skipped.
|
|
120
|
+
// (U+0600-0605, 06DD, 070F, 0890, 0891, 08E2, 110BD, 110CD are the
|
|
121
|
+
// Prepended_Concatenation_Mark set, which V8 does not expose as \p{...})
|
|
122
|
+
const COMPLEX_GRAPHEME_REGEX = /[\r\p{M}\p{Join_Control}\p{Emoji_Modifier}\p{Regional_Indicator}\u0600-\u0605\u06DD\u070F\u0890\u0891\u08E2\uFE00-\uFE0F\u1100-\u11FF\uA960-\uA97F\uD7B0-\uD7FF\u{110BD}\u{110CD}\u{E0000}-\u{E007F}]/u;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} str
|
|
126
|
+
* @param {boolean} [useGrapheme=false]
|
|
127
|
+
* @returns {number}
|
|
128
|
+
*/
|
|
129
|
+
export function getStringLength(str, useGrapheme = false) {
|
|
130
|
+
if (!useGrapheme) {
|
|
131
|
+
return str.length;
|
|
132
|
+
}
|
|
133
|
+
// Fast-path: check ASCII inline to avoid function call overhead
|
|
134
|
+
const len = str.length;
|
|
135
|
+
for (let i = 0; i < len; i++) {
|
|
136
|
+
if (str.charCodeAt(i) > 127) {
|
|
137
|
+
// Non-ASCII found - use grapheme counting.
|
|
138
|
+
// Segment the whole string: a cluster may span the ASCII boundary
|
|
139
|
+
// (e.g. 'e' followed by a combining accent).
|
|
140
|
+
if (COMPLEX_GRAPHEME_REGEX.test(str)) {
|
|
141
|
+
let count = 0;
|
|
142
|
+
for (const _ of getSegmenter().segment(str)) count++;
|
|
143
|
+
return count;
|
|
144
|
+
}
|
|
145
|
+
// No cluster-forming characters: count code points (surrogate aware)
|
|
146
|
+
return countCodePoints(str);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return len;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Count the Unicode code points of a string (surrogate-pair aware;
|
|
154
|
+
* a lone surrogate counts as one code point).
|
|
155
|
+
* @param {string} str
|
|
156
|
+
* @returns {number}
|
|
157
|
+
*/
|
|
158
|
+
export function countCodePoints(str) {
|
|
159
|
+
const slen = str.length;
|
|
160
|
+
let count = 0;
|
|
161
|
+
for (let i = 0; i < slen; i++) {
|
|
162
|
+
const c = str.charCodeAt(i);
|
|
163
|
+
if (c >= 0xD800 && c <= 0xDBFF && i + 1 < slen) {
|
|
164
|
+
const d = str.charCodeAt(i + 1);
|
|
165
|
+
if (d >= 0xDC00 && d <= 0xDFFF)
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
count++;
|
|
169
|
+
}
|
|
170
|
+
return count;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Compare two strings by Unicode scalar values (code points), per
|
|
175
|
+
* RFC 9535 section 2.3.5.2.2. This differs from JavaScript's native
|
|
176
|
+
* `<`, which compares UTF-16 code units and orders surrogate pairs
|
|
177
|
+
* (U+10000 and up) below unpaired BMP characters in U+E000-U+FFFF.
|
|
178
|
+
* When one string is a prefix of the other, the shorter sorts first.
|
|
179
|
+
* @param {string} a
|
|
180
|
+
* @param {string} b
|
|
181
|
+
* @returns {number} -1 when a < b, 0 when equal, 1 when a > b
|
|
182
|
+
*/
|
|
183
|
+
export function compareCodePoints(a, b) {
|
|
184
|
+
const alen = a.length;
|
|
185
|
+
const blen = b.length;
|
|
186
|
+
const m = alen < blen ? alen : blen;
|
|
187
|
+
let i = 0;
|
|
188
|
+
while (i < m && a.charCodeAt(i) === b.charCodeAt(i))
|
|
189
|
+
i++;
|
|
190
|
+
if (i === m)
|
|
191
|
+
return alen === blen ? 0 : (alen < blen ? -1 : 1);
|
|
192
|
+
// differing code units at i can never decode to equal code points
|
|
193
|
+
return a.codePointAt(i) < b.codePointAt(i) ? -1 : 1;
|
|
194
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const CONST_REGEXP_BASE64 = /^(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))$/;
|
|
2
|
+
export function isValidBase64Full(str) {
|
|
3
|
+
return CONST_REGEXP_BASE64.test(str);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Base64 validation regex (RFC 4648)
|
|
7
|
+
const BASE64_REGEX_SHORT = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
8
|
+
|
|
9
|
+
export function isValidBase64Old(str) {
|
|
10
|
+
// Check length is valid for base64 (multiple of 4)
|
|
11
|
+
if (str.length % 4 !== 0) return false;
|
|
12
|
+
// Check characters are valid base64
|
|
13
|
+
if (!BASE64_REGEX_SHORT.test(str)) return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})?$/;
|
|
18
|
+
|
|
19
|
+
export function isValidBase64(str) {
|
|
20
|
+
return BASE64_REGEX.test(str);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fast base64 validation - inline for performance
|
|
24
|
+
// Valid base64 chars: A-Z (65-90), a-z (97-122), 0-9 (48-57), + (43), / (47)
|
|
25
|
+
export function isValidBase64Fast(str) {
|
|
26
|
+
const len = str.length;
|
|
27
|
+
if (len === 0) return true;
|
|
28
|
+
|
|
29
|
+
// Check length is valid for base64 (must be multiple of 4)
|
|
30
|
+
if (len % 4 !== 0) return false;
|
|
31
|
+
|
|
32
|
+
// Check all characters are valid base64 (A-Z, a-z, 0-9, +, /, =)
|
|
33
|
+
for (let i = 0; i < len; i++) {
|
|
34
|
+
const code = str.charCodeAt(i);
|
|
35
|
+
// A-Z, a-z, 0-9, +, /, =
|
|
36
|
+
if (!((code >= 65 && code <= 90) ||
|
|
37
|
+
(code >= 97 && code <= 122) ||
|
|
38
|
+
(code >= 48 && code <= 57) ||
|
|
39
|
+
code === 43 || code === 47 || code === 61)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check padding rules
|
|
45
|
+
// Valid endings: xxxx (no padding), xxx= (1 padding), xx== (2 padding)
|
|
46
|
+
const c1 = str.charCodeAt(len - 2);
|
|
47
|
+
const c2 = str.charCodeAt(len - 1);
|
|
48
|
+
|
|
49
|
+
// If second to last is =, last must also be =
|
|
50
|
+
if (c1 === 61 && c2 !== 61) return false;
|
|
51
|
+
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const CONST_REGEXP_ALPHA = /^[a-zA-Z]+$/;
|
|
2
|
+
export function isValidAlpha(str) {
|
|
3
|
+
return CONST_REGEXP_ALPHA.test(str);
|
|
4
|
+
}
|
|
5
|
+
const CONST_REGEXP_ALPHANUMERIC = /^[a-zA-Z0-9]+$/;
|
|
6
|
+
export function isValidAlphaNumeric(str) {
|
|
7
|
+
return CONST_REGEXP_ALPHANUMERIC.test(str);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CONST_REGEXP_NUMERIC = /^[0-9]+$/;
|
|
11
|
+
export function isValidNumeric(str) {
|
|
12
|
+
return CONST_REGEXP_NUMERIC.test(str);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CONST_REGEXP_HEXADECIMAL = /^[a-fA-F0-9]+$/;
|
|
16
|
+
export function isValidHexaDecimal(str) {
|
|
17
|
+
return CONST_REGEXP_HEXADECIMAL.test(str);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CONST_REGEXP_HEXCOLOR = /^#(?:[0-9a-f]{3}){1,2}\b$/i;
|
|
21
|
+
export function isValidHexColor(str) {
|
|
22
|
+
return CONST_REGEXP_HEXCOLOR.test(str);
|
|
23
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isValidIPv4,
|
|
3
|
+
isValidIPv6,
|
|
4
|
+
} from './host.js';
|
|
5
|
+
|
|
6
|
+
// email (sources from json validator):
|
|
7
|
+
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363
|
|
8
|
+
// http://www.w3.org/TR/html5/forms.html#valid-e-mail-address (search for 'willful violation')
|
|
9
|
+
const CONST_REGEXP_EMAIL_FAST = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
10
|
+
|
|
11
|
+
// RFC 5321 slow-path pieces: quoted-string local part (qtext / quoted-pair),
|
|
12
|
+
// dot-atom local part and dot-atom domain.
|
|
13
|
+
const CONST_REGEXP_EMAIL_QUOTED_LOCAL = /^"(?:[\x20-\x21\x23-\x5b\x5d-\x7e]|\\[\x20-\x7e])*"$/;
|
|
14
|
+
const CONST_REGEXP_EMAIL_LOCAL = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*$/i;
|
|
15
|
+
const CONST_REGEXP_EMAIL_DOMAIN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
16
|
+
|
|
17
|
+
function isValidEmailDomain(domain) {
|
|
18
|
+
// RFC 5321 address literal: [IPv4] or [IPv6:...]
|
|
19
|
+
if (domain.charCodeAt(0) === 0x5b /* [ */) {
|
|
20
|
+
if (domain.charCodeAt(domain.length - 1) !== 0x5d /* ] */) return false;
|
|
21
|
+
const literal = domain.slice(1, -1);
|
|
22
|
+
if (literal.slice(0, 5).toLowerCase() === 'ipv6:')
|
|
23
|
+
return isValidIPv6(literal.slice(5));
|
|
24
|
+
return isValidIPv4(literal);
|
|
25
|
+
}
|
|
26
|
+
return CONST_REGEXP_EMAIL_DOMAIN.test(domain);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isValidEmail(str) {
|
|
30
|
+
// Fast path: dot-atom local part with a regular domain
|
|
31
|
+
if (CONST_REGEXP_EMAIL_FAST.test(str)) return true;
|
|
32
|
+
|
|
33
|
+
// Slow path (RFC 5321): quoted-string local part and/or address literal
|
|
34
|
+
if (str.charCodeAt(0) === 0x22 /* " */) {
|
|
35
|
+
// Find the end of the quoted string, honoring quoted-pairs
|
|
36
|
+
let i = 1;
|
|
37
|
+
for (; i < str.length; ++i) {
|
|
38
|
+
const c = str.charCodeAt(i);
|
|
39
|
+
if (c === 0x5c /* \ */) { i++; continue; }
|
|
40
|
+
if (c === 0x22 /* " */) break;
|
|
41
|
+
}
|
|
42
|
+
if (i >= str.length || str.charCodeAt(i + 1) !== 0x40 /* @ */) return false;
|
|
43
|
+
return CONST_REGEXP_EMAIL_QUOTED_LOCAL.test(str.slice(0, i + 1))
|
|
44
|
+
&& isValidEmailDomain(str.slice(i + 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const at = str.lastIndexOf('@');
|
|
48
|
+
if (at <= 0) return false;
|
|
49
|
+
return CONST_REGEXP_EMAIL_LOCAL.test(str.slice(0, at))
|
|
50
|
+
&& isValidEmailDomain(str.slice(at + 1));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const CONST_REGEXP_EMAIL_FULL = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
54
|
+
export function isValidEmailFull(str) {
|
|
55
|
+
return CONST_REGEXP_EMAIL_FULL.test(str);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const CONST_REGEXP_IDNEMAIL = /^[^@]+@[^@]+\.[^@]+$/;
|
|
59
|
+
export function isValidIdnEmail(str) {
|
|
60
|
+
return CONST_REGEXP_IDNEMAIL.test(str);
|
|
61
|
+
}
|