@gesslar/toolkit 4.2.0 → 4.4.0
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/README.md +48 -0
- package/package.json +10 -2
- package/scripts/vendor-build.js +32 -0
- package/src/browser/lib/Time.js +26 -9
- package/types/browser/lib/Time.d.ts +17 -7
- package/types/browser/lib/Time.d.ts.map +1 -1
- package/vendor/toolkit.esm.js +4234 -0
- package/vendor/toolkit.umd.js +4256 -0
|
@@ -0,0 +1,4256 @@
|
|
|
1
|
+
// @gesslar/toolkit v4.4.0 - UMD bundle
|
|
2
|
+
(function (global, factory) {
|
|
3
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
4
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
5
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Toolkit = {}));
|
|
6
|
+
})(this, (function (exports) { 'use strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @file Tantrum.js
|
|
10
|
+
*
|
|
11
|
+
* Defines the Tantrum class, a custom AggregateError type for toolkit
|
|
12
|
+
* that collects multiple errors with Sass-style reporting.
|
|
13
|
+
*
|
|
14
|
+
* Auto-wraps plain Error objects in Sass instances while preserving
|
|
15
|
+
* existing Sass errors, providing consistent formatted output for
|
|
16
|
+
* multiple error scenarios.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Custom aggregate error class that extends AggregateError.
|
|
22
|
+
* Automatically wraps plain errors in Sass instances for consistent reporting.
|
|
23
|
+
*/
|
|
24
|
+
class Tantrum extends AggregateError {
|
|
25
|
+
#trace = []
|
|
26
|
+
#sass
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new Tantrum instance.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} message - The aggregate error message
|
|
32
|
+
* @param {Array<Error|Sass>} errors - Array of errors to aggregate
|
|
33
|
+
* @param {Sass} sass - Sass constructor
|
|
34
|
+
*/
|
|
35
|
+
constructor(message, errors = [], sass=Sass) {
|
|
36
|
+
// Auto-wrap plain errors in Sass, keep existing Sass instances
|
|
37
|
+
const wrappedErrors = errors.map(error => {
|
|
38
|
+
if(error instanceof sass)
|
|
39
|
+
return error
|
|
40
|
+
|
|
41
|
+
if(!(error instanceof Error))
|
|
42
|
+
throw new TypeError(`All items in errors array must be Error instances, got: ${typeof error}`)
|
|
43
|
+
|
|
44
|
+
return sass.new(error.message, error)
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
super(wrappedErrors, message);
|
|
48
|
+
|
|
49
|
+
this.name = "Tantrum";
|
|
50
|
+
this.#sass = sass;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Adds a trace message and returns this instance for chaining.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} message - The trace message to add
|
|
57
|
+
* @param {Error|Sass} [_error] - Optional error (currently unused, reserved for future use)
|
|
58
|
+
* @returns {this} This Tantrum instance for method chaining
|
|
59
|
+
*/
|
|
60
|
+
addTrace(message, _error) {
|
|
61
|
+
if(typeof message !== "string")
|
|
62
|
+
throw this.#sass.new(`Tantrum.addTrace expected string, got ${JSON.stringify(message)}`)
|
|
63
|
+
|
|
64
|
+
this.trace = message;
|
|
65
|
+
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Gets the error trace array.
|
|
71
|
+
*
|
|
72
|
+
* @returns {Array<string>} Array of trace messages
|
|
73
|
+
*/
|
|
74
|
+
get trace() {
|
|
75
|
+
return this.#trace
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Adds a message to the beginning of the trace array.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} message - The trace message to add
|
|
82
|
+
*/
|
|
83
|
+
set trace(message) {
|
|
84
|
+
this.#trace.unshift(message);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reports all aggregated errors to the console with formatted output.
|
|
89
|
+
*
|
|
90
|
+
* @param {boolean} [nerdMode] - Whether to include detailed stack traces
|
|
91
|
+
* @param {boolean} [isNested] - Whether this is a nested error report
|
|
92
|
+
*/
|
|
93
|
+
report(nerdMode = false, isNested = false) {
|
|
94
|
+
if(isNested)
|
|
95
|
+
console.error();
|
|
96
|
+
|
|
97
|
+
console.group(
|
|
98
|
+
`[Tantrum Incoming] x${this.errors.length}\n` +
|
|
99
|
+
this.message
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if(this.trace.length > 0)
|
|
103
|
+
console.error(this.trace.join("\n"));
|
|
104
|
+
|
|
105
|
+
this.errors.forEach(error => {
|
|
106
|
+
error.report(nerdMode, true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.groupEnd();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Factory method to create a Tantrum instance.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} message - The aggregate error message
|
|
116
|
+
* @param {Array<Error|Sass>} errors - Array of errors to aggregate
|
|
117
|
+
* @returns {Tantrum} New Tantrum instance
|
|
118
|
+
*/
|
|
119
|
+
static new(message, errors = []) {
|
|
120
|
+
if(errors instanceof this)
|
|
121
|
+
return errors.addTrace(message)
|
|
122
|
+
|
|
123
|
+
return new this(message, errors)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @file Sass.js
|
|
129
|
+
*
|
|
130
|
+
* Defines the Sass class, a custom error type for toolkit compilation
|
|
131
|
+
* errors.
|
|
132
|
+
*
|
|
133
|
+
* Supports error chaining, trace management, and formatted reporting for both
|
|
134
|
+
* user-friendly and verbose (nerd) output.
|
|
135
|
+
*
|
|
136
|
+
* Used throughout the toolkit for structured error handling and
|
|
137
|
+
* debugging.
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Custom error class for toolkit errors.
|
|
143
|
+
* Provides error chaining, trace management, and formatted error reporting.
|
|
144
|
+
*/
|
|
145
|
+
class Sass extends Error {
|
|
146
|
+
#trace = []
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Creates a new Sass instance.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} message - The error message
|
|
152
|
+
* @param {...unknown} [arg] - Additional arguments passed to parent Error constructor
|
|
153
|
+
*/
|
|
154
|
+
constructor(message, ...arg) {
|
|
155
|
+
super(message, ...arg);
|
|
156
|
+
|
|
157
|
+
this.trace = message;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Gets the error trace array.
|
|
162
|
+
*
|
|
163
|
+
* @returns {Array<string>} Array of trace messages
|
|
164
|
+
*/
|
|
165
|
+
get trace() {
|
|
166
|
+
return this.#trace
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Adds a message to the beginning of the trace array.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} message - The trace message to add
|
|
173
|
+
*/
|
|
174
|
+
set trace(message) {
|
|
175
|
+
this.#trace.unshift(message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Adds a trace message and returns this instance for chaining.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} message - The trace message to add
|
|
182
|
+
* @returns {this} This Sass instance for method chaining
|
|
183
|
+
*/
|
|
184
|
+
addTrace(message) {
|
|
185
|
+
if(typeof message !== "string")
|
|
186
|
+
throw this.constructor.new(`Sass.addTrace expected string, got ${JSON.stringify(message)}`)
|
|
187
|
+
|
|
188
|
+
this.trace = message;
|
|
189
|
+
|
|
190
|
+
return this
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Reports the error to the console with formatted output.
|
|
195
|
+
* Optionally includes detailed stack trace information.
|
|
196
|
+
*
|
|
197
|
+
* @param {boolean} [nerdMode] - Whether to include detailed stack trace
|
|
198
|
+
* @param {boolean} [isNested] - Whether this is a nested error report
|
|
199
|
+
*/
|
|
200
|
+
report(nerdMode=false, isNested=false) {
|
|
201
|
+
if(isNested)
|
|
202
|
+
console.error();
|
|
203
|
+
|
|
204
|
+
console.group(
|
|
205
|
+
`[error] Something Went Wrong\n` +
|
|
206
|
+
this.trace.join("\n")
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if(nerdMode) {
|
|
210
|
+
console.error(
|
|
211
|
+
"\n" +
|
|
212
|
+
`[error] Nerd Victuals\n` +
|
|
213
|
+
this.#fullBodyMassage(this.stack)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if(this.cause) {
|
|
218
|
+
if(typeof this.cause.report === "function") {
|
|
219
|
+
if(nerdMode) {
|
|
220
|
+
console.error(
|
|
221
|
+
"\n" +
|
|
222
|
+
`[error] Caused By`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.cause.report(nerdMode, true);
|
|
227
|
+
} else if(nerdMode && this.cause.stack) {
|
|
228
|
+
console.error();
|
|
229
|
+
console.group();
|
|
230
|
+
console.error(
|
|
231
|
+
`[error] Rethrown From\n` +
|
|
232
|
+
this.#fullBodyMassage(this.cause.stack)
|
|
233
|
+
);
|
|
234
|
+
console.groupEnd();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.groupEnd();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Formats the stack trace for display, removing the first line and
|
|
243
|
+
* formatting each line with appropriate indentation.
|
|
244
|
+
*
|
|
245
|
+
* Note: Returns formatted stack trace or undefined if no stack available.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} stack - The error stack to massage.
|
|
248
|
+
* @returns {string|undefined} Formatted stack trace or undefined
|
|
249
|
+
*/
|
|
250
|
+
#fullBodyMassage(stack) {
|
|
251
|
+
stack = stack ?? "";
|
|
252
|
+
// Remove the first line, it's already been reported
|
|
253
|
+
const {rest} = stack.match(/^.*?\n(?<rest>[\s\S]+)$/m)?.groups ?? {};
|
|
254
|
+
const lines = [];
|
|
255
|
+
|
|
256
|
+
if(rest) {
|
|
257
|
+
lines.push(
|
|
258
|
+
...rest
|
|
259
|
+
.split("\n")
|
|
260
|
+
.map(line => {
|
|
261
|
+
const at = line.match(/^\s{4}at\s(?<at>.*)$/)?.groups?.at ?? "";
|
|
262
|
+
|
|
263
|
+
return at
|
|
264
|
+
? `* ${at}`
|
|
265
|
+
: line
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return lines.join("\n")
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Creates an Sass from an existing Error object with additional
|
|
275
|
+
* trace message.
|
|
276
|
+
*
|
|
277
|
+
* @param {Error} error - The original error object
|
|
278
|
+
* @param {string} message - Additional trace message to add
|
|
279
|
+
* @returns {Sass} New Sass instance with trace from the original error
|
|
280
|
+
* @throws {Sass} If the first parameter is not an Error instance
|
|
281
|
+
*/
|
|
282
|
+
static from(error, message) {
|
|
283
|
+
if(!(error instanceof Error))
|
|
284
|
+
throw this.new("Sass.from must take an Error object.")
|
|
285
|
+
|
|
286
|
+
const oldMessage = error.message;
|
|
287
|
+
const newError = new this(
|
|
288
|
+
oldMessage, {cause: error}
|
|
289
|
+
).addTrace(message);
|
|
290
|
+
|
|
291
|
+
return newError
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Factory method to create or enhance Sass instances.
|
|
296
|
+
* If error parameter is provided, enhances existing Sass or wraps
|
|
297
|
+
* other errors. Otherwise creates a new Sass instance.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} message - The error message
|
|
300
|
+
* @param {Error|Sass|Tantrum} [error] - Optional existing error to wrap or enhance
|
|
301
|
+
* @returns {Sass} New or enhanced Sass instance
|
|
302
|
+
*/
|
|
303
|
+
static new(message, error) {
|
|
304
|
+
if(error) {
|
|
305
|
+
if(error instanceof Tantrum)
|
|
306
|
+
return Tantrum.new(message, error)
|
|
307
|
+
|
|
308
|
+
return error instanceof this
|
|
309
|
+
? error.addTrace(message)
|
|
310
|
+
: this.from(error, message)
|
|
311
|
+
} else {
|
|
312
|
+
|
|
313
|
+
return new this(message)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @file Valid.js
|
|
320
|
+
*
|
|
321
|
+
* Provides validation utilities for type checking and assertion.
|
|
322
|
+
* Includes prototype pollution protection for secure object manipulation.
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Validation utility class providing type checking and assertion methods.
|
|
328
|
+
*/
|
|
329
|
+
class Valid {
|
|
330
|
+
/**
|
|
331
|
+
* Validates a value against a type. Uses Data.isType.
|
|
332
|
+
*
|
|
333
|
+
* @param {unknown} value - The value to validate
|
|
334
|
+
* @param {string} type - The expected type in the form of "object", "object[]", "object|object[]"
|
|
335
|
+
* @param {object} [options] - Additional options for validation.
|
|
336
|
+
*/
|
|
337
|
+
static type(value, type, options) {
|
|
338
|
+
Valid.assert(
|
|
339
|
+
Data.isType(value, type, options),
|
|
340
|
+
`Invalid type. Expected ${type}, got ${Data.typeOf(value)}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Asserts a condition
|
|
346
|
+
*
|
|
347
|
+
* @param {boolean} condition - The condition to assert
|
|
348
|
+
* @param {string} message - The message to display if the condition is not
|
|
349
|
+
* met
|
|
350
|
+
* @param {number} [arg] - The argument to display if the condition is not
|
|
351
|
+
* met (optional)
|
|
352
|
+
*/
|
|
353
|
+
static assert(condition, message, arg = null) {
|
|
354
|
+
if(!Data.isType(condition, "boolean")) {
|
|
355
|
+
throw Sass.new(`Condition must be a boolean, got ${condition}`)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if(!Data.isType(message, "string")) {
|
|
359
|
+
throw Sass.new(`Message must be a string, got ${message}`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if(!(arg === null || arg === undefined || typeof arg === "number")) {
|
|
363
|
+
throw Sass.new(`Arg must be a number, got ${arg}`)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if(!condition)
|
|
367
|
+
throw Sass.new(`${message}${arg ? `: ${arg}` : ""}`)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
static #restrictedProto = ["__proto__", "constructor", "prototype"]
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Protects against prototype pollution by checking keys for dangerous property names.
|
|
374
|
+
* Throws if any restricted prototype properties are found in the keys array.
|
|
375
|
+
*
|
|
376
|
+
* @param {Array<string>} keys - Array of property keys to validate
|
|
377
|
+
* @throws {Sass} If any key matches restricted prototype properties (__proto__, constructor, prototype)
|
|
378
|
+
*/
|
|
379
|
+
static prototypePollutionProtection(keys) {
|
|
380
|
+
Valid.type(keys, "String[]");
|
|
381
|
+
|
|
382
|
+
const oopsIDidItAgain = Collection.intersection(this.#restrictedProto, keys);
|
|
383
|
+
|
|
384
|
+
Valid.assert(
|
|
385
|
+
oopsIDidItAgain.length === 0,
|
|
386
|
+
`We don't pee in your pool, don't pollute ours with your ${String(oopsIDidItAgain)}`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Utility class providing common helper functions for string manipulation,
|
|
393
|
+
* timing, and option parsing.
|
|
394
|
+
*/
|
|
395
|
+
class Util {
|
|
396
|
+
/**
|
|
397
|
+
* Capitalizes the first letter of a string.
|
|
398
|
+
*
|
|
399
|
+
* @param {string} text - The text to capitalize
|
|
400
|
+
* @returns {string} Text with first letter capitalized
|
|
401
|
+
*/
|
|
402
|
+
static capitalize(text) {
|
|
403
|
+
if(typeof text !== "string")
|
|
404
|
+
throw new TypeError("Util.capitalize expects a string")
|
|
405
|
+
|
|
406
|
+
if(text.length === 0)
|
|
407
|
+
return ""
|
|
408
|
+
|
|
409
|
+
const [first, ...rest] = Array.from(text);
|
|
410
|
+
|
|
411
|
+
return `${first.toLocaleUpperCase()}${rest.join("")}`
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Measure wall-clock time for an async function.
|
|
416
|
+
*
|
|
417
|
+
* @template T
|
|
418
|
+
* @param {() => Promise<T>} fn - Thunk returning a promise.
|
|
419
|
+
* @returns {Promise<{result: T, cost: number}>} Object containing result and elapsed ms (number, 1 decimal).
|
|
420
|
+
*/
|
|
421
|
+
static async time(fn) {
|
|
422
|
+
const t0 = performance.now();
|
|
423
|
+
const result = await fn();
|
|
424
|
+
const cost = Math.round((performance.now() - t0) * 10) / 10;
|
|
425
|
+
|
|
426
|
+
return {result, cost}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Right-align a string inside a fixed width (left pad with spaces).
|
|
431
|
+
* If the string exceeds width it is returned unchanged.
|
|
432
|
+
*
|
|
433
|
+
* @param {string|number} text - Text to align.
|
|
434
|
+
* @param {number} width - Target field width (default 80).
|
|
435
|
+
* @returns {string} Padded string.
|
|
436
|
+
*/
|
|
437
|
+
static rightAlignText(text, width=80) {
|
|
438
|
+
const work = String(text);
|
|
439
|
+
|
|
440
|
+
if(work.length > width)
|
|
441
|
+
return work
|
|
442
|
+
|
|
443
|
+
const diff = width-work.length;
|
|
444
|
+
|
|
445
|
+
return `${" ".repeat(diff)}${work}`
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Centre-align a string inside a fixed width (pad with spaces on left).
|
|
450
|
+
* If the string exceeds width it is returned unchanged.
|
|
451
|
+
*
|
|
452
|
+
* @param {string|number} text - Text to align.
|
|
453
|
+
* @param {number} width - Target field width (default 80).
|
|
454
|
+
* @returns {string} Padded string with text centred.
|
|
455
|
+
*/
|
|
456
|
+
static centreAlignText(text, width=80) {
|
|
457
|
+
const work = String(text);
|
|
458
|
+
|
|
459
|
+
if(work.length >= width)
|
|
460
|
+
return work
|
|
461
|
+
|
|
462
|
+
const totalPadding = width - work.length;
|
|
463
|
+
const leftPadding = Math.floor(totalPadding / 2);
|
|
464
|
+
const rightPadding = totalPadding - leftPadding;
|
|
465
|
+
|
|
466
|
+
return `${" ".repeat(leftPadding)}${work}${" ".repeat(rightPadding)}`
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Determine the Levenshtein distance between two string values
|
|
471
|
+
*
|
|
472
|
+
* @param {string} a The first value for comparison.
|
|
473
|
+
* @param {string} b The second value for comparison.
|
|
474
|
+
* @returns {number} The Levenshtein distance
|
|
475
|
+
*/
|
|
476
|
+
static levenshteinDistance(a, b) {
|
|
477
|
+
const matrix = Array.from({length: a.length + 1}, (_, i) =>
|
|
478
|
+
Array.from({length: b.length + 1}, (_, j) =>
|
|
479
|
+
(i === 0 ? j : j === 0 ? i : 0)
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
for(let i = 1; i <= a.length; i++) {
|
|
484
|
+
for(let j = 1; j <= b.length; j++) {
|
|
485
|
+
matrix[i][j] =
|
|
486
|
+
a[i - 1] === b[j - 1]
|
|
487
|
+
? matrix[i - 1][j - 1]
|
|
488
|
+
: 1 + Math.min(
|
|
489
|
+
matrix[i - 1][j], matrix[i][j - 1],
|
|
490
|
+
matrix[i - 1][j - 1]
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return matrix[a.length][b.length]
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Determine the closest match between a string and allowed values
|
|
500
|
+
* from the Levenshtein distance.
|
|
501
|
+
*
|
|
502
|
+
* @param {string} input The input string to resolve
|
|
503
|
+
* @param {Array<string>} allowedValues The values which are permitted
|
|
504
|
+
* @param {number} [threshold] Max edit distance for a "close match"
|
|
505
|
+
* @returns {string} Suggested, probable match.
|
|
506
|
+
*/
|
|
507
|
+
static findClosestMatch(input, allowedValues, threshold=2) {
|
|
508
|
+
let closestMatch = null;
|
|
509
|
+
let closestDistance = Infinity;
|
|
510
|
+
let closestLengthDiff = Infinity;
|
|
511
|
+
|
|
512
|
+
for(const value of allowedValues) {
|
|
513
|
+
const distance = this.levenshteinDistance(input, value);
|
|
514
|
+
const lengthDiff = Math.abs(input.length - value.length);
|
|
515
|
+
|
|
516
|
+
if(distance < closestDistance && distance <= threshold) {
|
|
517
|
+
closestMatch = value;
|
|
518
|
+
closestDistance = distance;
|
|
519
|
+
closestLengthDiff = lengthDiff;
|
|
520
|
+
} else if(distance === closestDistance &&
|
|
521
|
+
distance <= threshold &&
|
|
522
|
+
lengthDiff < closestLengthDiff) {
|
|
523
|
+
closestMatch = value;
|
|
524
|
+
closestLengthDiff = lengthDiff;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return closestMatch
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
static regexify(input, trim=true, flags=[]) {
|
|
532
|
+
Valid.type(input, "String");
|
|
533
|
+
Valid.type(trim, "Boolean");
|
|
534
|
+
Valid.type(flags, "Array");
|
|
535
|
+
|
|
536
|
+
Valid.assert(
|
|
537
|
+
flags.length === 0 ||
|
|
538
|
+
(flags.length > 0 && Collection.isArrayUniform(flags, "String")),
|
|
539
|
+
"All flags must be strings");
|
|
540
|
+
|
|
541
|
+
return new RegExp(
|
|
542
|
+
input
|
|
543
|
+
.split(/\r\n|\r|\n/)
|
|
544
|
+
.map(i => trim ? i.trim() : i)
|
|
545
|
+
.filter(i => trim ? Boolean(i) : true)
|
|
546
|
+
.join("")
|
|
547
|
+
, flags?.join("")
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
static semver = {
|
|
552
|
+
meetsOrExceeds: (supplied, target) => {
|
|
553
|
+
Valid.type(supplied, "String", {allowEmpty: false});
|
|
554
|
+
Valid.type(target, "String", {allowEmpty: false});
|
|
555
|
+
|
|
556
|
+
const suppliedSemver = supplied.split(".").filter(Boolean).map(Number).filter(e => !isNaN(e));
|
|
557
|
+
const targetSemver = target.split(".").filter(Boolean).map(Number).filter(e => !isNaN(e));
|
|
558
|
+
|
|
559
|
+
Valid.assert(suppliedSemver.length === 3, "Invalid format for supplied semver.");
|
|
560
|
+
Valid.assert(targetSemver.length === 3, "Invalid format for target semver.");
|
|
561
|
+
|
|
562
|
+
if(suppliedSemver[0] < targetSemver[0])
|
|
563
|
+
return false
|
|
564
|
+
|
|
565
|
+
if(suppliedSemver[0] === targetSemver[0]) {
|
|
566
|
+
if(suppliedSemver[1] < targetSemver[1])
|
|
567
|
+
return false
|
|
568
|
+
|
|
569
|
+
if(suppliedSemver[1] === targetSemver[1])
|
|
570
|
+
if(suppliedSemver[2] < targetSemver[2])
|
|
571
|
+
return false
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return true
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @file Type specification and validation utilities.
|
|
581
|
+
* Provides TypeSpec class for parsing and validating complex type specifications
|
|
582
|
+
* including arrays, unions, and options.
|
|
583
|
+
*/
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Type specification class for parsing and validating complex type definitions.
|
|
588
|
+
* Supports union types, array types, and validation options.
|
|
589
|
+
*/
|
|
590
|
+
class TypeSpec {
|
|
591
|
+
#specs
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Creates a new TypeSpec instance.
|
|
595
|
+
*
|
|
596
|
+
* @param {string} string - The type specification string (e.g., "string|number", "object[]")
|
|
597
|
+
*/
|
|
598
|
+
constructor(string) {
|
|
599
|
+
this.#specs = [];
|
|
600
|
+
this.#parse(string);
|
|
601
|
+
Object.freeze(this.#specs);
|
|
602
|
+
this.specs = this.#specs;
|
|
603
|
+
this.length = this.#specs.length;
|
|
604
|
+
this.stringRepresentation = this.toString();
|
|
605
|
+
Object.freeze(this);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Returns a string representation of the type specification.
|
|
610
|
+
*
|
|
611
|
+
* @returns {string} The type specification as a string (e.g., "string|number[]")
|
|
612
|
+
*/
|
|
613
|
+
toString() {
|
|
614
|
+
// Reconstruct in parse order, grouping consecutive mixed specs
|
|
615
|
+
const parts = [];
|
|
616
|
+
const emittedGroups = new Set();
|
|
617
|
+
|
|
618
|
+
for(const spec of this.#specs) {
|
|
619
|
+
if(spec.mixed === false) {
|
|
620
|
+
parts.push(`${spec.typeName}${spec.array ? "[]" : ""}`);
|
|
621
|
+
} else if(!emittedGroups.has(spec.mixed)) {
|
|
622
|
+
emittedGroups.add(spec.mixed);
|
|
623
|
+
const group = this.#specs.filter(s => s.mixed === spec.mixed);
|
|
624
|
+
parts.push(`(${group.map(s => s.typeName).join("|")})[]`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return parts.join("|")
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Returns a JSON representation of the TypeSpec.
|
|
633
|
+
*
|
|
634
|
+
* @returns {unknown} Object containing specs, length, and string representation
|
|
635
|
+
*/
|
|
636
|
+
toJSON() {
|
|
637
|
+
// Serialize as a string representation or as raw data
|
|
638
|
+
return {
|
|
639
|
+
specs: this.#specs,
|
|
640
|
+
length: this.length,
|
|
641
|
+
stringRepresentation: this.toString(),
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Executes a provided function once for each type specification.
|
|
647
|
+
*
|
|
648
|
+
* @param {function(unknown): void} callback - Function to execute for each spec
|
|
649
|
+
*/
|
|
650
|
+
forEach(callback) {
|
|
651
|
+
this.#specs.forEach(callback);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Tests whether all type specifications pass the provided test function.
|
|
656
|
+
*
|
|
657
|
+
* @param {function(unknown): boolean} callback - Function to test each spec
|
|
658
|
+
* @returns {boolean} True if all specs pass the test
|
|
659
|
+
*/
|
|
660
|
+
every(callback) {
|
|
661
|
+
return this.#specs.every(callback)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Tests whether at least one type specification passes the provided test function.
|
|
666
|
+
*
|
|
667
|
+
* @param {function(unknown): boolean} callback - Function to test each spec
|
|
668
|
+
* @returns {boolean} True if at least one spec passes the test
|
|
669
|
+
*/
|
|
670
|
+
some(callback) {
|
|
671
|
+
return this.#specs.some(callback)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Creates a new array with all type specifications that pass the provided test function.
|
|
676
|
+
*
|
|
677
|
+
* @param {function(unknown): boolean} callback - Function to test each spec
|
|
678
|
+
* @returns {Array<unknown>} New array with filtered specs
|
|
679
|
+
*/
|
|
680
|
+
filter(callback) {
|
|
681
|
+
return this.#specs.filter(callback)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Creates a new array populated with the results of calling the provided function on every spec.
|
|
686
|
+
*
|
|
687
|
+
* @param {function(unknown): unknown} callback - Function to call on each spec
|
|
688
|
+
* @returns {Array<unknown>} New array with mapped values
|
|
689
|
+
*/
|
|
690
|
+
map(callback) {
|
|
691
|
+
return this.#specs.map(callback)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Executes a reducer function on each spec, resulting in a single output value.
|
|
696
|
+
*
|
|
697
|
+
* @param {function(unknown, unknown): unknown} callback - Function to execute on each spec
|
|
698
|
+
* @param {unknown} initialValue - Initial value for the accumulator
|
|
699
|
+
* @returns {unknown} The final accumulated value
|
|
700
|
+
*/
|
|
701
|
+
reduce(callback, initialValue) {
|
|
702
|
+
return this.#specs.reduce(callback, initialValue)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Returns the first type specification that satisfies the provided testing function.
|
|
707
|
+
*
|
|
708
|
+
* @param {function(unknown): boolean} callback - Function to test each spec
|
|
709
|
+
* @returns {object|undefined} The first spec that matches, or undefined
|
|
710
|
+
*/
|
|
711
|
+
find(callback) {
|
|
712
|
+
return this.#specs.find(callback)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Tests whether a value matches any of the type specifications.
|
|
717
|
+
* Handles array types, union types, and empty value validation.
|
|
718
|
+
*
|
|
719
|
+
* @param {unknown} value - The value to test against the type specifications
|
|
720
|
+
* @param {TypeMatchOptions} [options] - Validation options
|
|
721
|
+
* @returns {boolean} True if the value matches any type specification
|
|
722
|
+
*/
|
|
723
|
+
matches(value, options) {
|
|
724
|
+
return this.match(value, options).length > 0
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Options that can be passed to {@link TypeSpec.match}
|
|
729
|
+
*
|
|
730
|
+
* @typedef {object} TypeMatchOptions
|
|
731
|
+
* @property {boolean} [allowEmpty=true] - Permit a spec of {@link Data.emptyableTypes} to be empty
|
|
732
|
+
*/
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Returns matching type specifications for a value.
|
|
736
|
+
*
|
|
737
|
+
* @param {unknown} value - The value to test against the type specifications
|
|
738
|
+
* @param {TypeMatchOptions} [options] - Validation options
|
|
739
|
+
* @returns {Array<object>} Array of matching type specifications
|
|
740
|
+
*/
|
|
741
|
+
match(value, {
|
|
742
|
+
allowEmpty = true,
|
|
743
|
+
} = {}) {
|
|
744
|
+
|
|
745
|
+
// If we have a list of types, because the string was validly parsed, we
|
|
746
|
+
// need to ensure that all of the types that were parsed are valid types in
|
|
747
|
+
// JavaScript.
|
|
748
|
+
if(this.length && !this.every(t => Data.isValidType(t.typeName)))
|
|
749
|
+
return []
|
|
750
|
+
|
|
751
|
+
// Now, let's do some checking with the types, respecting the array flag
|
|
752
|
+
// with the value
|
|
753
|
+
const valueType = Data.typeOf(value);
|
|
754
|
+
const isArray = valueType === "Array";
|
|
755
|
+
|
|
756
|
+
// We need to ensure that we match the type and the consistency of the
|
|
757
|
+
// types in an array, if it is an array and an array is allowed.
|
|
758
|
+
const matchingTypeSpec = this.filter(spec => {
|
|
759
|
+
// Skip mixed specs — they are handled in the grouped-array check below
|
|
760
|
+
if(spec.mixed !== false)
|
|
761
|
+
return false
|
|
762
|
+
|
|
763
|
+
const {typeName: allowedType, array: allowedArray} = spec;
|
|
764
|
+
const empty = Data.emptyableTypes.includes(allowedType)
|
|
765
|
+
&& Data.isEmpty(value);
|
|
766
|
+
|
|
767
|
+
// Handle non-array values
|
|
768
|
+
if(!isArray && !allowedArray) {
|
|
769
|
+
if(valueType === allowedType)
|
|
770
|
+
return allowEmpty || !empty
|
|
771
|
+
|
|
772
|
+
if(valueType === "Null" || valueType === "Undefined")
|
|
773
|
+
return false
|
|
774
|
+
|
|
775
|
+
if(allowedType === "Object" && Data.isPlainObject(value))
|
|
776
|
+
return true
|
|
777
|
+
|
|
778
|
+
// We already don't match directly, let's check their breeding.
|
|
779
|
+
const lineage = this.#getTypeLineage(value);
|
|
780
|
+
|
|
781
|
+
return lineage.includes(allowedType)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Handle array values
|
|
785
|
+
if(isArray) {
|
|
786
|
+
// Special case for generic "Array" type
|
|
787
|
+
if(allowedType === "Array" && !allowedArray)
|
|
788
|
+
return allowEmpty || !empty
|
|
789
|
+
|
|
790
|
+
// Must be an array type specification
|
|
791
|
+
if(!allowedArray)
|
|
792
|
+
return false
|
|
793
|
+
|
|
794
|
+
// Handle empty arrays
|
|
795
|
+
if(empty)
|
|
796
|
+
return allowEmpty
|
|
797
|
+
|
|
798
|
+
// Check if array elements match the required type
|
|
799
|
+
return Collection.isArrayUniform(value, allowedType)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return false
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Check mixed-array groups independently. Each group (e.g.,
|
|
806
|
+
// (String|Number)[] vs (Boolean|Bigint)[]) is validated separately
|
|
807
|
+
// so that multiple groups don't merge into one.
|
|
808
|
+
if(isArray) {
|
|
809
|
+
const mixedSpecs = this.filter(spec => spec.mixed !== false);
|
|
810
|
+
|
|
811
|
+
if(mixedSpecs.length) {
|
|
812
|
+
const empty = Data.isEmpty(value);
|
|
813
|
+
|
|
814
|
+
if(empty)
|
|
815
|
+
return allowEmpty ? [...matchingTypeSpec, ...mixedSpecs] : []
|
|
816
|
+
|
|
817
|
+
// Collect unique group IDs
|
|
818
|
+
const groups = [...new Set(mixedSpecs.map(s => s.mixed))];
|
|
819
|
+
|
|
820
|
+
for(const gid of groups) {
|
|
821
|
+
const groupSpecs = mixedSpecs.filter(s => s.mixed === gid);
|
|
822
|
+
|
|
823
|
+
const allMatch = value.every(element => {
|
|
824
|
+
const elType = Data.typeOf(element);
|
|
825
|
+
|
|
826
|
+
return groupSpecs.some(spec => {
|
|
827
|
+
if(spec.typeName === "Object")
|
|
828
|
+
return Data.isPlainObject(element)
|
|
829
|
+
|
|
830
|
+
return elType === spec.typeName
|
|
831
|
+
})
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
if(allMatch)
|
|
835
|
+
return [...matchingTypeSpec, ...groupSpecs]
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return matchingTypeSpec
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Parses a type specification string into individual type specs.
|
|
845
|
+
* Handles union types separated by delimiters and array notation.
|
|
846
|
+
*
|
|
847
|
+
* @private
|
|
848
|
+
* @param {string} string - The type specification string to parse
|
|
849
|
+
* @throws {Sass} If the type specification is invalid
|
|
850
|
+
*/
|
|
851
|
+
#parse(string) {
|
|
852
|
+
const specs = [];
|
|
853
|
+
const groupPattern = /\((\w+(?:\|\w+)*)\)\[\]/g;
|
|
854
|
+
|
|
855
|
+
// Replace groups with placeholder X to validate structure and
|
|
856
|
+
// determine parse order
|
|
857
|
+
const groups = [];
|
|
858
|
+
const stripped = string.replace(groupPattern, (_, inner) => {
|
|
859
|
+
groups.push(inner);
|
|
860
|
+
|
|
861
|
+
return "X"
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// Validate for malformed delimiters and missing boundaries
|
|
865
|
+
if(/\|\||^\||\|$/.test(stripped) || /[^|]X|X[^|]/.test(stripped))
|
|
866
|
+
throw Sass.new(`Invalid type: ${string}`)
|
|
867
|
+
|
|
868
|
+
// Parse in order using the stripped template
|
|
869
|
+
const segments = stripped.split("|");
|
|
870
|
+
let groupId = 0;
|
|
871
|
+
|
|
872
|
+
for(const segment of segments) {
|
|
873
|
+
if(segment === "X") {
|
|
874
|
+
const currentGroup = groupId++;
|
|
875
|
+
const inner = groups[currentGroup];
|
|
876
|
+
|
|
877
|
+
for(const raw of inner.split("|")) {
|
|
878
|
+
const typeName = Util.capitalize(raw);
|
|
879
|
+
|
|
880
|
+
if(!Data.isValidType(typeName))
|
|
881
|
+
throw Sass.new(`Invalid type: ${raw}`)
|
|
882
|
+
|
|
883
|
+
specs.push({typeName, array: true, mixed: currentGroup});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
continue
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const typeMatches = /^(\w+)(\[\])?$/.exec(segment);
|
|
890
|
+
|
|
891
|
+
if(!typeMatches || typeMatches.length !== 3)
|
|
892
|
+
throw Sass.new(`Invalid type: ${segment}`)
|
|
893
|
+
|
|
894
|
+
const typeName = Util.capitalize(typeMatches[1]);
|
|
895
|
+
|
|
896
|
+
if(!Data.isValidType(typeName))
|
|
897
|
+
throw Sass.new(`Invalid type: ${typeMatches[1]}`)
|
|
898
|
+
|
|
899
|
+
specs.push({
|
|
900
|
+
typeName,
|
|
901
|
+
array: typeMatches[2] === "[]",
|
|
902
|
+
mixed: false,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
this.#specs = specs;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
#getTypeLineage(value) {
|
|
910
|
+
const lineage = [Object.getPrototypeOf(value)];
|
|
911
|
+
const names = [lineage.at(-1).constructor.name];
|
|
912
|
+
|
|
913
|
+
for(;;) {
|
|
914
|
+
const prototype = Object.getPrototypeOf(lineage.at(-1));
|
|
915
|
+
const name = prototype?.constructor.name;
|
|
916
|
+
|
|
917
|
+
if(!prototype || !name || name === "Object")
|
|
918
|
+
break
|
|
919
|
+
|
|
920
|
+
lineage.push(prototype);
|
|
921
|
+
names.push(prototype.constructor.name);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return names
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* @file Data utility functions for type checking, object manipulation, and
|
|
930
|
+
* array operations.
|
|
931
|
+
*
|
|
932
|
+
* Provides comprehensive utilities for working with JavaScript data types and
|
|
933
|
+
* structures.
|
|
934
|
+
*/
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
class Data {
|
|
938
|
+
/**
|
|
939
|
+
* Array of JavaScript primitive type names.
|
|
940
|
+
* Includes basic types and object categories from the typeof operator.
|
|
941
|
+
*
|
|
942
|
+
* @type {Array<string>}
|
|
943
|
+
*/
|
|
944
|
+
static primitives = Object.freeze([
|
|
945
|
+
// Primitives
|
|
946
|
+
"Bigint",
|
|
947
|
+
"Boolean",
|
|
948
|
+
"Class",
|
|
949
|
+
"Null",
|
|
950
|
+
"Number",
|
|
951
|
+
"String",
|
|
952
|
+
"Symbol",
|
|
953
|
+
"Undefined",
|
|
954
|
+
|
|
955
|
+
// Object Categories from typeof
|
|
956
|
+
"Function",
|
|
957
|
+
"Object",
|
|
958
|
+
])
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Array of JavaScript constructor names for built-in objects.
|
|
962
|
+
* Includes common object types and typed arrays.
|
|
963
|
+
*
|
|
964
|
+
* @type {Array<string>}
|
|
965
|
+
*/
|
|
966
|
+
static constructors = Object.freeze([
|
|
967
|
+
// Object Constructors
|
|
968
|
+
"Array",
|
|
969
|
+
"Date",
|
|
970
|
+
"Error",
|
|
971
|
+
"Float32Array",
|
|
972
|
+
"Float64Array",
|
|
973
|
+
"Function",
|
|
974
|
+
"Int8Array",
|
|
975
|
+
"Map",
|
|
976
|
+
"Object",
|
|
977
|
+
"Promise",
|
|
978
|
+
"RegExp",
|
|
979
|
+
"Set",
|
|
980
|
+
"Uint8Array",
|
|
981
|
+
"WeakMap",
|
|
982
|
+
"WeakSet",
|
|
983
|
+
])
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Combined array of all supported data types (primitives and constructors in
|
|
987
|
+
* lowercase).
|
|
988
|
+
*
|
|
989
|
+
* Used for type validation throughout the utility functions.
|
|
990
|
+
*
|
|
991
|
+
* @type {Array<string>}
|
|
992
|
+
*/
|
|
993
|
+
static dataTypes = Object.freeze([
|
|
994
|
+
...Data.primitives,
|
|
995
|
+
...Data.constructors
|
|
996
|
+
])
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Array of type names that can be checked for emptiness.
|
|
1000
|
+
* These types have meaningful empty states that can be tested.
|
|
1001
|
+
*
|
|
1002
|
+
* @type {Array<string>}
|
|
1003
|
+
*/
|
|
1004
|
+
static emptyableTypes = Object.freeze(["String", "Array", "Object", "Map", "Set"])
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Appends a string to another string if it does not already end with it.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {string} string - The string to append to
|
|
1010
|
+
* @param {string} append - The string to append
|
|
1011
|
+
* @returns {string} The appended string
|
|
1012
|
+
*/
|
|
1013
|
+
static append(string, append) {
|
|
1014
|
+
return string.endsWith(append)
|
|
1015
|
+
? string :
|
|
1016
|
+
`${string}${append}`
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Prepends a string to another string if it does not already start with it.
|
|
1021
|
+
*
|
|
1022
|
+
* @param {string} string - The string to prepend to
|
|
1023
|
+
* @param {string} prepend - The string to prepend
|
|
1024
|
+
* @returns {string} The prepended string
|
|
1025
|
+
*/
|
|
1026
|
+
static prepend(string, prepend) {
|
|
1027
|
+
return string.startsWith(prepend)
|
|
1028
|
+
? string
|
|
1029
|
+
: `${prepend}${string}`
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Remove a suffix from the end of a string if present.
|
|
1034
|
+
*
|
|
1035
|
+
* @param {string} string - The string to process
|
|
1036
|
+
* @param {string} toChop - The suffix to remove from the end
|
|
1037
|
+
* @param {boolean} [caseInsensitive=false] - Whether to perform case-insensitive matching
|
|
1038
|
+
* @returns {string} The string with suffix removed, or original if suffix not found
|
|
1039
|
+
* @example
|
|
1040
|
+
* Data.chopRight("hello.txt", ".txt") // "hello"
|
|
1041
|
+
* Data.chopRight("Hello", "o") // "Hell"
|
|
1042
|
+
* Data.chopRight("HELLO", "lo", true) // "HEL"
|
|
1043
|
+
*/
|
|
1044
|
+
static chopRight(string, toChop, caseInsensitive=false) {
|
|
1045
|
+
const escaped = toChop.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1046
|
+
const regex = new RegExp(`${escaped}$`, caseInsensitive === true ? "i" : "");
|
|
1047
|
+
|
|
1048
|
+
return string.replace(regex, "")
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Remove a prefix from the beginning of a string if present.
|
|
1053
|
+
*
|
|
1054
|
+
* @param {string} string - The string to process
|
|
1055
|
+
* @param {string} toChop - The prefix to remove from the beginning
|
|
1056
|
+
* @param {boolean} [caseInsensitive=false] - Whether to perform case-insensitive matching
|
|
1057
|
+
* @returns {string} The string with prefix removed, or original if prefix not found
|
|
1058
|
+
* @example
|
|
1059
|
+
* Data.chopLeft("hello.txt", "hello") // ".txt"
|
|
1060
|
+
* Data.chopLeft("Hello", "H") // "ello"
|
|
1061
|
+
* Data.chopLeft("HELLO", "he", true) // "LLO"
|
|
1062
|
+
*/
|
|
1063
|
+
static chopLeft(string, toChop, caseInsensitive=false) {
|
|
1064
|
+
const escaped = toChop.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1065
|
+
const regex = new RegExp(`^${escaped}`, caseInsensitive === true ? "i" : "");
|
|
1066
|
+
|
|
1067
|
+
return string.replace(regex, "")
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Chop a string after the first occurence of another string.
|
|
1072
|
+
*
|
|
1073
|
+
* @param {string} string - The string to search
|
|
1074
|
+
* @param {string} needle - The bit to chop after
|
|
1075
|
+
* @param {boolean} caseInsensitive - Whether to search insensitive to case
|
|
1076
|
+
* @returns {string} The remaining string
|
|
1077
|
+
*/
|
|
1078
|
+
static chopAfter(string, needle, caseInsensitive=false) {
|
|
1079
|
+
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1080
|
+
const regex = new RegExp(`${escaped}`, caseInsensitive === true ? "i" : "");
|
|
1081
|
+
const index = string.search(regex);
|
|
1082
|
+
|
|
1083
|
+
if(index === -1)
|
|
1084
|
+
return string
|
|
1085
|
+
|
|
1086
|
+
return string.slice(0, index)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Chop a string before the first occurrence of another string.
|
|
1091
|
+
*
|
|
1092
|
+
* @param {string} string - The string to search
|
|
1093
|
+
* @param {string} needle - The bit to chop before
|
|
1094
|
+
* @param {boolean} caseInsensitive - Whether to search insensitive to case
|
|
1095
|
+
* @returns {string} The remaining string
|
|
1096
|
+
*/
|
|
1097
|
+
static chopBefore(string, needle, caseInsensitive=false) {
|
|
1098
|
+
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1099
|
+
const regex = new RegExp(`${escaped}`, caseInsensitive === true ? "i" : "");
|
|
1100
|
+
const length = needle.length;
|
|
1101
|
+
const index = string.search(regex);
|
|
1102
|
+
|
|
1103
|
+
if(index === -1)
|
|
1104
|
+
return string
|
|
1105
|
+
|
|
1106
|
+
return string.slice(index + length)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Options for creating a new TypeSpec.
|
|
1111
|
+
*
|
|
1112
|
+
* @typedef {object} TypeSpecOptions
|
|
1113
|
+
* @property {string} [delimiter="|"] - The delimiter for union types
|
|
1114
|
+
*/
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Options for type validation methods.
|
|
1118
|
+
*
|
|
1119
|
+
* @typedef {object} TypeValidationOptions
|
|
1120
|
+
* @property {boolean} [allowEmpty=true] - Whether empty values are allowed
|
|
1121
|
+
*/
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Creates a type spec from a string. A type spec is an array of objects
|
|
1125
|
+
* defining the type of a value and whether an array is expected.
|
|
1126
|
+
*
|
|
1127
|
+
* @param {string} string - The string to parse into a type spec.
|
|
1128
|
+
* @returns {TypeSpec} A new TypeSpec instance.
|
|
1129
|
+
*/
|
|
1130
|
+
static newTypeSpec(string) {
|
|
1131
|
+
return new TypeSpec(string)
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Checks if a value is of a specified type
|
|
1136
|
+
*
|
|
1137
|
+
* @param {unknown} value The value to check
|
|
1138
|
+
* @param {string|TypeSpec} type The type to check for
|
|
1139
|
+
* @param {TypeValidationOptions} [options] Additional options for checking
|
|
1140
|
+
* @returns {boolean} Whether the value is of the specified type
|
|
1141
|
+
*/
|
|
1142
|
+
static isType(value, type, options = {}) {
|
|
1143
|
+
const typeSpec = type instanceof TypeSpec
|
|
1144
|
+
? type
|
|
1145
|
+
: Data.newTypeSpec(type, options);
|
|
1146
|
+
|
|
1147
|
+
return typeSpec.matches(value, options)
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Checks if a type is valid
|
|
1152
|
+
*
|
|
1153
|
+
* @param {string} type - The type to check
|
|
1154
|
+
* @returns {boolean} Whether the type is valid
|
|
1155
|
+
*/
|
|
1156
|
+
static isValidType(type) {
|
|
1157
|
+
// Allow built-in types
|
|
1158
|
+
if(Data.dataTypes.includes(type))
|
|
1159
|
+
return true
|
|
1160
|
+
|
|
1161
|
+
// Allow custom classes (PascalCase starting with capital letter)
|
|
1162
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(type)
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Checks if a value is of a specified type. Unlike the type function, this
|
|
1167
|
+
* function does not parse the type string, and only checks for primitive
|
|
1168
|
+
* or constructor types.
|
|
1169
|
+
*
|
|
1170
|
+
* @param {unknown} value - The value to check
|
|
1171
|
+
* @param {string} type - The type to check for
|
|
1172
|
+
* @returns {boolean} Whether the value is of the specified type
|
|
1173
|
+
*/
|
|
1174
|
+
static isBaseType(value, type) {
|
|
1175
|
+
if(!Data.isValidType(type))
|
|
1176
|
+
return false
|
|
1177
|
+
|
|
1178
|
+
const valueType = Data.typeOf(value);
|
|
1179
|
+
|
|
1180
|
+
// Special cases that need extra validation
|
|
1181
|
+
switch(valueType) {
|
|
1182
|
+
case "Number":
|
|
1183
|
+
return type === "Number" && !isNaN(value) // Excludes NaN
|
|
1184
|
+
default:
|
|
1185
|
+
return valueType === type
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Returns the type of a value, whether it be a primitive, object, or
|
|
1191
|
+
* function.
|
|
1192
|
+
*
|
|
1193
|
+
* @param {unknown} value - The value to check
|
|
1194
|
+
* @returns {string} The type of the value
|
|
1195
|
+
*/
|
|
1196
|
+
static typeOf(value) {
|
|
1197
|
+
if(value === null)
|
|
1198
|
+
return "Null"
|
|
1199
|
+
|
|
1200
|
+
const type = typeof value;
|
|
1201
|
+
|
|
1202
|
+
if(type === "object")
|
|
1203
|
+
return value.constructor?.name ?? "Object"
|
|
1204
|
+
|
|
1205
|
+
if(typeof value === "function"
|
|
1206
|
+
&& Object.getOwnPropertyDescriptor(value, "prototype")?.writable === false
|
|
1207
|
+
&& /^class[\s{]/.test(Function.prototype.toString.call(value))) {
|
|
1208
|
+
return "Class"
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const [first, ...rest] = Array.from(type);
|
|
1212
|
+
|
|
1213
|
+
return `${first?.toLocaleUpperCase() ?? ""}${rest.join("")}`
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Checks a value is undefined or null.
|
|
1218
|
+
*
|
|
1219
|
+
* @param {unknown} value The value to check
|
|
1220
|
+
* @returns {boolean} Whether the value is undefined or null
|
|
1221
|
+
*/
|
|
1222
|
+
static isNothing(value) {
|
|
1223
|
+
return value === undefined || value === null
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Checks if a value is empty. This function is used to check if an object,
|
|
1228
|
+
* array, or string is empty. Null and undefined values are considered empty.
|
|
1229
|
+
*
|
|
1230
|
+
* @param {unknown} value The value to check
|
|
1231
|
+
* @param {boolean} checkForNothing Whether to check for null or undefined
|
|
1232
|
+
* values
|
|
1233
|
+
* @returns {boolean} Whether the value is empty
|
|
1234
|
+
*/
|
|
1235
|
+
static isEmpty(value, checkForNothing = true) {
|
|
1236
|
+
if(checkForNothing && Data.isNothing(value))
|
|
1237
|
+
return true
|
|
1238
|
+
|
|
1239
|
+
// When checkForNothing is false, null/undefined should not be treated as empty
|
|
1240
|
+
// They should be processed like regular values
|
|
1241
|
+
if(!checkForNothing && Data.isNothing(value))
|
|
1242
|
+
return false
|
|
1243
|
+
|
|
1244
|
+
const type = Data.typeOf(value);
|
|
1245
|
+
|
|
1246
|
+
if(!Data.emptyableTypes.includes(type))
|
|
1247
|
+
return false
|
|
1248
|
+
|
|
1249
|
+
switch(type) {
|
|
1250
|
+
case "Array":
|
|
1251
|
+
return value.length === 0
|
|
1252
|
+
case "Object":
|
|
1253
|
+
// null was already handled above, so this should only be real objects
|
|
1254
|
+
return Object.keys(value).length === 0
|
|
1255
|
+
case "String":
|
|
1256
|
+
return value.trim().length === 0
|
|
1257
|
+
case "Map":
|
|
1258
|
+
case "Set":
|
|
1259
|
+
return value.size === 0
|
|
1260
|
+
default:
|
|
1261
|
+
return false
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Freezes an object and all of its properties recursively.
|
|
1267
|
+
*
|
|
1268
|
+
* @param {object} obj The object to freeze.
|
|
1269
|
+
* @returns {object} The frozen object.
|
|
1270
|
+
*/
|
|
1271
|
+
static deepFreezeObject(obj) {
|
|
1272
|
+
if(obj === null || typeof obj !== "object")
|
|
1273
|
+
return obj // Skip null and non-objects
|
|
1274
|
+
|
|
1275
|
+
// Retrieve and freeze properties
|
|
1276
|
+
const propNames = Object.getOwnPropertyNames(obj);
|
|
1277
|
+
|
|
1278
|
+
for(const name of propNames) {
|
|
1279
|
+
const value = obj[name];
|
|
1280
|
+
|
|
1281
|
+
// Recursively freeze nested objects
|
|
1282
|
+
if(typeof value === "object" && value !== null)
|
|
1283
|
+
Data.deepFreezeObject(value);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Freeze the object itself
|
|
1287
|
+
return Object.freeze(obj)
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Ensures that a nested path of objects exists within the given object.
|
|
1292
|
+
* Creates empty objects along the path if they don't exist.
|
|
1293
|
+
*
|
|
1294
|
+
* @param {object} obj - The object to check/modify
|
|
1295
|
+
* @param {Array<string>} keys - Array of keys representing the path to ensure
|
|
1296
|
+
* @returns {object} Reference to the deepest nested object in the path
|
|
1297
|
+
*/
|
|
1298
|
+
static assureObjectPath(obj, keys) {
|
|
1299
|
+
let current = obj; // a moving reference to internal objects within obj
|
|
1300
|
+
const len = keys.length;
|
|
1301
|
+
|
|
1302
|
+
for(let i = 0; i < len; i++) {
|
|
1303
|
+
const elem = keys[i];
|
|
1304
|
+
|
|
1305
|
+
if(!current[elem])
|
|
1306
|
+
current[elem] = {};
|
|
1307
|
+
|
|
1308
|
+
current = current[elem];
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Return the current pointer
|
|
1312
|
+
return current
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Sets a value in a nested object structure using an array of keys; creating
|
|
1317
|
+
* the structure if it does not exist.
|
|
1318
|
+
*
|
|
1319
|
+
* @param {object} obj - The target object to set the value in
|
|
1320
|
+
* @param {Array<string>} keys - Array of keys representing the path to the target property
|
|
1321
|
+
* @param {unknown} value - The value to set at the target location
|
|
1322
|
+
*/
|
|
1323
|
+
static setNestedValue(obj, keys, value) {
|
|
1324
|
+
const nested = Data.assureObjectPath(obj, keys.slice(0, -1));
|
|
1325
|
+
|
|
1326
|
+
nested[keys[keys.length-1]] = value;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Deeply merges two or more objects. Arrays are replaced, not merged.
|
|
1331
|
+
*
|
|
1332
|
+
* @param {...object} sources - Objects to merge (left to right)
|
|
1333
|
+
* @returns {object} The merged object
|
|
1334
|
+
*/
|
|
1335
|
+
static mergeObject(...sources) {
|
|
1336
|
+
const isObject = obj => typeof obj === "object" && obj !== null && !Array.isArray(obj);
|
|
1337
|
+
|
|
1338
|
+
return sources.reduce((acc, obj) => {
|
|
1339
|
+
if(!isObject(obj))
|
|
1340
|
+
return acc
|
|
1341
|
+
|
|
1342
|
+
Object.keys(obj).forEach(key => {
|
|
1343
|
+
const accVal = acc[key];
|
|
1344
|
+
const objVal = obj[key];
|
|
1345
|
+
|
|
1346
|
+
if(isObject(accVal) && isObject(objVal))
|
|
1347
|
+
acc[key] = Data.mergeObject(accVal, objVal);
|
|
1348
|
+
else
|
|
1349
|
+
acc[key] = objVal;
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
return acc
|
|
1353
|
+
}, {})
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Filters an array asynchronously using a predicate function.
|
|
1358
|
+
* Applies the predicate to all items in parallel and returns filtered results.
|
|
1359
|
+
*
|
|
1360
|
+
* @param {Array<unknown>} arr - The array to filter
|
|
1361
|
+
* @param {(value: unknown) => Promise<boolean>} predicate - Async predicate function that returns a promise resolving to boolean
|
|
1362
|
+
* @returns {Promise<Array<unknown>>} Promise resolving to the filtered array
|
|
1363
|
+
*/
|
|
1364
|
+
static async asyncFilter(arr, predicate) {
|
|
1365
|
+
const results = await Promise.all(arr.map(predicate));
|
|
1366
|
+
|
|
1367
|
+
return arr.filter((_, index) => results[index])
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Ensures a value is within a specified range.
|
|
1372
|
+
*
|
|
1373
|
+
* @param {number} val - The value to check.
|
|
1374
|
+
* @param {number} min - The minimum value.
|
|
1375
|
+
* @param {number} max - The maximum value.
|
|
1376
|
+
* @returns {number} The value, constrained within the range of `min` to `max`.
|
|
1377
|
+
*/
|
|
1378
|
+
static clamp(val, min, max) {
|
|
1379
|
+
return val >= min ? val <= max ? val : max : min
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Checks if a value is within a specified range (inclusive).
|
|
1384
|
+
*
|
|
1385
|
+
* @param {number} val - The value to check.
|
|
1386
|
+
* @param {number} min - The minimum value (inclusive).
|
|
1387
|
+
* @param {number} max - The maximum value (inclusive).
|
|
1388
|
+
* @returns {boolean} True if the value is within the range, false otherwise.
|
|
1389
|
+
*/
|
|
1390
|
+
static clamped(val, min, max) {
|
|
1391
|
+
return val >= min && val <= max
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Checks if a value is a plain object - created with object literals {},
|
|
1396
|
+
* new Object(), or Object.create(null).
|
|
1397
|
+
*
|
|
1398
|
+
* Distinguishes plain objects from objects created by custom constructors, built-ins,
|
|
1399
|
+
* or primitives. Plain objects only have Object.prototype or null in their prototype chain.
|
|
1400
|
+
*
|
|
1401
|
+
* @param {unknown} value - The value to check
|
|
1402
|
+
* @returns {boolean} True if the value is a plain object, false otherwise
|
|
1403
|
+
*
|
|
1404
|
+
* @example
|
|
1405
|
+
* isPlainObject({}) // true
|
|
1406
|
+
* isPlainObject(new Object()) // true
|
|
1407
|
+
* isPlainObject(Object.create(null)) // true
|
|
1408
|
+
* isPlainObject([]) // false
|
|
1409
|
+
* isPlainObject(new Date()) // false
|
|
1410
|
+
* isPlainObject(null) // false
|
|
1411
|
+
* isPlainObject("string") // false
|
|
1412
|
+
* isPlainObject(class Person{}) // false
|
|
1413
|
+
*/
|
|
1414
|
+
static isPlainObject(value) {
|
|
1415
|
+
// First, check if it's an object and not null
|
|
1416
|
+
if(typeof value !== "object" || value === null)
|
|
1417
|
+
return false
|
|
1418
|
+
|
|
1419
|
+
// If it has no prototype, it's plain (created with Object.create(null))
|
|
1420
|
+
const proto = Object.getPrototypeOf(value);
|
|
1421
|
+
|
|
1422
|
+
if(proto === null)
|
|
1423
|
+
return true
|
|
1424
|
+
|
|
1425
|
+
// Check if the prototype chain ends at Object.prototype
|
|
1426
|
+
// This handles objects created with {} or new Object()
|
|
1427
|
+
let current = proto;
|
|
1428
|
+
|
|
1429
|
+
while(Object.getPrototypeOf(current) !== null)
|
|
1430
|
+
current = Object.getPrototypeOf(current);
|
|
1431
|
+
|
|
1432
|
+
return proto === current
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Checks if a value is binary data.
|
|
1437
|
+
* Returns true for ArrayBuffer, TypedArrays (Uint8Array, Int16Array, etc.),
|
|
1438
|
+
* Blob, and Node Buffer instances.
|
|
1439
|
+
*
|
|
1440
|
+
* @param {unknown} value - The value to check
|
|
1441
|
+
* @returns {boolean} True if the value is binary data, false otherwise
|
|
1442
|
+
* @example
|
|
1443
|
+
* Data.isBinary(new Uint8Array([1, 2, 3])) // true
|
|
1444
|
+
* Data.isBinary(new ArrayBuffer(10)) // true
|
|
1445
|
+
* Data.isBinary(Buffer.from('hello')) // true
|
|
1446
|
+
* Data.isBinary(new Blob(['text'])) // true
|
|
1447
|
+
* Data.isBinary('string') // false
|
|
1448
|
+
* Data.isBinary({}) // false
|
|
1449
|
+
* Data.isBinary(undefined) // false
|
|
1450
|
+
*/
|
|
1451
|
+
static isBinary(value) {
|
|
1452
|
+
return (value !== undefined) &&
|
|
1453
|
+
(
|
|
1454
|
+
ArrayBuffer.isView(value) ||
|
|
1455
|
+
Data.isType(value, "ArrayBuffer|Blob|Buffer")
|
|
1456
|
+
)
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* @file Collection.js
|
|
1463
|
+
*
|
|
1464
|
+
* Provides utility functions for working with collections (arrays, objects, sets, maps).
|
|
1465
|
+
* Includes methods for iteration, transformation, validation, and manipulation of
|
|
1466
|
+
* various collection types.
|
|
1467
|
+
*/
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Utility class for collection operations.
|
|
1472
|
+
* Provides static methods for working with arrays, objects, sets, and maps.
|
|
1473
|
+
*/
|
|
1474
|
+
class Collection {
|
|
1475
|
+
/**
|
|
1476
|
+
* Evaluates an array with a predicate function, optionally in reverse order.
|
|
1477
|
+
* Returns the first truthy result from the predicate.
|
|
1478
|
+
*
|
|
1479
|
+
* @param {Array<unknown>} collection - The array to evaluate
|
|
1480
|
+
* @param {(value: unknown, index: number, array: Array<unknown>) => unknown} predicate - Function to evaluate each element
|
|
1481
|
+
* @param {boolean} [forward] - Whether to iterate forward (true) or backward (false). Defaults to true
|
|
1482
|
+
* @returns {unknown|undefined} The first truthy result from the predicate, or undefined
|
|
1483
|
+
* @throws {Sass} If collection is not an array or predicate is not a function
|
|
1484
|
+
*/
|
|
1485
|
+
static evalArray(collection, predicate, forward=true) {
|
|
1486
|
+
const req = "Array";
|
|
1487
|
+
const type = Data.typeOf(collection);
|
|
1488
|
+
|
|
1489
|
+
Valid.type(collection, req, `Invalid collection. Expected '${req}, got ${type}`);
|
|
1490
|
+
Valid.type(predicate, "Function",
|
|
1491
|
+
`Invalid predicate, expected 'Function', got ${Data.typeOf(predicate)}`);
|
|
1492
|
+
|
|
1493
|
+
const work = forward
|
|
1494
|
+
? Array.from(collection)
|
|
1495
|
+
: Array.from(collection).toReversed();
|
|
1496
|
+
|
|
1497
|
+
for(let i = 0; i < work.length; i++) {
|
|
1498
|
+
const result = predicate(work[i], i, collection) ?? null;
|
|
1499
|
+
|
|
1500
|
+
if(result)
|
|
1501
|
+
return result
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Evaluates an object with a predicate function.
|
|
1507
|
+
* Returns the first truthy result from the predicate.
|
|
1508
|
+
*
|
|
1509
|
+
* @param {object} collection - The object to evaluate
|
|
1510
|
+
* @param {(value: unknown, key: string, object: object) => unknown} predicate - Function to evaluate each property
|
|
1511
|
+
* @returns {unknown|undefined} The first truthy result from the predicate, or undefined
|
|
1512
|
+
* @throws {Sass} If collection is not an object or predicate is not a function
|
|
1513
|
+
*/
|
|
1514
|
+
static evalObject(collection, predicate) {
|
|
1515
|
+
const req = "Object";
|
|
1516
|
+
const type = Data.typeOf(collection);
|
|
1517
|
+
|
|
1518
|
+
Valid.type(collection, req, `Invalid collection. Expected '${req}, got ${type}`);
|
|
1519
|
+
Valid.type(predicate, "Function",
|
|
1520
|
+
`Invalid predicate, expected 'Function', got ${Data.typeOf(predicate)}`);
|
|
1521
|
+
|
|
1522
|
+
const work = Object.entries(collection);
|
|
1523
|
+
|
|
1524
|
+
for(let i = 0; i < work.length; i++) {
|
|
1525
|
+
const result = predicate(work[i][1], work[i][0], collection);
|
|
1526
|
+
|
|
1527
|
+
if(result)
|
|
1528
|
+
return result
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Evaluates a Set with a predicate function.
|
|
1534
|
+
* Returns the first truthy result from the predicate.
|
|
1535
|
+
*
|
|
1536
|
+
* @param {Set<unknown>} collection - The Set to evaluate
|
|
1537
|
+
* @param {(value: unknown, set: Set<unknown>) => unknown} predicate - Function to evaluate each element
|
|
1538
|
+
* @returns {unknown|undefined} The first truthy result from the predicate, or undefined
|
|
1539
|
+
* @throws {Sass} If collection is not a Set or predicate is not a function
|
|
1540
|
+
*/
|
|
1541
|
+
static evalSet(collection, predicate) {
|
|
1542
|
+
const req = "Set";
|
|
1543
|
+
const type = Data.typeOf(collection);
|
|
1544
|
+
|
|
1545
|
+
Valid.type(collection, req, `Invalid collection. Expected '${req}, got ${type}`);
|
|
1546
|
+
Valid.type(predicate, "Function",
|
|
1547
|
+
`Invalid predicate, expected 'Function', got ${Data.typeOf(predicate)}`);
|
|
1548
|
+
|
|
1549
|
+
const work = Array.from(collection);
|
|
1550
|
+
|
|
1551
|
+
for(let i = 0; i < work.length; i++) {
|
|
1552
|
+
const result = predicate(work[i], collection);
|
|
1553
|
+
|
|
1554
|
+
if(result)
|
|
1555
|
+
return result
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* Evaluates a Map with a predicate function, optionally in reverse order.
|
|
1561
|
+
* Returns the first truthy result from the predicate.
|
|
1562
|
+
*
|
|
1563
|
+
* @param {Map<unknown, unknown>} collection - The Map to evaluate
|
|
1564
|
+
* @param {(value: unknown, key: unknown, map: Map<unknown, unknown>) => unknown} predicate - Function to evaluate each entry
|
|
1565
|
+
* @param {boolean} [forward] - Whether to iterate forward (true) or backward (false). Defaults to true
|
|
1566
|
+
* @returns {unknown|undefined} The first truthy result from the predicate, or undefined
|
|
1567
|
+
* @throws {Sass} If collection is not a Map or predicate is not a function
|
|
1568
|
+
*/
|
|
1569
|
+
static evalMap(collection, predicate, forward=true) {
|
|
1570
|
+
const req = "Map";
|
|
1571
|
+
const type = Data.typeOf(collection);
|
|
1572
|
+
|
|
1573
|
+
Valid.type(collection, req, `Invalid collection. Expected '${req}, got ${type}`);
|
|
1574
|
+
Valid.type(predicate, "Function",
|
|
1575
|
+
`Invalid predicate, expected 'Function', got ${Data.typeOf(predicate)}`);
|
|
1576
|
+
|
|
1577
|
+
const work = forward
|
|
1578
|
+
? Array.from(collection)
|
|
1579
|
+
: Array.from(collection).toReversed();
|
|
1580
|
+
|
|
1581
|
+
for(let i = 0; i < work.length; i++) {
|
|
1582
|
+
const result = predicate(work[i][1], work[i][0], collection) ?? null;
|
|
1583
|
+
|
|
1584
|
+
if(result)
|
|
1585
|
+
return result
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Zips two arrays together into an array of pairs.
|
|
1591
|
+
* The resulting array length equals the shorter input array.
|
|
1592
|
+
*
|
|
1593
|
+
* @param {Array<unknown>} array1 - The first array
|
|
1594
|
+
* @param {Array<unknown>} array2 - The second array
|
|
1595
|
+
* @returns {Array<[unknown, unknown]>} Array of paired elements
|
|
1596
|
+
*/
|
|
1597
|
+
static zip(array1, array2) {
|
|
1598
|
+
const minLength = Math.min(array1.length, array2.length);
|
|
1599
|
+
|
|
1600
|
+
return Array.from({length: minLength}, (_, i) => [array1[i], array2[i]])
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Unzips an array of pairs into separate arrays.
|
|
1605
|
+
* Transposes a 2D array structure.
|
|
1606
|
+
*
|
|
1607
|
+
* @param {Array<Array<unknown>>} array - Array of arrays to unzip
|
|
1608
|
+
* @returns {Array<Array<unknown>>} Array of unzipped arrays, or empty array for invalid input
|
|
1609
|
+
*/
|
|
1610
|
+
static unzip(array) {
|
|
1611
|
+
if(!Array.isArray(array) || array.length === 0) {
|
|
1612
|
+
return [] // Handle empty or invalid input
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// Determine the number of "unzipped" arrays needed
|
|
1616
|
+
// This assumes all inner arrays have the same length, or we take the max length
|
|
1617
|
+
const numUnzippedArrays = Math.max(...array.map(arr => arr.length));
|
|
1618
|
+
|
|
1619
|
+
// Initialize an array of empty arrays to hold the unzipped results
|
|
1620
|
+
const unzipped = Array.from({length: numUnzippedArrays}, () => []);
|
|
1621
|
+
|
|
1622
|
+
// Iterate through the zipped array and populate the unzipped arrays
|
|
1623
|
+
for(let i = 0; i < array.length; i++) {
|
|
1624
|
+
for(let j = 0; j < numUnzippedArrays; j++) {
|
|
1625
|
+
unzipped[j].push(array[i][j]);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
return unzipped
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Maps an array using an async function, processing items sequentially.
|
|
1634
|
+
* Unlike Promise.all(array.map()), this processes one item at a time.
|
|
1635
|
+
*
|
|
1636
|
+
* @param {Array<unknown>} array - The array to map
|
|
1637
|
+
* @param {(item: unknown) => Promise<unknown>} asyncFn - Async function to apply to each element
|
|
1638
|
+
* @returns {Promise<Array<unknown>>} Promise resolving to the mapped array
|
|
1639
|
+
* @throws {Sass} If array is not an Array or asyncFn is not a function
|
|
1640
|
+
*/
|
|
1641
|
+
static async asyncMap(array, asyncFn) {
|
|
1642
|
+
const req = "Array";
|
|
1643
|
+
const type = Data.typeOf(array);
|
|
1644
|
+
|
|
1645
|
+
Valid.type(array, req, `Invalid array. Expected '${req}', got '${type}'`);
|
|
1646
|
+
Valid.type(asyncFn, "Function",
|
|
1647
|
+
`Invalid mapper function, expected 'Function', got '${Data.typeOf(asyncFn)}'`);
|
|
1648
|
+
|
|
1649
|
+
const results = [];
|
|
1650
|
+
|
|
1651
|
+
for(const item of array) {
|
|
1652
|
+
results.push(await asyncFn(item));
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
return results
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Checks if all elements in an array are of a specified type
|
|
1660
|
+
*
|
|
1661
|
+
* @param {Array<unknown>} arr - The array to check
|
|
1662
|
+
* @param {string} [type] - The type to check for (optional, defaults to the type of the first element)
|
|
1663
|
+
* @param {unknown} options - Options for checking types
|
|
1664
|
+
* @param {boolean} [options.strict] - Whether to use strict type or looser TypeSpec checking
|
|
1665
|
+
* @returns {boolean} Whether all elements are of the specified type
|
|
1666
|
+
*/
|
|
1667
|
+
static isArrayUniform(arr, type, options={strict: true}) {
|
|
1668
|
+
const req = "Array";
|
|
1669
|
+
const arrType = Data.typeOf(arr);
|
|
1670
|
+
|
|
1671
|
+
Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`);
|
|
1672
|
+
|
|
1673
|
+
// Validate type parameter if provided
|
|
1674
|
+
if(type !== undefined)
|
|
1675
|
+
Valid.type(type, "string", `Invalid type parameter. Expected 'string', got '${Data.typeOf(type)}'`);
|
|
1676
|
+
|
|
1677
|
+
const checkType = type ? Util.capitalize(type) : Data.typeOf(arr[0]);
|
|
1678
|
+
|
|
1679
|
+
if(options?.strict === false) {
|
|
1680
|
+
const ts = new TypeSpec(checkType);
|
|
1681
|
+
|
|
1682
|
+
return arr.every(e => ts.matches(e))
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
return arr.every(e => Data.typeOf(e) === checkType)
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Checks if an array is unique
|
|
1690
|
+
*
|
|
1691
|
+
* @param {Array<unknown>} arr - The array of which to remove duplicates
|
|
1692
|
+
* @returns {Array<unknown>} The unique elements of the array
|
|
1693
|
+
*/
|
|
1694
|
+
static isArrayUnique(arr) {
|
|
1695
|
+
const req = "Array";
|
|
1696
|
+
const arrType = Data.typeOf(arr);
|
|
1697
|
+
|
|
1698
|
+
Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`);
|
|
1699
|
+
|
|
1700
|
+
return arr.filter((item, index, self) => self.indexOf(item) === index)
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Returns the intersection of two arrays.
|
|
1705
|
+
*
|
|
1706
|
+
* @param {Array<unknown>} arr1 - The first array.
|
|
1707
|
+
* @param {Array<unknown>} arr2 - The second array.
|
|
1708
|
+
* @returns {Array<unknown>} The intersection of the two arrays.
|
|
1709
|
+
*/
|
|
1710
|
+
static intersection(arr1, arr2) {
|
|
1711
|
+
const req = "Array";
|
|
1712
|
+
const arr1Type = Data.typeOf(arr1);
|
|
1713
|
+
const arr2Type = Data.typeOf(arr2);
|
|
1714
|
+
|
|
1715
|
+
Valid.type(arr1, req, `Invalid first array. Expected '${req}', got '${arr1Type}'`);
|
|
1716
|
+
Valid.type(arr2, req, `Invalid second array. Expected '${req}', got '${arr2Type}'`);
|
|
1717
|
+
|
|
1718
|
+
const [short,long] = [arr1,arr2].sort((a,b) => a.length - b.length);
|
|
1719
|
+
|
|
1720
|
+
return short.filter(value => long.includes(value))
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* Checks whether two arrays have any elements in common.
|
|
1725
|
+
*
|
|
1726
|
+
* This function returns `true` if at least one element from `arr1` exists in
|
|
1727
|
+
* `arr2`, and `false` otherwise. It optimizes by iterating over the shorter
|
|
1728
|
+
* array for efficiency.
|
|
1729
|
+
*
|
|
1730
|
+
* Example:
|
|
1731
|
+
* Collection.intersects([1, 2, 3], [3, 4, 5]) // returns true
|
|
1732
|
+
* Collection.intersects(["a", "b"], ["c", "d"]) // returns false
|
|
1733
|
+
*
|
|
1734
|
+
* @param {Array<unknown>} arr1 - The first array to check for intersection.
|
|
1735
|
+
* @param {Array<unknown>} arr2 - The second array to check for intersection.
|
|
1736
|
+
* @returns {boolean} True if any element is shared between the arrays, false otherwise.
|
|
1737
|
+
*/
|
|
1738
|
+
static intersects(arr1, arr2) {
|
|
1739
|
+
const req = "Array";
|
|
1740
|
+
const arr1Type = Data.typeOf(arr1);
|
|
1741
|
+
const arr2Type = Data.typeOf(arr2);
|
|
1742
|
+
|
|
1743
|
+
Valid.type(arr1, req, `Invalid first array. Expected '${req}', got '${arr1Type}'`);
|
|
1744
|
+
Valid.type(arr2, req, `Invalid second array. Expected '${req}', got '${arr2Type}'`);
|
|
1745
|
+
|
|
1746
|
+
const [short,long] = [arr1,arr2].sort((a,b) => a.length - b.length);
|
|
1747
|
+
|
|
1748
|
+
return !!short.find(value => long.includes(value))
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Pads an array to a specified length with a value. This operation
|
|
1753
|
+
* occurs in-place.
|
|
1754
|
+
*
|
|
1755
|
+
* @param {Array<unknown>} arr - The array to pad.
|
|
1756
|
+
* @param {number} length - The length to pad the array to.
|
|
1757
|
+
* @param {unknown} value - The value to pad the array with.
|
|
1758
|
+
* @param {number} [position] - The position to pad the array at. Defaults to 0
|
|
1759
|
+
* @returns {Array<unknown>} The padded array.
|
|
1760
|
+
*/
|
|
1761
|
+
static arrayPad(arr, length, value, position = 0) {
|
|
1762
|
+
const req = "Array";
|
|
1763
|
+
const arrType = Data.typeOf(arr);
|
|
1764
|
+
|
|
1765
|
+
Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`);
|
|
1766
|
+
Valid.type(length, "Number", `Invalid length. Expected 'Number', got '${Data.typeOf(length)}'`);
|
|
1767
|
+
Valid.type(position, "Number", `Invalid position. Expected 'Number', got '${Data.typeOf(position)}'`);
|
|
1768
|
+
|
|
1769
|
+
const diff = length - arr.length;
|
|
1770
|
+
|
|
1771
|
+
if(diff <= 0)
|
|
1772
|
+
return arr
|
|
1773
|
+
|
|
1774
|
+
const padding = Array(diff).fill(value);
|
|
1775
|
+
|
|
1776
|
+
if(position === 0)
|
|
1777
|
+
// prepend - default
|
|
1778
|
+
return padding.concat(arr)
|
|
1779
|
+
else if(position === -1)
|
|
1780
|
+
// append
|
|
1781
|
+
return arr.concat(padding) // somewhere in the middle - THAT IS ILLEGAL
|
|
1782
|
+
else
|
|
1783
|
+
throw Sass.new("Invalid position")
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Filters an array asynchronously using a predicate function.
|
|
1788
|
+
* Applies the predicate to all items in parallel and returns filtered results.
|
|
1789
|
+
*
|
|
1790
|
+
* @param {Array<unknown>} arr - The array to filter
|
|
1791
|
+
* @param {(value: unknown, index: number, array: Array<unknown>) => Promise<boolean>} predicate - Async predicate function that returns a promise resolving to boolean
|
|
1792
|
+
* @returns {Promise<Array<unknown>>} Promise resolving to the filtered array
|
|
1793
|
+
*/
|
|
1794
|
+
static async asyncFilter(arr, predicate) {
|
|
1795
|
+
const req = "Array";
|
|
1796
|
+
const arrType = Data.typeOf(arr);
|
|
1797
|
+
|
|
1798
|
+
Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`);
|
|
1799
|
+
Valid.type(predicate, "Function",
|
|
1800
|
+
`Invalid predicate function, expected 'Function', got '${Data.typeOf(predicate)}'`);
|
|
1801
|
+
|
|
1802
|
+
const results = await Promise.all(arr.map(predicate));
|
|
1803
|
+
|
|
1804
|
+
return arr.filter((_, index) => results[index])
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* Clones an object
|
|
1809
|
+
*
|
|
1810
|
+
* @param {object} obj - The object to clone
|
|
1811
|
+
* @param {boolean} freeze - Whether to freeze the cloned object
|
|
1812
|
+
* @returns {object} The cloned object
|
|
1813
|
+
*/
|
|
1814
|
+
static cloneObject(obj, freeze = false) {
|
|
1815
|
+
const result = {};
|
|
1816
|
+
|
|
1817
|
+
for(const [key, value] of Object.entries(obj)) {
|
|
1818
|
+
if(Data.isType(value, "Array")) {
|
|
1819
|
+
// Clone arrays by mapping over them
|
|
1820
|
+
result[key] = value.map(item =>
|
|
1821
|
+
Data.isType(item, "object") || Data.isType(item, "Array")
|
|
1822
|
+
? Collection.cloneObject(item)
|
|
1823
|
+
: item
|
|
1824
|
+
);
|
|
1825
|
+
} else if(Data.isType(value, "object")) {
|
|
1826
|
+
result[key] = Collection.cloneObject(value);
|
|
1827
|
+
} else {
|
|
1828
|
+
result[key] = value;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
return freeze ? Object.freeze(result) : result
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Checks if an object is empty
|
|
1837
|
+
*
|
|
1838
|
+
* @param {object} obj - The object to check
|
|
1839
|
+
* @returns {boolean} Whether the object is empty
|
|
1840
|
+
*/
|
|
1841
|
+
static isObjectEmpty(obj) {
|
|
1842
|
+
const req = "Object";
|
|
1843
|
+
const objType = Data.typeOf(obj);
|
|
1844
|
+
|
|
1845
|
+
Valid.type(obj, req, `Invalid object. Expected '${req}', got '${objType}'`);
|
|
1846
|
+
|
|
1847
|
+
return Object.keys(obj).length === 0
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* Ensures that a nested path of objects exists within the given object.
|
|
1852
|
+
* Creates empty objects along the path if they don't exist.
|
|
1853
|
+
*
|
|
1854
|
+
* @param {object} obj - The object to check/modify
|
|
1855
|
+
* @param {Array<string>} keys - Array of keys representing the path to ensure
|
|
1856
|
+
* @returns {object} Reference to the deepest nested object in the path
|
|
1857
|
+
*/
|
|
1858
|
+
static assureObjectPath(obj, keys) {
|
|
1859
|
+
const req = "Object";
|
|
1860
|
+
const objType = Data.typeOf(obj);
|
|
1861
|
+
const keysReq = "Array";
|
|
1862
|
+
const keysType = Data.typeOf(keys);
|
|
1863
|
+
|
|
1864
|
+
Valid.type(obj, req, `Invalid object. Expected '${req}', got '${objType}'`);
|
|
1865
|
+
Valid.type(keys, keysReq, `Invalid keys array. Expected '${keysReq}', got '${keysType}'`);
|
|
1866
|
+
|
|
1867
|
+
let current = obj; // a moving reference to internal objects within obj
|
|
1868
|
+
const len = keys.length;
|
|
1869
|
+
|
|
1870
|
+
Valid.prototypePollutionProtection(keys);
|
|
1871
|
+
|
|
1872
|
+
for(let i = 0; i < len; i++) {
|
|
1873
|
+
const elem = keys[i];
|
|
1874
|
+
|
|
1875
|
+
if(!current[elem])
|
|
1876
|
+
current[elem] = {};
|
|
1877
|
+
|
|
1878
|
+
current = current[elem];
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// Return the current pointer
|
|
1882
|
+
return current
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Sets a value in a nested object structure using an array of keys; creating
|
|
1887
|
+
* the structure if it does not exist.
|
|
1888
|
+
*
|
|
1889
|
+
* @param {object} obj - The target object to set the value in
|
|
1890
|
+
* @param {Array<string>} keys - Array of keys representing the path to the target property
|
|
1891
|
+
* @param {unknown} value - The value to set at the target location
|
|
1892
|
+
*/
|
|
1893
|
+
static setNestedValue(obj, keys, value) {
|
|
1894
|
+
const req = "Object";
|
|
1895
|
+
const objType = Data.typeOf(obj);
|
|
1896
|
+
const keysReq = "Array";
|
|
1897
|
+
const keysType = Data.typeOf(keys);
|
|
1898
|
+
|
|
1899
|
+
Valid.type(obj, req, `Invalid object. Expected '${req}', got '${objType}'`);
|
|
1900
|
+
Valid.type(keys, keysReq, `Invalid keys array. Expected '${keysReq}', got '${keysType}'`);
|
|
1901
|
+
|
|
1902
|
+
const nested = Collection.assureObjectPath(obj, keys.slice(0, -1));
|
|
1903
|
+
const finalKey = keys[keys.length-1];
|
|
1904
|
+
|
|
1905
|
+
Valid.prototypePollutionProtection([finalKey]);
|
|
1906
|
+
|
|
1907
|
+
nested[finalKey] = value;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* Deeply merges two or more objects. Arrays are replaced, not merged.
|
|
1912
|
+
*
|
|
1913
|
+
* @param {...object} sources - Objects to merge (left to right)
|
|
1914
|
+
* @returns {object} The merged object
|
|
1915
|
+
*/
|
|
1916
|
+
static mergeObject(...sources) {
|
|
1917
|
+
const isObject = obj => typeof obj === "object" && obj !== null && !Array.isArray(obj);
|
|
1918
|
+
|
|
1919
|
+
return sources.reduce((acc, obj) => {
|
|
1920
|
+
if(!isObject(obj))
|
|
1921
|
+
return acc
|
|
1922
|
+
|
|
1923
|
+
Object.keys(obj).forEach(key => {
|
|
1924
|
+
const accVal = acc[key];
|
|
1925
|
+
const objVal = obj[key];
|
|
1926
|
+
|
|
1927
|
+
if(isObject(accVal) && isObject(objVal))
|
|
1928
|
+
acc[key] = Collection.mergeObject(accVal, objVal);
|
|
1929
|
+
else
|
|
1930
|
+
acc[key] = objVal;
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
return acc
|
|
1934
|
+
}, {})
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/**
|
|
1938
|
+
* Freezes an object and all of its properties recursively.
|
|
1939
|
+
*
|
|
1940
|
+
* @param {object} obj The object to freeze.
|
|
1941
|
+
* @returns {object} The frozen object.
|
|
1942
|
+
*/
|
|
1943
|
+
static deepFreezeObject(obj) {
|
|
1944
|
+
if(obj === null || typeof obj !== "object")
|
|
1945
|
+
return obj // Skip null and non-objects
|
|
1946
|
+
|
|
1947
|
+
// Retrieve and freeze properties
|
|
1948
|
+
const propNames = Object.getOwnPropertyNames(obj);
|
|
1949
|
+
|
|
1950
|
+
for(const name of propNames) {
|
|
1951
|
+
const value = obj[name];
|
|
1952
|
+
|
|
1953
|
+
// Recursively freeze nested objects
|
|
1954
|
+
if(typeof value === "object" && value !== null)
|
|
1955
|
+
Collection.deepFreezeObject(value);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Freeze the object itself
|
|
1959
|
+
return Object.freeze(obj)
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
/**
|
|
1963
|
+
* Maps an object using a transformer function
|
|
1964
|
+
*
|
|
1965
|
+
* @param {object} original The original object
|
|
1966
|
+
* @param {function(unknown): unknown} transformer The transformer function
|
|
1967
|
+
* @param {boolean} mutate Whether to mutate the original object
|
|
1968
|
+
* @returns {Promise<object>} The mapped object
|
|
1969
|
+
*/
|
|
1970
|
+
static async mapObject(original, transformer, mutate = false) {
|
|
1971
|
+
Valid.type(original, "object", {allowEmpty: true});
|
|
1972
|
+
Valid.type(transformer, "function");
|
|
1973
|
+
Valid.type(mutate, "boolean");
|
|
1974
|
+
|
|
1975
|
+
const result = mutate ? original : {};
|
|
1976
|
+
|
|
1977
|
+
for(const [key, value] of Object.entries(original))
|
|
1978
|
+
result[key] = Data.isType(value, "object")
|
|
1979
|
+
? await Collection.mapObject(value, transformer, mutate)
|
|
1980
|
+
: (result[key] = await transformer(key, value));
|
|
1981
|
+
|
|
1982
|
+
return result
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
/**
|
|
1986
|
+
* Allocates an object from a source array and a spec array or function.
|
|
1987
|
+
*
|
|
1988
|
+
* @param {Array<unknown>} source The source array
|
|
1989
|
+
* @param {Array<unknown>|function(Array<unknown>): Promise<Array<unknown>>|Array<unknown>} spec The spec array or function
|
|
1990
|
+
* @returns {Promise<object>} The allocated object
|
|
1991
|
+
*/
|
|
1992
|
+
static async allocateObject(source, spec) {
|
|
1993
|
+
const workSource = [],
|
|
1994
|
+
workSpec = [],
|
|
1995
|
+
result = {};
|
|
1996
|
+
|
|
1997
|
+
if(!Data.isType(source, "Array", {allowEmpty: false}))
|
|
1998
|
+
throw Sass.new("Source must be an array.")
|
|
1999
|
+
|
|
2000
|
+
workSource.push(...source);
|
|
2001
|
+
|
|
2002
|
+
if(
|
|
2003
|
+
!Data.isType(spec, "Array", {allowEmpty: false}) &&
|
|
2004
|
+
!Data.isType(spec, "Function")
|
|
2005
|
+
)
|
|
2006
|
+
throw Sass.new("Spec must be an array or a function.")
|
|
2007
|
+
|
|
2008
|
+
if(Data.isType(spec, "Function")) {
|
|
2009
|
+
const specResult = await spec(workSource);
|
|
2010
|
+
|
|
2011
|
+
if(!Data.isType(specResult, "Array"))
|
|
2012
|
+
throw Sass.new("Spec resulting from function must be an array.")
|
|
2013
|
+
|
|
2014
|
+
workSpec.push(...specResult);
|
|
2015
|
+
} else if(Data.isType(spec, "Array", {allowEmpty: false})) {
|
|
2016
|
+
workSpec.push(...spec);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if(workSource.length !== workSpec.length)
|
|
2020
|
+
throw Sass.new("Source and spec must have the same number of elements.")
|
|
2021
|
+
|
|
2022
|
+
// Objects must always be indexed by strings.
|
|
2023
|
+
workSource.map((element, index, arr) => (arr[index] = String(element)));
|
|
2024
|
+
|
|
2025
|
+
// Check that all keys are strings
|
|
2026
|
+
if(!Collection.isArrayUniform(workSource, "String"))
|
|
2027
|
+
throw Sass.new("Indices of an Object must be of type string.")
|
|
2028
|
+
|
|
2029
|
+
workSource.forEach((element, index) => (result[element] = workSpec[index]));
|
|
2030
|
+
|
|
2031
|
+
return result
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Trims falsy values from both ends of an array (in-place).
|
|
2036
|
+
* Optionally preserves specific falsy values.
|
|
2037
|
+
*
|
|
2038
|
+
* @param {Array<unknown>} arr - The array to trim
|
|
2039
|
+
* @param {Array<unknown>} [except] - Values to preserve even if falsy. Defaults to empty array
|
|
2040
|
+
* @returns {Array<unknown>} The trimmed array (same reference, modified in-place)
|
|
2041
|
+
* @throws {Sass} If arr is not an Array or except is not an Array
|
|
2042
|
+
*/
|
|
2043
|
+
static trimArray(arr, except=[]) {
|
|
2044
|
+
Valid.type(arr, "Array");
|
|
2045
|
+
Valid.type(except, "Array");
|
|
2046
|
+
|
|
2047
|
+
Collection.trimArrayLeft(arr, except);
|
|
2048
|
+
Collection.trimArrayRight(arr, except);
|
|
2049
|
+
|
|
2050
|
+
return arr
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* Trims falsy values from the right end of an array (in-place).
|
|
2055
|
+
* Optionally preserves specific falsy values.
|
|
2056
|
+
*
|
|
2057
|
+
* @param {Array<unknown>} arr - The array to trim
|
|
2058
|
+
* @param {Array<unknown>} [except] - Values to preserve even if falsy. Defaults to empty array
|
|
2059
|
+
* @returns {Array<unknown>} The trimmed array (same reference, modified in-place)
|
|
2060
|
+
* @throws {Sass} If arr is not an Array or except is not an Array
|
|
2061
|
+
*/
|
|
2062
|
+
static trimArrayRight(arr, except=[]) {
|
|
2063
|
+
Valid.type(arr, "Array");
|
|
2064
|
+
Valid.type(except, "Array");
|
|
2065
|
+
|
|
2066
|
+
arr.reverse();
|
|
2067
|
+
Collection.trimArrayLeft(arr, except);
|
|
2068
|
+
arr.reverse();
|
|
2069
|
+
|
|
2070
|
+
return arr
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* Trims falsy values from the left end of an array (in-place).
|
|
2075
|
+
* Optionally preserves specific falsy values.
|
|
2076
|
+
*
|
|
2077
|
+
* @param {Array<unknown>} arr - The array to trim
|
|
2078
|
+
* @param {Array<unknown>} [except] - Values to preserve even if falsy. Defaults to empty array
|
|
2079
|
+
* @returns {Array<unknown>} The trimmed array (same reference, modified in-place)
|
|
2080
|
+
* @throws {Sass} If arr is not an Array or except is not an Array
|
|
2081
|
+
*/
|
|
2082
|
+
static trimArrayLeft(arr, except=[]) {
|
|
2083
|
+
Valid.type(arr, "Array");
|
|
2084
|
+
Valid.type(except, "Array");
|
|
2085
|
+
|
|
2086
|
+
while(arr.length > 0) {
|
|
2087
|
+
const value = arr[0];
|
|
2088
|
+
|
|
2089
|
+
if(value || except.includes(value))
|
|
2090
|
+
break
|
|
2091
|
+
|
|
2092
|
+
arr.shift();
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
return arr
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Transposes an array of objects into an object of arrays.
|
|
2100
|
+
* Collects values for each key across all objects into arrays.
|
|
2101
|
+
*
|
|
2102
|
+
* @param {Array<object>} objects - Array of plain objects to transpose
|
|
2103
|
+
* @returns {object} Object with keys from input objects, values as arrays
|
|
2104
|
+
* @throws {Sass} If objects is not an Array or contains non-plain objects
|
|
2105
|
+
*/
|
|
2106
|
+
static transposeObjects(objects) {
|
|
2107
|
+
const req = "Array";
|
|
2108
|
+
const type = Data.typeOf(objects);
|
|
2109
|
+
|
|
2110
|
+
Valid.type(objects, req, `Invalid objects array. Expected '${req}', got '${type}'`);
|
|
2111
|
+
|
|
2112
|
+
return objects.reduce((acc, curr) => {
|
|
2113
|
+
const elemType = Data.typeOf(curr);
|
|
2114
|
+
|
|
2115
|
+
if(!Data.isPlainObject(curr))
|
|
2116
|
+
throw Sass.new(`Invalid array element. Expected plain object, got '${elemType}'`)
|
|
2117
|
+
|
|
2118
|
+
Valid.prototypePollutionProtection(Object.keys(curr));
|
|
2119
|
+
|
|
2120
|
+
Object.entries(curr).forEach(([key, value]) => {
|
|
2121
|
+
if(!acc[key])
|
|
2122
|
+
acc[key] = [];
|
|
2123
|
+
|
|
2124
|
+
acc[key].push(value);
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
return acc
|
|
2128
|
+
}, {})
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/**
|
|
2132
|
+
* Flattens an array (or nested array) of objects and transposes them.
|
|
2133
|
+
* Combines flat() and transposeObjects() operations.
|
|
2134
|
+
*
|
|
2135
|
+
* @param {Array<object>|Array<Array<object>>} input - Array or nested array of objects
|
|
2136
|
+
* @returns {object} Transposed object with arrays of values
|
|
2137
|
+
*/
|
|
2138
|
+
static flattenObjectArray(input) {
|
|
2139
|
+
const flattened = Array.isArray(input) ? input.flat() : input;
|
|
2140
|
+
|
|
2141
|
+
return Collection.transposeObjects(flattened)
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
/**
|
|
2145
|
+
* Computes the structured difference between two objects.
|
|
2146
|
+
* Returns an object with three keys: `added`, `removed`, and `changed`.
|
|
2147
|
+
* Nested objects are recursed into, producing the same structure.
|
|
2148
|
+
* Primitive changes are represented as `{from, to}` pairs.
|
|
2149
|
+
*
|
|
2150
|
+
* @param {object|Array<unknown>} original - The original object or array to compare from
|
|
2151
|
+
* @param {object|Array<unknown>} updated - The updated object or array to compare against
|
|
2152
|
+
* @returns {{added: object, removed: object, changed: object}} Structured diff.
|
|
2153
|
+
* `added` contains keys new in `updated` with their new values.
|
|
2154
|
+
* `removed` contains keys absent from `updated` with their old values.
|
|
2155
|
+
* `changed` contains keys present in both but with different values;
|
|
2156
|
+
* primitives are `{from, to}` pairs, nested objects recurse.
|
|
2157
|
+
* All three keys are always present, empty when there are no differences.
|
|
2158
|
+
*/
|
|
2159
|
+
static diff(original, updated) {
|
|
2160
|
+
const added = {};
|
|
2161
|
+
const removed = {};
|
|
2162
|
+
const changed = {};
|
|
2163
|
+
|
|
2164
|
+
for(const key of Object.keys(updated)) {
|
|
2165
|
+
Valid.prototypePollutionProtection([key]);
|
|
2166
|
+
|
|
2167
|
+
if(!Object.hasOwn(original, key)) {
|
|
2168
|
+
added[key] = updated[key];
|
|
2169
|
+
} else if(!Object.is(original[key], updated[key])) {
|
|
2170
|
+
if(
|
|
2171
|
+
(Data.isPlainObject(original[key]) &&
|
|
2172
|
+
Data.isPlainObject(updated[key])) ||
|
|
2173
|
+
(Array.isArray(original[key]) && Array.isArray(updated[key]))
|
|
2174
|
+
) {
|
|
2175
|
+
const nested = Collection.diff(original[key], updated[key]);
|
|
2176
|
+
const hasChanges =
|
|
2177
|
+
Object.keys(nested.added).length > 0 ||
|
|
2178
|
+
Object.keys(nested.removed).length > 0 ||
|
|
2179
|
+
Object.keys(nested.changed).length > 0;
|
|
2180
|
+
|
|
2181
|
+
if(hasChanges)
|
|
2182
|
+
changed[key] = nested;
|
|
2183
|
+
|
|
2184
|
+
} else {
|
|
2185
|
+
changed[key] = {from: original[key], to: updated[key]};
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
for(const key of Object.keys(original)) {
|
|
2191
|
+
Valid.prototypePollutionProtection([key]);
|
|
2192
|
+
|
|
2193
|
+
if(!Object.hasOwn(updated, key))
|
|
2194
|
+
removed[key] = original[key];
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
return {added, removed, changed}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* Simple lifecycle helper that tracks disposer callbacks.
|
|
2203
|
+
* Register any teardown functions and call dispose() to run them in reverse.
|
|
2204
|
+
*/
|
|
2205
|
+
class Disposer {
|
|
2206
|
+
#disposers = []
|
|
2207
|
+
|
|
2208
|
+
/**
|
|
2209
|
+
* Registers a disposer callback to be executed when disposed.
|
|
2210
|
+
*
|
|
2211
|
+
* Accepts one or more callbacks (or a single array) and returns matching
|
|
2212
|
+
* unregisters. A single disposer returns a single unregister for
|
|
2213
|
+
* convenience.
|
|
2214
|
+
*
|
|
2215
|
+
* @param {...(() => void)|Array<() => void>} disposers - Cleanup callbacks.
|
|
2216
|
+
* @returns {(() => void)|Array<() => void>} Unregister function(s).
|
|
2217
|
+
*/
|
|
2218
|
+
register(...disposers) {
|
|
2219
|
+
const normalized = this.#normalizeDisposers(disposers);
|
|
2220
|
+
const unregisters = normalized.map(
|
|
2221
|
+
disposer => this.#registerDisposer(disposer)
|
|
2222
|
+
);
|
|
2223
|
+
|
|
2224
|
+
return unregisters.length === 1 ? unregisters[0] : unregisters
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
#registerDisposer(disposer) {
|
|
2228
|
+
if(typeof disposer !== "function")
|
|
2229
|
+
return () => {}
|
|
2230
|
+
|
|
2231
|
+
this.#disposers.push(disposer);
|
|
2232
|
+
|
|
2233
|
+
return () => this.#removeDisposer(disposer)
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Runs all registered disposers in reverse order.
|
|
2238
|
+
*
|
|
2239
|
+
* @returns {void}
|
|
2240
|
+
*/
|
|
2241
|
+
dispose() {
|
|
2242
|
+
const errors = [];
|
|
2243
|
+
this.#disposers.toReversed().forEach(disposer => {
|
|
2244
|
+
try {
|
|
2245
|
+
disposer();
|
|
2246
|
+
} catch(error) {
|
|
2247
|
+
errors.push(error);
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2250
|
+
this.#disposers.length = 0;
|
|
2251
|
+
|
|
2252
|
+
if(errors.length > 0)
|
|
2253
|
+
throw new AggregateError(errors, "Errors occurred during disposal.")
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
#normalizeDisposers(disposers) {
|
|
2257
|
+
const normalized = (
|
|
2258
|
+
disposers.length === 1 && Array.isArray(disposers[0])
|
|
2259
|
+
? disposers[0]
|
|
2260
|
+
: disposers
|
|
2261
|
+
);
|
|
2262
|
+
|
|
2263
|
+
Valid.type(normalized, "Function[]");
|
|
2264
|
+
|
|
2265
|
+
return normalized
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/**
|
|
2269
|
+
* Read-only list of registered disposers.
|
|
2270
|
+
*
|
|
2271
|
+
* @returns {Array<() => void>} Snapshot of disposer callbacks.
|
|
2272
|
+
*/
|
|
2273
|
+
get disposers() {
|
|
2274
|
+
return Object.freeze([...this.#disposers])
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
#removeDisposer(disposer) {
|
|
2278
|
+
const index = this.#disposers.indexOf(disposer);
|
|
2279
|
+
|
|
2280
|
+
if(index >= 0)
|
|
2281
|
+
this.#disposers.splice(index, 1);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
var Disposer_default = new Disposer();
|
|
2286
|
+
|
|
2287
|
+
/*! @license DOMPurify 3.3.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.0/LICENSE */
|
|
2288
|
+
|
|
2289
|
+
const {
|
|
2290
|
+
entries,
|
|
2291
|
+
setPrototypeOf,
|
|
2292
|
+
isFrozen,
|
|
2293
|
+
getPrototypeOf,
|
|
2294
|
+
getOwnPropertyDescriptor
|
|
2295
|
+
} = Object;
|
|
2296
|
+
let {
|
|
2297
|
+
freeze,
|
|
2298
|
+
seal,
|
|
2299
|
+
create
|
|
2300
|
+
} = Object; // eslint-disable-line import/no-mutable-exports
|
|
2301
|
+
let {
|
|
2302
|
+
apply,
|
|
2303
|
+
construct
|
|
2304
|
+
} = typeof Reflect !== "undefined" && Reflect;
|
|
2305
|
+
if(!freeze) {
|
|
2306
|
+
freeze = function freeze(x) {
|
|
2307
|
+
return x
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if(!seal) {
|
|
2312
|
+
seal = function seal(x) {
|
|
2313
|
+
return x
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
if(!apply) {
|
|
2318
|
+
apply = function apply(func, thisArg) {
|
|
2319
|
+
for(var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
|
|
2320
|
+
args[_key - 2] = arguments[_key];
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
return func.apply(thisArg, args)
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
if(!construct) {
|
|
2328
|
+
construct = function construct(Func) {
|
|
2329
|
+
for(var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
|
2330
|
+
args[_key2 - 1] = arguments[_key2];
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
return new Func(...args)
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
const arrayForEach = unapply(Array.prototype.forEach);
|
|
2338
|
+
const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
|
|
2339
|
+
const arrayPop = unapply(Array.prototype.pop);
|
|
2340
|
+
const arrayPush = unapply(Array.prototype.push);
|
|
2341
|
+
const arraySplice = unapply(Array.prototype.splice);
|
|
2342
|
+
const stringToLowerCase = unapply(String.prototype.toLowerCase);
|
|
2343
|
+
const stringToString = unapply(String.prototype.toString);
|
|
2344
|
+
const stringMatch = unapply(String.prototype.match);
|
|
2345
|
+
const stringReplace = unapply(String.prototype.replace);
|
|
2346
|
+
const stringIndexOf = unapply(String.prototype.indexOf);
|
|
2347
|
+
const stringTrim = unapply(String.prototype.trim);
|
|
2348
|
+
const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
|
|
2349
|
+
const regExpTest = unapply(RegExp.prototype.test);
|
|
2350
|
+
const typeErrorCreate = unconstruct(TypeError);
|
|
2351
|
+
/**
|
|
2352
|
+
* Creates a new function that calls the given function with a specified thisArg and arguments.
|
|
2353
|
+
*
|
|
2354
|
+
* @param func - The function to be wrapped and called.
|
|
2355
|
+
* @returns A new function that calls the given function with a specified thisArg and arguments.
|
|
2356
|
+
*/
|
|
2357
|
+
function unapply(func) {
|
|
2358
|
+
return function(thisArg) {
|
|
2359
|
+
if(thisArg instanceof RegExp) {
|
|
2360
|
+
thisArg.lastIndex = 0;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
for(var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
|
|
2364
|
+
args[_key3 - 1] = arguments[_key3];
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
return apply(func, thisArg, args)
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Creates a new function that constructs an instance of the given constructor function with the provided arguments.
|
|
2372
|
+
*
|
|
2373
|
+
* @param func - The constructor function to be wrapped and called.
|
|
2374
|
+
* @param Func
|
|
2375
|
+
* @returns A new function that constructs an instance of the given constructor function with the provided arguments.
|
|
2376
|
+
*/
|
|
2377
|
+
function unconstruct(Func) {
|
|
2378
|
+
return function() {
|
|
2379
|
+
for(var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
|
|
2380
|
+
args[_key4] = arguments[_key4];
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
return construct(Func, args)
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Add properties to a lookup table
|
|
2388
|
+
*
|
|
2389
|
+
* @param set - The set to which elements will be added.
|
|
2390
|
+
* @param array - The array containing elements to be added to the set.
|
|
2391
|
+
* @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.
|
|
2392
|
+
* @returns The modified set with added elements.
|
|
2393
|
+
*/
|
|
2394
|
+
function addToSet(set, array) {
|
|
2395
|
+
const transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;
|
|
2396
|
+
if(setPrototypeOf) {
|
|
2397
|
+
// Make 'in' and truthy checks like Boolean(set.constructor)
|
|
2398
|
+
// independent of any properties defined on Object.prototype.
|
|
2399
|
+
// Prevent prototype setters from intercepting set as a this value.
|
|
2400
|
+
setPrototypeOf(set, null);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
let l = array.length;
|
|
2404
|
+
while(l--) {
|
|
2405
|
+
let element = array[l];
|
|
2406
|
+
if(typeof element === "string") {
|
|
2407
|
+
const lcElement = transformCaseFunc(element);
|
|
2408
|
+
if(lcElement !== element) {
|
|
2409
|
+
// Config presets (e.g. tags.js, attrs.js) are immutable.
|
|
2410
|
+
if(!isFrozen(array)) {
|
|
2411
|
+
array[l] = lcElement;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
element = lcElement;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
set[element] = true;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
return set
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Clean up an array to harden against CSPP
|
|
2425
|
+
*
|
|
2426
|
+
* @param array - The array to be cleaned.
|
|
2427
|
+
* @returns The cleaned version of the array
|
|
2428
|
+
*/
|
|
2429
|
+
function cleanArray(array) {
|
|
2430
|
+
for(let index = 0; index < array.length; index++) {
|
|
2431
|
+
const isPropertyExist = objectHasOwnProperty(array, index);
|
|
2432
|
+
if(!isPropertyExist) {
|
|
2433
|
+
array[index] = null;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
return array
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Shallow clone an object
|
|
2441
|
+
*
|
|
2442
|
+
* @param object - The object to be cloned.
|
|
2443
|
+
* @returns A new object that copies the original.
|
|
2444
|
+
*/
|
|
2445
|
+
function clone(object) {
|
|
2446
|
+
const newObject = create(null);
|
|
2447
|
+
for(const [property, value] of entries(object)) {
|
|
2448
|
+
const isPropertyExist = objectHasOwnProperty(object, property);
|
|
2449
|
+
if(isPropertyExist) {
|
|
2450
|
+
if(Array.isArray(value)) {
|
|
2451
|
+
newObject[property] = cleanArray(value);
|
|
2452
|
+
} else if(value && typeof value === "object" && value.constructor === Object) {
|
|
2453
|
+
newObject[property] = clone(value);
|
|
2454
|
+
} else {
|
|
2455
|
+
newObject[property] = value;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
return newObject
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* This method automatically checks if the prop is function or getter and behaves accordingly.
|
|
2464
|
+
*
|
|
2465
|
+
* @param object - The object to look up the getter function in its prototype chain.
|
|
2466
|
+
* @param prop - The property name for which to find the getter function.
|
|
2467
|
+
* @returns The getter function found in the prototype chain or a fallback function.
|
|
2468
|
+
*/
|
|
2469
|
+
function lookupGetter(object, prop) {
|
|
2470
|
+
while(object !== null) {
|
|
2471
|
+
const desc = getOwnPropertyDescriptor(object, prop);
|
|
2472
|
+
if(desc) {
|
|
2473
|
+
if(desc.get) {
|
|
2474
|
+
return unapply(desc.get)
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
if(typeof desc.value === "function") {
|
|
2478
|
+
return unapply(desc.value)
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
object = getPrototypeOf(object);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function fallbackValue() {
|
|
2486
|
+
return null
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
return fallbackValue
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
const html$1 = freeze(["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "search", "section", "select", "shadow", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]);
|
|
2493
|
+
const svg$1 = freeze(["svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "enterkeyhint", "exportparts", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "inputmode", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "part", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "view", "vkern"]);
|
|
2494
|
+
const svgFilters = freeze(["feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"]);
|
|
2495
|
+
// List of SVG elements that are disallowed by default.
|
|
2496
|
+
// We still need to know them so that we can do namespace
|
|
2497
|
+
// checks properly in case one wants to add them to
|
|
2498
|
+
// allow-list.
|
|
2499
|
+
const svgDisallowed = freeze(["animate", "color-profile", "cursor", "discard", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch", "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set", "solidcolor", "unknown", "use"]);
|
|
2500
|
+
const mathMl$1 = freeze(["math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover", "mprescripts"]);
|
|
2501
|
+
// Similarly to SVG, we want to know all MathML elements,
|
|
2502
|
+
// even those that we disallow by default.
|
|
2503
|
+
const mathMlDisallowed = freeze(["maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup", "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"]);
|
|
2504
|
+
const text = freeze(["#text"]);
|
|
2505
|
+
|
|
2506
|
+
const html = freeze(["accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture", "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "controls", "controlslist", "coords", "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture", "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "exportparts", "face", "for", "headers", "height", "hidden", "high", "href", "hreflang", "id", "inert", "inputmode", "integrity", "ismap", "kind", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "minlength", "multiple", "muted", "name", "nonce", "noshade", "novalidate", "nowrap", "open", "optimum", "part", "pattern", "placeholder", "playsinline", "popover", "popovertarget", "popovertargetaction", "poster", "preload", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "slot", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "wrap", "xmlns", "slot"]);
|
|
2507
|
+
const svg = freeze(["accent-height", "accumulate", "additive", "alignment-baseline", "amplitude", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "exponent", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "mask-type", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "slope", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "tablevalues", "targetx", "targety", "transform", "transform-origin", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"]);
|
|
2508
|
+
const mathMl = freeze(["accent", "accentunder", "align", "bevelled", "close", "columnsalign", "columnlines", "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame", "height", "href", "id", "largeop", "length", "linethickness", "lspace", "lquote", "mathbackground", "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign", "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel", "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy", "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"]);
|
|
2509
|
+
const xml = freeze(["xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"]);
|
|
2510
|
+
|
|
2511
|
+
// eslint-disable-next-line unicorn/better-regex
|
|
2512
|
+
const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
|
|
2513
|
+
const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
|
|
2514
|
+
const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
|
|
2515
|
+
const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
|
|
2516
|
+
const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
|
|
2517
|
+
const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
|
|
2518
|
+
);
|
|
2519
|
+
const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
|
|
2520
|
+
const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
|
|
2521
|
+
);
|
|
2522
|
+
const DOCTYPE_NAME = seal(/^html$/i);
|
|
2523
|
+
const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
|
|
2524
|
+
|
|
2525
|
+
var EXPRESSIONS = /*#__PURE__*/Object.freeze({
|
|
2526
|
+
__proto__: null,
|
|
2527
|
+
ARIA_ATTR: ARIA_ATTR,
|
|
2528
|
+
ATTR_WHITESPACE: ATTR_WHITESPACE,
|
|
2529
|
+
CUSTOM_ELEMENT: CUSTOM_ELEMENT,
|
|
2530
|
+
DATA_ATTR: DATA_ATTR,
|
|
2531
|
+
DOCTYPE_NAME: DOCTYPE_NAME,
|
|
2532
|
+
ERB_EXPR: ERB_EXPR,
|
|
2533
|
+
IS_ALLOWED_URI: IS_ALLOWED_URI,
|
|
2534
|
+
IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
|
|
2535
|
+
MUSTACHE_EXPR: MUSTACHE_EXPR,
|
|
2536
|
+
TMPLIT_EXPR: TMPLIT_EXPR
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
/* eslint-disable @typescript-eslint/indent */
|
|
2540
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
|
|
2541
|
+
const NODE_TYPE = {
|
|
2542
|
+
element: 1,
|
|
2543
|
+
text: 3,
|
|
2544
|
+
// Deprecated
|
|
2545
|
+
progressingInstruction: 7,
|
|
2546
|
+
comment: 8,
|
|
2547
|
+
document: 9};
|
|
2548
|
+
const getGlobal = function getGlobal() {
|
|
2549
|
+
return typeof window === "undefined" ? null : window
|
|
2550
|
+
};
|
|
2551
|
+
/**
|
|
2552
|
+
* Creates a no-op policy for internal use only.
|
|
2553
|
+
* Don't export this function outside this module!
|
|
2554
|
+
*
|
|
2555
|
+
* @param trustedTypes The policy factory.
|
|
2556
|
+
* @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).
|
|
2557
|
+
* @returns The policy created (or null, if Trusted Types
|
|
2558
|
+
* are not supported or creating the policy failed).
|
|
2559
|
+
*/
|
|
2560
|
+
const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {
|
|
2561
|
+
if(typeof trustedTypes !== "object" || typeof trustedTypes.createPolicy !== "function") {
|
|
2562
|
+
return null
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// Allow the callers to control the unique policy name
|
|
2566
|
+
// by adding a data-tt-policy-suffix to the script element with the DOMPurify.
|
|
2567
|
+
// Policy creation with duplicate names throws in Trusted Types.
|
|
2568
|
+
let suffix = null;
|
|
2569
|
+
const ATTR_NAME = "data-tt-policy-suffix";
|
|
2570
|
+
if(purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
|
|
2571
|
+
suffix = purifyHostElement.getAttribute(ATTR_NAME);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
const policyName = "dompurify" + (suffix ? "#" + suffix : "");
|
|
2575
|
+
try {
|
|
2576
|
+
return trustedTypes.createPolicy(policyName, {
|
|
2577
|
+
createHTML(html) {
|
|
2578
|
+
return html
|
|
2579
|
+
},
|
|
2580
|
+
createScriptURL(scriptUrl) {
|
|
2581
|
+
return scriptUrl
|
|
2582
|
+
}
|
|
2583
|
+
})
|
|
2584
|
+
} catch(_) {
|
|
2585
|
+
// Policy creation failed (most likely another DOMPurify script has
|
|
2586
|
+
// already run). Skip creating the policy, as this will only cause errors
|
|
2587
|
+
// if TT are enforced.
|
|
2588
|
+
console.warn("TrustedTypes policy " + policyName + " could not be created.");
|
|
2589
|
+
|
|
2590
|
+
return null
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
const _createHooksMap = function _createHooksMap() {
|
|
2594
|
+
return {
|
|
2595
|
+
afterSanitizeAttributes: [],
|
|
2596
|
+
afterSanitizeElements: [],
|
|
2597
|
+
afterSanitizeShadowDOM: [],
|
|
2598
|
+
beforeSanitizeAttributes: [],
|
|
2599
|
+
beforeSanitizeElements: [],
|
|
2600
|
+
beforeSanitizeShadowDOM: [],
|
|
2601
|
+
uponSanitizeAttribute: [],
|
|
2602
|
+
uponSanitizeElement: [],
|
|
2603
|
+
uponSanitizeShadowNode: []
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
function createDOMPurify() {
|
|
2607
|
+
const window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
|
|
2608
|
+
const DOMPurify = root => createDOMPurify(root);
|
|
2609
|
+
DOMPurify.version = "3.3.0";
|
|
2610
|
+
DOMPurify.removed = [];
|
|
2611
|
+
if(!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
|
|
2612
|
+
// Not running in a browser, provide a factory function
|
|
2613
|
+
// so that you can pass your own Window
|
|
2614
|
+
DOMPurify.isSupported = false;
|
|
2615
|
+
|
|
2616
|
+
return DOMPurify
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
let {
|
|
2620
|
+
document
|
|
2621
|
+
} = window;
|
|
2622
|
+
const originalDocument = document;
|
|
2623
|
+
const currentScript = originalDocument.currentScript;
|
|
2624
|
+
const {
|
|
2625
|
+
DocumentFragment,
|
|
2626
|
+
HTMLTemplateElement,
|
|
2627
|
+
Node,
|
|
2628
|
+
Element,
|
|
2629
|
+
NodeFilter,
|
|
2630
|
+
NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
|
|
2631
|
+
HTMLFormElement,
|
|
2632
|
+
DOMParser,
|
|
2633
|
+
trustedTypes
|
|
2634
|
+
} = window;
|
|
2635
|
+
const ElementPrototype = Element.prototype;
|
|
2636
|
+
const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
|
|
2637
|
+
const remove = lookupGetter(ElementPrototype, "remove");
|
|
2638
|
+
const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
|
|
2639
|
+
const getChildNodes = lookupGetter(ElementPrototype, "childNodes");
|
|
2640
|
+
const getParentNode = lookupGetter(ElementPrototype, "parentNode");
|
|
2641
|
+
// As per issue #47, the web-components registry is inherited by a
|
|
2642
|
+
// new document created via createHTMLDocument. As per the spec
|
|
2643
|
+
// (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
|
|
2644
|
+
// a new empty registry is used when creating a template contents owner
|
|
2645
|
+
// document, so we use that as our parent document to ensure nothing
|
|
2646
|
+
// is inherited.
|
|
2647
|
+
if(typeof HTMLTemplateElement === "function") {
|
|
2648
|
+
const template = document.createElement("template");
|
|
2649
|
+
if(template.content && template.content.ownerDocument) {
|
|
2650
|
+
document = template.content.ownerDocument;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
let trustedTypesPolicy;
|
|
2655
|
+
let emptyHTML = "";
|
|
2656
|
+
const {
|
|
2657
|
+
implementation,
|
|
2658
|
+
createNodeIterator,
|
|
2659
|
+
createDocumentFragment,
|
|
2660
|
+
getElementsByTagName
|
|
2661
|
+
} = document;
|
|
2662
|
+
const {
|
|
2663
|
+
importNode
|
|
2664
|
+
} = originalDocument;
|
|
2665
|
+
let hooks = _createHooksMap();
|
|
2666
|
+
/**
|
|
2667
|
+
* Expose whether this browser supports running the full DOMPurify.
|
|
2668
|
+
*/
|
|
2669
|
+
DOMPurify.isSupported = typeof entries === "function" && typeof getParentNode === "function" && implementation && implementation.createHTMLDocument !== undefined;
|
|
2670
|
+
const {
|
|
2671
|
+
MUSTACHE_EXPR,
|
|
2672
|
+
ERB_EXPR,
|
|
2673
|
+
TMPLIT_EXPR,
|
|
2674
|
+
DATA_ATTR,
|
|
2675
|
+
ARIA_ATTR,
|
|
2676
|
+
IS_SCRIPT_OR_DATA,
|
|
2677
|
+
ATTR_WHITESPACE,
|
|
2678
|
+
CUSTOM_ELEMENT
|
|
2679
|
+
} = EXPRESSIONS;
|
|
2680
|
+
let {
|
|
2681
|
+
IS_ALLOWED_URI: IS_ALLOWED_URI$1
|
|
2682
|
+
} = EXPRESSIONS;
|
|
2683
|
+
/**
|
|
2684
|
+
* We consider the elements and attributes below to be safe. Ideally
|
|
2685
|
+
* don't add any new ones but feel free to remove unwanted ones.
|
|
2686
|
+
*/
|
|
2687
|
+
/* allowed element names */
|
|
2688
|
+
let ALLOWED_TAGS = null;
|
|
2689
|
+
const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);
|
|
2690
|
+
/* Allowed attribute names */
|
|
2691
|
+
let ALLOWED_ATTR = null;
|
|
2692
|
+
const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);
|
|
2693
|
+
/*
|
|
2694
|
+
* Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.
|
|
2695
|
+
* @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)
|
|
2696
|
+
* @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)
|
|
2697
|
+
* @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.
|
|
2698
|
+
*/
|
|
2699
|
+
let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {
|
|
2700
|
+
tagNameCheck: {
|
|
2701
|
+
writable: true,
|
|
2702
|
+
configurable: false,
|
|
2703
|
+
enumerable: true,
|
|
2704
|
+
value: null
|
|
2705
|
+
},
|
|
2706
|
+
attributeNameCheck: {
|
|
2707
|
+
writable: true,
|
|
2708
|
+
configurable: false,
|
|
2709
|
+
enumerable: true,
|
|
2710
|
+
value: null
|
|
2711
|
+
},
|
|
2712
|
+
allowCustomizedBuiltInElements: {
|
|
2713
|
+
writable: true,
|
|
2714
|
+
configurable: false,
|
|
2715
|
+
enumerable: true,
|
|
2716
|
+
value: false
|
|
2717
|
+
}
|
|
2718
|
+
}));
|
|
2719
|
+
/* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
|
|
2720
|
+
let FORBID_TAGS = null;
|
|
2721
|
+
/* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
|
|
2722
|
+
let FORBID_ATTR = null;
|
|
2723
|
+
/* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */
|
|
2724
|
+
const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, {
|
|
2725
|
+
tagCheck: {
|
|
2726
|
+
writable: true,
|
|
2727
|
+
configurable: false,
|
|
2728
|
+
enumerable: true,
|
|
2729
|
+
value: null
|
|
2730
|
+
},
|
|
2731
|
+
attributeCheck: {
|
|
2732
|
+
writable: true,
|
|
2733
|
+
configurable: false,
|
|
2734
|
+
enumerable: true,
|
|
2735
|
+
value: null
|
|
2736
|
+
}
|
|
2737
|
+
}));
|
|
2738
|
+
/* Decide if ARIA attributes are okay */
|
|
2739
|
+
let ALLOW_ARIA_ATTR = true;
|
|
2740
|
+
/* Decide if custom data attributes are okay */
|
|
2741
|
+
let ALLOW_DATA_ATTR = true;
|
|
2742
|
+
/* Decide if unknown protocols are okay */
|
|
2743
|
+
let ALLOW_UNKNOWN_PROTOCOLS = false;
|
|
2744
|
+
/* Decide if self-closing tags in attributes are allowed.
|
|
2745
|
+
* Usually removed due to a mXSS issue in jQuery 3.0 */
|
|
2746
|
+
let ALLOW_SELF_CLOSE_IN_ATTR = true;
|
|
2747
|
+
/* Output should be safe for common template engines.
|
|
2748
|
+
* This means, DOMPurify removes data attributes, mustaches and ERB
|
|
2749
|
+
*/
|
|
2750
|
+
let SAFE_FOR_TEMPLATES = false;
|
|
2751
|
+
/* Output should be safe even for XML used within HTML and alike.
|
|
2752
|
+
* This means, DOMPurify removes comments when containing risky content.
|
|
2753
|
+
*/
|
|
2754
|
+
let SAFE_FOR_XML = true;
|
|
2755
|
+
/* Decide if document with <html>... should be returned */
|
|
2756
|
+
let WHOLE_DOCUMENT = false;
|
|
2757
|
+
/* Track whether config is already set on this instance of DOMPurify. */
|
|
2758
|
+
let SET_CONFIG = false;
|
|
2759
|
+
/* Decide if all elements (e.g. style, script) must be children of
|
|
2760
|
+
* document.body. By default, browsers might move them to document.head */
|
|
2761
|
+
let FORCE_BODY = false;
|
|
2762
|
+
/* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
|
|
2763
|
+
* string (or a TrustedHTML object if Trusted Types are supported).
|
|
2764
|
+
* If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
|
|
2765
|
+
*/
|
|
2766
|
+
let RETURN_DOM = false;
|
|
2767
|
+
/* Decide if a DOM `DocumentFragment` should be returned, instead of a html
|
|
2768
|
+
* string (or a TrustedHTML object if Trusted Types are supported) */
|
|
2769
|
+
let RETURN_DOM_FRAGMENT = false;
|
|
2770
|
+
/* Try to return a Trusted Type object instead of a string, return a string in
|
|
2771
|
+
* case Trusted Types are not supported */
|
|
2772
|
+
let RETURN_TRUSTED_TYPE = false;
|
|
2773
|
+
/* Output should be free from DOM clobbering attacks?
|
|
2774
|
+
* This sanitizes markups named with colliding, clobberable built-in DOM APIs.
|
|
2775
|
+
*/
|
|
2776
|
+
let SANITIZE_DOM = true;
|
|
2777
|
+
/* Achieve full DOM Clobbering protection by isolating the namespace of named
|
|
2778
|
+
* properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.
|
|
2779
|
+
*
|
|
2780
|
+
* HTML/DOM spec rules that enable DOM Clobbering:
|
|
2781
|
+
* - Named Access on Window (§7.3.3)
|
|
2782
|
+
* - DOM Tree Accessors (§3.1.5)
|
|
2783
|
+
* - Form Element Parent-Child Relations (§4.10.3)
|
|
2784
|
+
* - Iframe srcdoc / Nested WindowProxies (§4.8.5)
|
|
2785
|
+
* - HTMLCollection (§4.2.10.2)
|
|
2786
|
+
*
|
|
2787
|
+
* Namespace isolation is implemented by prefixing `id` and `name` attributes
|
|
2788
|
+
* with a constant string, i.e., `user-content-`
|
|
2789
|
+
*/
|
|
2790
|
+
let SANITIZE_NAMED_PROPS = false;
|
|
2791
|
+
const SANITIZE_NAMED_PROPS_PREFIX = "user-content-";
|
|
2792
|
+
/* Keep element content when removing element? */
|
|
2793
|
+
let KEEP_CONTENT = true;
|
|
2794
|
+
/* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
|
|
2795
|
+
* of importing it into a new Document and returning a sanitized copy */
|
|
2796
|
+
let IN_PLACE = false;
|
|
2797
|
+
/* Allow usage of profiles like html, svg and mathMl */
|
|
2798
|
+
let USE_PROFILES = {};
|
|
2799
|
+
/* Tags to ignore content of when KEEP_CONTENT is true */
|
|
2800
|
+
let FORBID_CONTENTS = null;
|
|
2801
|
+
const DEFAULT_FORBID_CONTENTS = addToSet({}, ["annotation-xml", "audio", "colgroup", "desc", "foreignobject", "head", "iframe", "math", "mi", "mn", "mo", "ms", "mtext", "noembed", "noframes", "noscript", "plaintext", "script", "style", "svg", "template", "thead", "title", "video", "xmp"]);
|
|
2802
|
+
/* Tags that are safe for data: URIs */
|
|
2803
|
+
let DATA_URI_TAGS = null;
|
|
2804
|
+
const DEFAULT_DATA_URI_TAGS = addToSet({}, ["audio", "video", "img", "source", "image", "track"]);
|
|
2805
|
+
/* Attributes safe for values like "javascript:" */
|
|
2806
|
+
let URI_SAFE_ATTRIBUTES = null;
|
|
2807
|
+
const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ["alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "role", "summary", "title", "value", "style", "xmlns"]);
|
|
2808
|
+
const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
|
|
2809
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
2810
|
+
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
2811
|
+
/* Document namespace */
|
|
2812
|
+
let NAMESPACE = HTML_NAMESPACE;
|
|
2813
|
+
let IS_EMPTY_INPUT = false;
|
|
2814
|
+
/* Allowed XHTML+XML namespaces */
|
|
2815
|
+
let ALLOWED_NAMESPACES = null;
|
|
2816
|
+
const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
|
|
2817
|
+
let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ["mi", "mo", "mn", "ms", "mtext"]);
|
|
2818
|
+
let HTML_INTEGRATION_POINTS = addToSet({}, ["annotation-xml"]);
|
|
2819
|
+
// Certain elements are allowed in both SVG and HTML
|
|
2820
|
+
// namespace. We need to specify them explicitly
|
|
2821
|
+
// so that they don't get erroneously deleted from
|
|
2822
|
+
// HTML namespace.
|
|
2823
|
+
const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ["title", "style", "font", "a", "script"]);
|
|
2824
|
+
/* Parsing of strict XHTML documents */
|
|
2825
|
+
let PARSER_MEDIA_TYPE = null;
|
|
2826
|
+
const SUPPORTED_PARSER_MEDIA_TYPES = ["application/xhtml+xml", "text/html"];
|
|
2827
|
+
const DEFAULT_PARSER_MEDIA_TYPE = "text/html";
|
|
2828
|
+
let transformCaseFunc = null;
|
|
2829
|
+
/* Keep a reference to config to pass to hooks */
|
|
2830
|
+
let CONFIG = null;
|
|
2831
|
+
/* Ideally, do not touch anything below this line */
|
|
2832
|
+
/* ______________________________________________ */
|
|
2833
|
+
const formElement = document.createElement("form");
|
|
2834
|
+
const isRegexOrFunction = function isRegexOrFunction(testValue) {
|
|
2835
|
+
return testValue instanceof RegExp || testValue instanceof Function
|
|
2836
|
+
};
|
|
2837
|
+
/**
|
|
2838
|
+
* _parseConfig
|
|
2839
|
+
*
|
|
2840
|
+
* @param cfg optional config literal
|
|
2841
|
+
*/
|
|
2842
|
+
|
|
2843
|
+
const _parseConfig = function _parseConfig() {
|
|
2844
|
+
let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
2845
|
+
if(CONFIG && CONFIG === cfg) {
|
|
2846
|
+
return
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
/* Shield configuration object from tampering */
|
|
2850
|
+
if(!cfg || typeof cfg !== "object") {
|
|
2851
|
+
cfg = {};
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
/* Shield configuration object from prototype pollution */
|
|
2855
|
+
cfg = clone(cfg);
|
|
2856
|
+
PARSER_MEDIA_TYPE =
|
|
2857
|
+
// eslint-disable-next-line unicorn/prefer-includes
|
|
2858
|
+
SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;
|
|
2859
|
+
// HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
|
|
2860
|
+
transformCaseFunc = PARSER_MEDIA_TYPE === "application/xhtml+xml" ? stringToString : stringToLowerCase;
|
|
2861
|
+
/* Set configuration parameters */
|
|
2862
|
+
ALLOWED_TAGS = objectHasOwnProperty(cfg, "ALLOWED_TAGS") ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
|
|
2863
|
+
ALLOWED_ATTR = objectHasOwnProperty(cfg, "ALLOWED_ATTR") ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
|
|
2864
|
+
ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, "ALLOWED_NAMESPACES") ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
|
|
2865
|
+
URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
|
|
2866
|
+
DATA_URI_TAGS = objectHasOwnProperty(cfg, "ADD_DATA_URI_TAGS") ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
|
|
2867
|
+
FORBID_CONTENTS = objectHasOwnProperty(cfg, "FORBID_CONTENTS") ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
|
|
2868
|
+
FORBID_TAGS = objectHasOwnProperty(cfg, "FORBID_TAGS") ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
|
|
2869
|
+
FORBID_ATTR = objectHasOwnProperty(cfg, "FORBID_ATTR") ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
|
|
2870
|
+
USE_PROFILES = objectHasOwnProperty(cfg, "USE_PROFILES") ? cfg.USE_PROFILES : false;
|
|
2871
|
+
ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
|
|
2872
|
+
ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
|
|
2873
|
+
ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
|
|
2874
|
+
ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true
|
|
2875
|
+
SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
|
|
2876
|
+
SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true
|
|
2877
|
+
WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
|
|
2878
|
+
RETURN_DOM = cfg.RETURN_DOM || false; // Default false
|
|
2879
|
+
RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
|
|
2880
|
+
RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
|
|
2881
|
+
FORCE_BODY = cfg.FORCE_BODY || false; // Default false
|
|
2882
|
+
SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
|
|
2883
|
+
SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
|
|
2884
|
+
KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
|
|
2885
|
+
IN_PLACE = cfg.IN_PLACE || false; // Default false
|
|
2886
|
+
IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
|
|
2887
|
+
NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
|
|
2888
|
+
MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
|
|
2889
|
+
HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
|
|
2890
|
+
CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};
|
|
2891
|
+
if(cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
|
|
2892
|
+
CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
if(cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
|
|
2896
|
+
CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
if(cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === "boolean") {
|
|
2900
|
+
CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
if(SAFE_FOR_TEMPLATES) {
|
|
2904
|
+
ALLOW_DATA_ATTR = false;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
if(RETURN_DOM_FRAGMENT) {
|
|
2908
|
+
RETURN_DOM = true;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
/* Parse profile info */
|
|
2912
|
+
if(USE_PROFILES) {
|
|
2913
|
+
ALLOWED_TAGS = addToSet({}, text);
|
|
2914
|
+
ALLOWED_ATTR = [];
|
|
2915
|
+
if(USE_PROFILES.html === true) {
|
|
2916
|
+
addToSet(ALLOWED_TAGS, html$1);
|
|
2917
|
+
addToSet(ALLOWED_ATTR, html);
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
if(USE_PROFILES.svg === true) {
|
|
2921
|
+
addToSet(ALLOWED_TAGS, svg$1);
|
|
2922
|
+
addToSet(ALLOWED_ATTR, svg);
|
|
2923
|
+
addToSet(ALLOWED_ATTR, xml);
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
if(USE_PROFILES.svgFilters === true) {
|
|
2927
|
+
addToSet(ALLOWED_TAGS, svgFilters);
|
|
2928
|
+
addToSet(ALLOWED_ATTR, svg);
|
|
2929
|
+
addToSet(ALLOWED_ATTR, xml);
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
if(USE_PROFILES.mathMl === true) {
|
|
2933
|
+
addToSet(ALLOWED_TAGS, mathMl$1);
|
|
2934
|
+
addToSet(ALLOWED_ATTR, mathMl);
|
|
2935
|
+
addToSet(ALLOWED_ATTR, xml);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
/* Merge configuration parameters */
|
|
2940
|
+
if(cfg.ADD_TAGS) {
|
|
2941
|
+
if(typeof cfg.ADD_TAGS === "function") {
|
|
2942
|
+
EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
|
|
2943
|
+
} else {
|
|
2944
|
+
if(ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
|
|
2945
|
+
ALLOWED_TAGS = clone(ALLOWED_TAGS);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
if(cfg.ADD_ATTR) {
|
|
2953
|
+
if(typeof cfg.ADD_ATTR === "function") {
|
|
2954
|
+
EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
|
|
2955
|
+
} else {
|
|
2956
|
+
if(ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
|
|
2957
|
+
ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
if(cfg.ADD_URI_SAFE_ATTR) {
|
|
2965
|
+
addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
if(cfg.FORBID_CONTENTS) {
|
|
2969
|
+
if(FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
|
|
2970
|
+
FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
/* Add #text in case KEEP_CONTENT is set to true */
|
|
2977
|
+
if(KEEP_CONTENT) {
|
|
2978
|
+
ALLOWED_TAGS["#text"] = true;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
/* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
|
|
2982
|
+
if(WHOLE_DOCUMENT) {
|
|
2983
|
+
addToSet(ALLOWED_TAGS, ["html", "head", "body"]);
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
/* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
|
|
2987
|
+
if(ALLOWED_TAGS.table) {
|
|
2988
|
+
addToSet(ALLOWED_TAGS, ["tbody"]);
|
|
2989
|
+
delete FORBID_TAGS.tbody;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
if(cfg.TRUSTED_TYPES_POLICY) {
|
|
2993
|
+
if(typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== "function") {
|
|
2994
|
+
throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.')
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
if(typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== "function") {
|
|
2998
|
+
throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.')
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// Overwrite existing TrustedTypes policy.
|
|
3002
|
+
trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
|
|
3003
|
+
// Sign local variables required by `sanitize`.
|
|
3004
|
+
emptyHTML = trustedTypesPolicy.createHTML("");
|
|
3005
|
+
} else {
|
|
3006
|
+
// Uninitialized policy, attempt to initialize the internal dompurify policy.
|
|
3007
|
+
if(trustedTypesPolicy === undefined) {
|
|
3008
|
+
trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// If creating the internal policy succeeded sign internal variables.
|
|
3012
|
+
if(trustedTypesPolicy !== null && typeof emptyHTML === "string") {
|
|
3013
|
+
emptyHTML = trustedTypesPolicy.createHTML("");
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
// Prevent further manipulation of configuration.
|
|
3018
|
+
// Not available in IE8, Safari 5, etc.
|
|
3019
|
+
if(freeze) {
|
|
3020
|
+
freeze(cfg);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
CONFIG = cfg;
|
|
3024
|
+
};
|
|
3025
|
+
/* Keep track of all possible SVG and MathML tags
|
|
3026
|
+
* so that we can perform the namespace checks
|
|
3027
|
+
* correctly. */
|
|
3028
|
+
const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
|
|
3029
|
+
const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
|
|
3030
|
+
/**
|
|
3031
|
+
* @param element a DOM element whose namespace is being checked
|
|
3032
|
+
* @returns Return false if the element has a
|
|
3033
|
+
* namespace that a spec-compliant parser would never
|
|
3034
|
+
* return. Return true otherwise.
|
|
3035
|
+
*/
|
|
3036
|
+
const _checkValidNamespace = function _checkValidNamespace(element) {
|
|
3037
|
+
let parent = getParentNode(element);
|
|
3038
|
+
// In JSDOM, if we're inside shadow DOM, then parentNode
|
|
3039
|
+
// can be null. We just simulate parent in this case.
|
|
3040
|
+
if(!parent || !parent.tagName) {
|
|
3041
|
+
parent = {
|
|
3042
|
+
namespaceURI: NAMESPACE,
|
|
3043
|
+
tagName: "template"
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
const tagName = stringToLowerCase(element.tagName);
|
|
3048
|
+
const parentTagName = stringToLowerCase(parent.tagName);
|
|
3049
|
+
if(!ALLOWED_NAMESPACES[element.namespaceURI]) {
|
|
3050
|
+
return false
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
if(element.namespaceURI === SVG_NAMESPACE) {
|
|
3054
|
+
// The only way to switch from HTML namespace to SVG
|
|
3055
|
+
// is via <svg>. If it happens via any other tag, then
|
|
3056
|
+
// it should be killed.
|
|
3057
|
+
if(parent.namespaceURI === HTML_NAMESPACE) {
|
|
3058
|
+
return tagName === "svg"
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// The only way to switch from MathML to SVG is via`
|
|
3062
|
+
// svg if parent is either <annotation-xml> or MathML
|
|
3063
|
+
// text integration points.
|
|
3064
|
+
if(parent.namespaceURI === MATHML_NAMESPACE) {
|
|
3065
|
+
return tagName === "svg" && (parentTagName === "annotation-xml" || MATHML_TEXT_INTEGRATION_POINTS[parentTagName])
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// We only allow elements that are defined in SVG
|
|
3069
|
+
// spec. All others are disallowed in SVG namespace.
|
|
3070
|
+
return Boolean(ALL_SVG_TAGS[tagName])
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
if(element.namespaceURI === MATHML_NAMESPACE) {
|
|
3074
|
+
// The only way to switch from HTML namespace to MathML
|
|
3075
|
+
// is via <math>. If it happens via any other tag, then
|
|
3076
|
+
// it should be killed.
|
|
3077
|
+
if(parent.namespaceURI === HTML_NAMESPACE) {
|
|
3078
|
+
return tagName === "math"
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// The only way to switch from SVG to MathML is via
|
|
3082
|
+
// <math> and HTML integration points
|
|
3083
|
+
if(parent.namespaceURI === SVG_NAMESPACE) {
|
|
3084
|
+
return tagName === "math" && HTML_INTEGRATION_POINTS[parentTagName]
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
// We only allow elements that are defined in MathML
|
|
3088
|
+
// spec. All others are disallowed in MathML namespace.
|
|
3089
|
+
return Boolean(ALL_MATHML_TAGS[tagName])
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
if(element.namespaceURI === HTML_NAMESPACE) {
|
|
3093
|
+
// The only way to switch from SVG to HTML is via
|
|
3094
|
+
// HTML integration points, and from MathML to HTML
|
|
3095
|
+
// is via MathML text integration points
|
|
3096
|
+
if(parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
|
|
3097
|
+
return false
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
if(parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
|
|
3101
|
+
return false
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
// We disallow tags that are specific for MathML
|
|
3105
|
+
// or SVG and should never appear in HTML namespace
|
|
3106
|
+
return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// For XHTML and XML documents that support custom namespaces
|
|
3110
|
+
if(PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[element.namespaceURI]) {
|
|
3111
|
+
return true
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// The code should never reach this place (this means
|
|
3115
|
+
// that the element somehow got namespace that is not
|
|
3116
|
+
// HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).
|
|
3117
|
+
// Return false just in case.
|
|
3118
|
+
return false
|
|
3119
|
+
};
|
|
3120
|
+
/**
|
|
3121
|
+
* _forceRemove
|
|
3122
|
+
*
|
|
3123
|
+
* @param node a DOM node
|
|
3124
|
+
*/
|
|
3125
|
+
const _forceRemove = function _forceRemove(node) {
|
|
3126
|
+
arrayPush(DOMPurify.removed, {
|
|
3127
|
+
element: node
|
|
3128
|
+
});
|
|
3129
|
+
try {
|
|
3130
|
+
// eslint-disable-next-line unicorn/prefer-dom-node-remove
|
|
3131
|
+
getParentNode(node).removeChild(node);
|
|
3132
|
+
} catch(_) {
|
|
3133
|
+
remove(node);
|
|
3134
|
+
}
|
|
3135
|
+
};
|
|
3136
|
+
/**
|
|
3137
|
+
* _removeAttribute
|
|
3138
|
+
*
|
|
3139
|
+
* @param name an Attribute name
|
|
3140
|
+
* @param element a DOM node
|
|
3141
|
+
*/
|
|
3142
|
+
const _removeAttribute = function _removeAttribute(name, element) {
|
|
3143
|
+
try {
|
|
3144
|
+
arrayPush(DOMPurify.removed, {
|
|
3145
|
+
attribute: element.getAttributeNode(name),
|
|
3146
|
+
from: element
|
|
3147
|
+
});
|
|
3148
|
+
} catch(_) {
|
|
3149
|
+
arrayPush(DOMPurify.removed, {
|
|
3150
|
+
attribute: null,
|
|
3151
|
+
from: element
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
element.removeAttribute(name);
|
|
3155
|
+
// We void attribute values for unremovable "is" attributes
|
|
3156
|
+
if(name === "is") {
|
|
3157
|
+
if(RETURN_DOM || RETURN_DOM_FRAGMENT) {
|
|
3158
|
+
try {
|
|
3159
|
+
_forceRemove(element);
|
|
3160
|
+
} catch(_) {}
|
|
3161
|
+
} else {
|
|
3162
|
+
try {
|
|
3163
|
+
element.setAttribute(name, "");
|
|
3164
|
+
} catch(_) {}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
};
|
|
3168
|
+
/**
|
|
3169
|
+
* _initDocument
|
|
3170
|
+
*
|
|
3171
|
+
* @param dirty - a string of dirty markup
|
|
3172
|
+
* @returns a DOM, filled with the dirty markup
|
|
3173
|
+
*/
|
|
3174
|
+
const _initDocument = function _initDocument(dirty) {
|
|
3175
|
+
/* Create a HTML document */
|
|
3176
|
+
let doc = null;
|
|
3177
|
+
let leadingWhitespace = null;
|
|
3178
|
+
if(FORCE_BODY) {
|
|
3179
|
+
dirty = "<remove></remove>" + dirty;
|
|
3180
|
+
} else {
|
|
3181
|
+
/* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
|
|
3182
|
+
const matches = stringMatch(dirty, /^[\r\n\t ]+/);
|
|
3183
|
+
leadingWhitespace = matches && matches[0];
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
if(PARSER_MEDIA_TYPE === "application/xhtml+xml" && NAMESPACE === HTML_NAMESPACE) {
|
|
3187
|
+
// Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
|
|
3188
|
+
dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + "</body></html>";
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
|
|
3192
|
+
/*
|
|
3193
|
+
* Use the DOMParser API by default, fallback later if needs be
|
|
3194
|
+
* DOMParser not work for svg when has multiple root element.
|
|
3195
|
+
*/
|
|
3196
|
+
if(NAMESPACE === HTML_NAMESPACE) {
|
|
3197
|
+
try {
|
|
3198
|
+
doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
|
|
3199
|
+
} catch(_) {}
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
/* Use createHTMLDocument in case DOMParser is not available */
|
|
3203
|
+
if(!doc || !doc.documentElement) {
|
|
3204
|
+
doc = implementation.createDocument(NAMESPACE, "template", null);
|
|
3205
|
+
try {
|
|
3206
|
+
doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;
|
|
3207
|
+
} catch(_) {
|
|
3208
|
+
// Syntax error if dirtyPayload is invalid xml
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
const body = doc.body || doc.documentElement;
|
|
3213
|
+
if(dirty && leadingWhitespace) {
|
|
3214
|
+
body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
/* Work on whole document or just its body */
|
|
3218
|
+
if(NAMESPACE === HTML_NAMESPACE) {
|
|
3219
|
+
return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0]
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
return WHOLE_DOCUMENT ? doc.documentElement : body
|
|
3223
|
+
};
|
|
3224
|
+
/**
|
|
3225
|
+
* Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.
|
|
3226
|
+
*
|
|
3227
|
+
* @param root The root element or node to start traversing on.
|
|
3228
|
+
* @returns The created NodeIterator
|
|
3229
|
+
*/
|
|
3230
|
+
const _createNodeIterator = function _createNodeIterator(root) {
|
|
3231
|
+
return createNodeIterator.call(root.ownerDocument || root, root,
|
|
3232
|
+
|
|
3233
|
+
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null)
|
|
3234
|
+
};
|
|
3235
|
+
/**
|
|
3236
|
+
* _isClobbered
|
|
3237
|
+
*
|
|
3238
|
+
* @param element element to check for clobbering attacks
|
|
3239
|
+
* @returns true if clobbered, false if safe
|
|
3240
|
+
*/
|
|
3241
|
+
const _isClobbered = function _isClobbered(element) {
|
|
3242
|
+
return element instanceof HTMLFormElement && (typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function")
|
|
3243
|
+
};
|
|
3244
|
+
/**
|
|
3245
|
+
* Checks whether the given object is a DOM node.
|
|
3246
|
+
*
|
|
3247
|
+
* @param value object to check whether it's a DOM node
|
|
3248
|
+
* @returns true is object is a DOM node
|
|
3249
|
+
*/
|
|
3250
|
+
const _isNode = function _isNode(value) {
|
|
3251
|
+
return typeof Node === "function" && value instanceof Node
|
|
3252
|
+
};
|
|
3253
|
+
function _executeHooks(hooks, currentNode, data) {
|
|
3254
|
+
arrayForEach(hooks, hook => {
|
|
3255
|
+
hook.call(DOMPurify, currentNode, data, CONFIG);
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* _sanitizeElements
|
|
3260
|
+
*
|
|
3261
|
+
* @protect nodeName
|
|
3262
|
+
* @protect textContent
|
|
3263
|
+
* @protect removeChild
|
|
3264
|
+
* @param currentNode to check for permission to exist
|
|
3265
|
+
* @returns true if node was killed, false if left alive
|
|
3266
|
+
*/
|
|
3267
|
+
const _sanitizeElements = function _sanitizeElements(currentNode) {
|
|
3268
|
+
let content = null;
|
|
3269
|
+
/* Execute a hook if present */
|
|
3270
|
+
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
3271
|
+
/* Check if element is clobbered or can clobber */
|
|
3272
|
+
if(_isClobbered(currentNode)) {
|
|
3273
|
+
_forceRemove(currentNode);
|
|
3274
|
+
|
|
3275
|
+
return true
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
/* Now let's check the element's type and name */
|
|
3279
|
+
const tagName = transformCaseFunc(currentNode.nodeName);
|
|
3280
|
+
/* Execute a hook if present */
|
|
3281
|
+
_executeHooks(hooks.uponSanitizeElement, currentNode, {
|
|
3282
|
+
tagName,
|
|
3283
|
+
allowedTags: ALLOWED_TAGS
|
|
3284
|
+
});
|
|
3285
|
+
/* Detect mXSS attempts abusing namespace confusion */
|
|
3286
|
+
if(SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
|
|
3287
|
+
_forceRemove(currentNode);
|
|
3288
|
+
|
|
3289
|
+
return true
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
/* Remove any occurrence of processing instructions */
|
|
3293
|
+
if(currentNode.nodeType === NODE_TYPE.progressingInstruction) {
|
|
3294
|
+
_forceRemove(currentNode);
|
|
3295
|
+
|
|
3296
|
+
return true
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
/* Remove any kind of possibly harmful comments */
|
|
3300
|
+
if(SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
|
|
3301
|
+
_forceRemove(currentNode);
|
|
3302
|
+
|
|
3303
|
+
return true
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
/* Remove element if anything forbids its presence */
|
|
3307
|
+
if(!(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName])) {
|
|
3308
|
+
/* Check if we have a custom element to handle */
|
|
3309
|
+
if(!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
|
3310
|
+
if(CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
|
|
3311
|
+
return false
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
if(CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
|
|
3315
|
+
return false
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
/* Keep content except for bad-listed elements */
|
|
3320
|
+
if(KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
|
|
3321
|
+
const parentNode = getParentNode(currentNode) || currentNode.parentNode;
|
|
3322
|
+
const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
|
|
3323
|
+
if(childNodes && parentNode) {
|
|
3324
|
+
const childCount = childNodes.length;
|
|
3325
|
+
for(let i = childCount - 1; i >= 0; --i) {
|
|
3326
|
+
const childClone = cloneNode(childNodes[i], true);
|
|
3327
|
+
childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
|
|
3328
|
+
parentNode.insertBefore(childClone, getNextSibling(currentNode));
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
_forceRemove(currentNode);
|
|
3334
|
+
|
|
3335
|
+
return true
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
/* Check whether element has a valid namespace */
|
|
3339
|
+
if(currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
|
|
3340
|
+
_forceRemove(currentNode);
|
|
3341
|
+
|
|
3342
|
+
return true
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
/* Make sure that older browsers don't get fallback-tag mXSS */
|
|
3346
|
+
if((tagName === "noscript" || tagName === "noembed" || tagName === "noframes") && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
|
|
3347
|
+
_forceRemove(currentNode);
|
|
3348
|
+
|
|
3349
|
+
return true
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
/* Sanitize element content to be template-safe */
|
|
3353
|
+
if(SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
|
|
3354
|
+
/* Get the element's text content */
|
|
3355
|
+
content = currentNode.textContent;
|
|
3356
|
+
arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
|
|
3357
|
+
content = stringReplace(content, expr, " ");
|
|
3358
|
+
});
|
|
3359
|
+
if(currentNode.textContent !== content) {
|
|
3360
|
+
arrayPush(DOMPurify.removed, {
|
|
3361
|
+
element: currentNode.cloneNode()
|
|
3362
|
+
});
|
|
3363
|
+
currentNode.textContent = content;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
/* Execute a hook if present */
|
|
3368
|
+
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
|
|
3369
|
+
|
|
3370
|
+
return false
|
|
3371
|
+
};
|
|
3372
|
+
/**
|
|
3373
|
+
* _isValidAttribute
|
|
3374
|
+
*
|
|
3375
|
+
* @param lcTag Lowercase tag name of containing element.
|
|
3376
|
+
* @param lcName Lowercase attribute name.
|
|
3377
|
+
* @param value Attribute value.
|
|
3378
|
+
* @returns Returns true if `value` is valid, otherwise false.
|
|
3379
|
+
*/
|
|
3380
|
+
|
|
3381
|
+
const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
|
|
3382
|
+
/* Make sure attribute cannot clobber */
|
|
3383
|
+
if(SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document || value in formElement)) {
|
|
3384
|
+
return false
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
/* Allow valid data-* attributes: At least one character after "-"
|
|
3388
|
+
(https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
|
|
3389
|
+
XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
|
|
3390
|
+
We don't need to check the value; it's always URI safe. */
|
|
3391
|
+
if(ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName))
|
|
3392
|
+
; else if(ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName))
|
|
3393
|
+
; else if(EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag))
|
|
3394
|
+
; else if(!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
|
|
3395
|
+
if(
|
|
3396
|
+
// First condition does a very basic check if a) it's basically a valid custom element tagname AND
|
|
3397
|
+
// b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
|
|
3398
|
+
// and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
|
|
3399
|
+
_isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName, lcTag)) ||
|
|
3400
|
+
// Alternative, second condition checks if it's an `is`-attribute, AND
|
|
3401
|
+
// the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
|
|
3402
|
+
lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value)))
|
|
3403
|
+
; else {
|
|
3404
|
+
return false
|
|
3405
|
+
}
|
|
3406
|
+
/* Check value is safe. First, is attr inert? If so, is safe */
|
|
3407
|
+
} else if(URI_SAFE_ATTRIBUTES[lcName])
|
|
3408
|
+
; else if(regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, "")))
|
|
3409
|
+
; else if((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag])
|
|
3410
|
+
; else if(ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, "")))
|
|
3411
|
+
; else if(value) {
|
|
3412
|
+
return false
|
|
3413
|
+
} else
|
|
3414
|
+
;
|
|
3415
|
+
|
|
3416
|
+
return true
|
|
3417
|
+
};
|
|
3418
|
+
/**
|
|
3419
|
+
* _isBasicCustomElement
|
|
3420
|
+
* checks if at least one dash is included in tagName, and it's not the first char
|
|
3421
|
+
* for more sophisticated checking see https://github.com/sindresorhus/validate-element-name
|
|
3422
|
+
*
|
|
3423
|
+
* @param tagName name of the tag of the node to sanitize
|
|
3424
|
+
* @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
|
|
3425
|
+
*/
|
|
3426
|
+
const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
|
|
3427
|
+
return tagName !== "annotation-xml" && stringMatch(tagName, CUSTOM_ELEMENT)
|
|
3428
|
+
};
|
|
3429
|
+
/**
|
|
3430
|
+
* _sanitizeAttributes
|
|
3431
|
+
*
|
|
3432
|
+
* @protect attributes
|
|
3433
|
+
* @protect nodeName
|
|
3434
|
+
* @protect removeAttribute
|
|
3435
|
+
* @protect setAttribute
|
|
3436
|
+
*
|
|
3437
|
+
* @param currentNode to sanitize
|
|
3438
|
+
*/
|
|
3439
|
+
const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
|
|
3440
|
+
/* Execute a hook if present */
|
|
3441
|
+
_executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
|
|
3442
|
+
const {
|
|
3443
|
+
attributes
|
|
3444
|
+
} = currentNode;
|
|
3445
|
+
/* Check if we have attributes; if not we might have a text node */
|
|
3446
|
+
if(!attributes || _isClobbered(currentNode)) {
|
|
3447
|
+
return
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
const hookEvent = {
|
|
3451
|
+
attrName: "",
|
|
3452
|
+
attrValue: "",
|
|
3453
|
+
keepAttr: true,
|
|
3454
|
+
allowedAttributes: ALLOWED_ATTR,
|
|
3455
|
+
forceKeepAttr: undefined
|
|
3456
|
+
};
|
|
3457
|
+
let l = attributes.length;
|
|
3458
|
+
/* Go backwards over all attributes; safely remove bad ones */
|
|
3459
|
+
while(l--) {
|
|
3460
|
+
const attr = attributes[l];
|
|
3461
|
+
const {
|
|
3462
|
+
name,
|
|
3463
|
+
namespaceURI,
|
|
3464
|
+
value: attrValue
|
|
3465
|
+
} = attr;
|
|
3466
|
+
const lcName = transformCaseFunc(name);
|
|
3467
|
+
const initValue = attrValue;
|
|
3468
|
+
let value = name === "value" ? initValue : stringTrim(initValue);
|
|
3469
|
+
/* Execute a hook if present */
|
|
3470
|
+
hookEvent.attrName = lcName;
|
|
3471
|
+
hookEvent.attrValue = value;
|
|
3472
|
+
hookEvent.keepAttr = true;
|
|
3473
|
+
hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
|
|
3474
|
+
_executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
|
|
3475
|
+
value = hookEvent.attrValue;
|
|
3476
|
+
/* Full DOM Clobbering protection via namespace isolation,
|
|
3477
|
+
* Prefix id and name attributes with `user-content-`
|
|
3478
|
+
*/
|
|
3479
|
+
if(SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name")) {
|
|
3480
|
+
// Remove the attribute with this value
|
|
3481
|
+
_removeAttribute(name, currentNode);
|
|
3482
|
+
// Prefix the value and later re-create the attribute with the sanitized value
|
|
3483
|
+
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
/* Work around a security issue with comments inside attributes */
|
|
3487
|
+
if(SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title|textarea)/i, value)) {
|
|
3488
|
+
_removeAttribute(name, currentNode);
|
|
3489
|
+
continue
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
/* Make sure we cannot easily use animated hrefs, even if animations are allowed */
|
|
3493
|
+
if(lcName === "attributename" && stringMatch(value, "href")) {
|
|
3494
|
+
_removeAttribute(name, currentNode);
|
|
3495
|
+
continue
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
/* Did the hooks approve of the attribute? */
|
|
3499
|
+
if(hookEvent.forceKeepAttr) {
|
|
3500
|
+
continue
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
/* Did the hooks approve of the attribute? */
|
|
3504
|
+
if(!hookEvent.keepAttr) {
|
|
3505
|
+
_removeAttribute(name, currentNode);
|
|
3506
|
+
continue
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
/* Work around a security issue in jQuery 3.0 */
|
|
3510
|
+
if(!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
|
|
3511
|
+
_removeAttribute(name, currentNode);
|
|
3512
|
+
continue
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
/* Sanitize attribute content to be template-safe */
|
|
3516
|
+
if(SAFE_FOR_TEMPLATES) {
|
|
3517
|
+
arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
|
|
3518
|
+
value = stringReplace(value, expr, " ");
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
/* Is `value` valid for this attribute? */
|
|
3523
|
+
const lcTag = transformCaseFunc(currentNode.nodeName);
|
|
3524
|
+
if(!_isValidAttribute(lcTag, lcName, value)) {
|
|
3525
|
+
_removeAttribute(name, currentNode);
|
|
3526
|
+
continue
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
/* Handle attributes that require Trusted Types */
|
|
3530
|
+
if(trustedTypesPolicy && typeof trustedTypes === "object" && typeof trustedTypes.getAttributeType === "function") {
|
|
3531
|
+
if(namespaceURI)
|
|
3532
|
+
; else {
|
|
3533
|
+
switch(trustedTypes.getAttributeType(lcTag, lcName)) {
|
|
3534
|
+
case "TrustedHTML":
|
|
3535
|
+
{
|
|
3536
|
+
value = trustedTypesPolicy.createHTML(value);
|
|
3537
|
+
break
|
|
3538
|
+
}
|
|
3539
|
+
case "TrustedScriptURL":
|
|
3540
|
+
{
|
|
3541
|
+
value = trustedTypesPolicy.createScriptURL(value);
|
|
3542
|
+
break
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
/* Handle invalid data-* attribute set by try-catching it */
|
|
3549
|
+
if(value !== initValue) {
|
|
3550
|
+
try {
|
|
3551
|
+
if(namespaceURI) {
|
|
3552
|
+
currentNode.setAttributeNS(namespaceURI, name, value);
|
|
3553
|
+
} else {
|
|
3554
|
+
/* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
|
|
3555
|
+
currentNode.setAttribute(name, value);
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
if(_isClobbered(currentNode)) {
|
|
3559
|
+
_forceRemove(currentNode);
|
|
3560
|
+
} else {
|
|
3561
|
+
arrayPop(DOMPurify.removed);
|
|
3562
|
+
}
|
|
3563
|
+
} catch(_) {
|
|
3564
|
+
_removeAttribute(name, currentNode);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
/* Execute a hook if present */
|
|
3570
|
+
_executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
|
|
3571
|
+
};
|
|
3572
|
+
/**
|
|
3573
|
+
* _sanitizeShadowDOM
|
|
3574
|
+
*
|
|
3575
|
+
* @param fragment to iterate over recursively
|
|
3576
|
+
*/
|
|
3577
|
+
const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
|
|
3578
|
+
let shadowNode = null;
|
|
3579
|
+
const shadowIterator = _createNodeIterator(fragment);
|
|
3580
|
+
/* Execute a hook if present */
|
|
3581
|
+
_executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);
|
|
3582
|
+
while(shadowNode = shadowIterator.nextNode()) {
|
|
3583
|
+
/* Execute a hook if present */
|
|
3584
|
+
_executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
|
|
3585
|
+
/* Sanitize tags and elements */
|
|
3586
|
+
_sanitizeElements(shadowNode);
|
|
3587
|
+
/* Check attributes next */
|
|
3588
|
+
_sanitizeAttributes(shadowNode);
|
|
3589
|
+
/* Deep shadow DOM detected */
|
|
3590
|
+
if(shadowNode.content instanceof DocumentFragment) {
|
|
3591
|
+
_sanitizeShadowDOM(shadowNode.content);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
/* Execute a hook if present */
|
|
3596
|
+
_executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
|
|
3597
|
+
};
|
|
3598
|
+
|
|
3599
|
+
DOMPurify.sanitize = function(dirty) {
|
|
3600
|
+
const cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
3601
|
+
let body = null;
|
|
3602
|
+
let importedNode = null;
|
|
3603
|
+
let currentNode = null;
|
|
3604
|
+
let returnNode = null;
|
|
3605
|
+
/* Make sure we have a string to sanitize.
|
|
3606
|
+
DO NOT return early, as this will return the wrong type if
|
|
3607
|
+
the user has requested a DOM object rather than a string */
|
|
3608
|
+
IS_EMPTY_INPUT = !dirty;
|
|
3609
|
+
if(IS_EMPTY_INPUT) {
|
|
3610
|
+
dirty = "<!-->";
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
/* Stringify, in case dirty is an object */
|
|
3614
|
+
if(typeof dirty !== "string" && !_isNode(dirty)) {
|
|
3615
|
+
if(typeof dirty.toString === "function") {
|
|
3616
|
+
dirty = dirty.toString();
|
|
3617
|
+
if(typeof dirty !== "string") {
|
|
3618
|
+
throw typeErrorCreate("dirty is not a string, aborting")
|
|
3619
|
+
}
|
|
3620
|
+
} else {
|
|
3621
|
+
throw typeErrorCreate("toString is not a function")
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
/* Return dirty HTML if DOMPurify cannot run */
|
|
3626
|
+
if(!DOMPurify.isSupported) {
|
|
3627
|
+
return dirty
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
/* Assign config vars */
|
|
3631
|
+
if(!SET_CONFIG) {
|
|
3632
|
+
_parseConfig(cfg);
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
/* Clean up removed elements */
|
|
3636
|
+
DOMPurify.removed = [];
|
|
3637
|
+
/* Check if dirty is correctly typed for IN_PLACE */
|
|
3638
|
+
if(typeof dirty === "string") {
|
|
3639
|
+
IN_PLACE = false;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
if(IN_PLACE) {
|
|
3643
|
+
/* Do some early pre-sanitization to avoid unsafe root nodes */
|
|
3644
|
+
if(dirty.nodeName) {
|
|
3645
|
+
const tagName = transformCaseFunc(dirty.nodeName);
|
|
3646
|
+
if(!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|
|
3647
|
+
throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place")
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
} else if(dirty instanceof Node) {
|
|
3651
|
+
/* If dirty is a DOM element, append to an empty document to avoid
|
|
3652
|
+
elements being stripped by the parser */
|
|
3653
|
+
body = _initDocument("<!---->");
|
|
3654
|
+
importedNode = body.ownerDocument.importNode(dirty, true);
|
|
3655
|
+
if(importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === "BODY") {
|
|
3656
|
+
/* Node is already a body, use as is */
|
|
3657
|
+
body = importedNode;
|
|
3658
|
+
} else if(importedNode.nodeName === "HTML") {
|
|
3659
|
+
body = importedNode;
|
|
3660
|
+
} else {
|
|
3661
|
+
// eslint-disable-next-line unicorn/prefer-dom-node-append
|
|
3662
|
+
body.appendChild(importedNode);
|
|
3663
|
+
}
|
|
3664
|
+
} else {
|
|
3665
|
+
/* Exit directly if we have nothing to do */
|
|
3666
|
+
if(!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
|
|
3667
|
+
// eslint-disable-next-line unicorn/prefer-includes
|
|
3668
|
+
dirty.indexOf("<") === -1) {
|
|
3669
|
+
return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
/* Initialize the document to work on */
|
|
3673
|
+
body = _initDocument(dirty);
|
|
3674
|
+
/* Check we have a DOM node from the data */
|
|
3675
|
+
if(!body) {
|
|
3676
|
+
return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ""
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
/* Remove first element node (ours) if FORCE_BODY is set */
|
|
3681
|
+
if(body && FORCE_BODY) {
|
|
3682
|
+
_forceRemove(body.firstChild);
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
/* Get node iterator */
|
|
3686
|
+
const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
|
|
3687
|
+
/* Now start iterating over the created document */
|
|
3688
|
+
while(currentNode = nodeIterator.nextNode()) {
|
|
3689
|
+
/* Sanitize tags and elements */
|
|
3690
|
+
_sanitizeElements(currentNode);
|
|
3691
|
+
/* Check attributes next */
|
|
3692
|
+
_sanitizeAttributes(currentNode);
|
|
3693
|
+
/* Shadow DOM detected, sanitize it */
|
|
3694
|
+
if(currentNode.content instanceof DocumentFragment) {
|
|
3695
|
+
_sanitizeShadowDOM(currentNode.content);
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
/* If we sanitized `dirty` in-place, return it. */
|
|
3700
|
+
if(IN_PLACE) {
|
|
3701
|
+
return dirty
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
/* Return sanitized string or DOM */
|
|
3705
|
+
if(RETURN_DOM) {
|
|
3706
|
+
if(RETURN_DOM_FRAGMENT) {
|
|
3707
|
+
returnNode = createDocumentFragment.call(body.ownerDocument);
|
|
3708
|
+
while(body.firstChild) {
|
|
3709
|
+
// eslint-disable-next-line unicorn/prefer-dom-node-append
|
|
3710
|
+
returnNode.appendChild(body.firstChild);
|
|
3711
|
+
}
|
|
3712
|
+
} else {
|
|
3713
|
+
returnNode = body;
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
if(ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {
|
|
3717
|
+
/*
|
|
3718
|
+
AdoptNode() is not used because internal state is not reset
|
|
3719
|
+
(e.g. the past names map of a HTMLFormElement), this is safe
|
|
3720
|
+
in theory but we would rather not risk another attack vector.
|
|
3721
|
+
The state that is cloned by importNode() is explicitly defined
|
|
3722
|
+
by the specs.
|
|
3723
|
+
*/
|
|
3724
|
+
returnNode = importNode.call(originalDocument, returnNode, true);
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
return returnNode
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
|
|
3731
|
+
/* Serialize doctype if allowed */
|
|
3732
|
+
if(WHOLE_DOCUMENT && ALLOWED_TAGS["!doctype"] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {
|
|
3733
|
+
serializedHTML = "<!DOCTYPE " + body.ownerDocument.doctype.name + ">\n" + serializedHTML;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
/* Sanitize final string template-safe */
|
|
3737
|
+
if(SAFE_FOR_TEMPLATES) {
|
|
3738
|
+
arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
|
|
3739
|
+
serializedHTML = stringReplace(serializedHTML, expr, " ");
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML
|
|
3744
|
+
};
|
|
3745
|
+
DOMPurify.setConfig = function() {
|
|
3746
|
+
const cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
3747
|
+
_parseConfig(cfg);
|
|
3748
|
+
SET_CONFIG = true;
|
|
3749
|
+
};
|
|
3750
|
+
DOMPurify.clearConfig = function() {
|
|
3751
|
+
CONFIG = null;
|
|
3752
|
+
SET_CONFIG = false;
|
|
3753
|
+
};
|
|
3754
|
+
DOMPurify.isValidAttribute = function(tag, attr, value) {
|
|
3755
|
+
/* Initialize shared config vars if necessary. */
|
|
3756
|
+
if(!CONFIG) {
|
|
3757
|
+
_parseConfig({});
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
const lcTag = transformCaseFunc(tag);
|
|
3761
|
+
const lcName = transformCaseFunc(attr);
|
|
3762
|
+
|
|
3763
|
+
return _isValidAttribute(lcTag, lcName, value)
|
|
3764
|
+
};
|
|
3765
|
+
DOMPurify.addHook = function(entryPoint, hookFunction) {
|
|
3766
|
+
if(typeof hookFunction !== "function") {
|
|
3767
|
+
return
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
arrayPush(hooks[entryPoint], hookFunction);
|
|
3771
|
+
};
|
|
3772
|
+
DOMPurify.removeHook = function(entryPoint, hookFunction) {
|
|
3773
|
+
if(hookFunction !== undefined) {
|
|
3774
|
+
const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
|
|
3775
|
+
|
|
3776
|
+
return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0]
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
return arrayPop(hooks[entryPoint])
|
|
3780
|
+
};
|
|
3781
|
+
DOMPurify.removeHooks = function(entryPoint) {
|
|
3782
|
+
hooks[entryPoint] = [];
|
|
3783
|
+
};
|
|
3784
|
+
DOMPurify.removeAllHooks = function() {
|
|
3785
|
+
hooks = _createHooksMap();
|
|
3786
|
+
};
|
|
3787
|
+
|
|
3788
|
+
return DOMPurify
|
|
3789
|
+
}
|
|
3790
|
+
var purify = createDOMPurify();
|
|
3791
|
+
|
|
3792
|
+
class HTML {
|
|
3793
|
+
#domPurify
|
|
3794
|
+
|
|
3795
|
+
/**
|
|
3796
|
+
* Lightweight HTML helper utilities for browser contexts.
|
|
3797
|
+
*
|
|
3798
|
+
* @param {object|(() => unknown)} domPurify - Optional DOMPurify instance or factory.
|
|
3799
|
+
*/
|
|
3800
|
+
constructor(domPurify=purify) {
|
|
3801
|
+
this.#domPurify = domPurify;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
/**
|
|
3805
|
+
* Fetches an HTML fragment and returns the contents inside the <body> tag when present.
|
|
3806
|
+
*
|
|
3807
|
+
* @param {string} url - Location of the HTML resource to load.
|
|
3808
|
+
* @param {boolean} filterBodyContent - If true, returns only content found between the <body> tags. Defaults to false.
|
|
3809
|
+
* @returns {Promise<string>} Sanitized HTML string or empty string on missing content.
|
|
3810
|
+
*/
|
|
3811
|
+
async loadHTML(url, filterBodyContent=false) {
|
|
3812
|
+
try {
|
|
3813
|
+
const response = await fetch(url);
|
|
3814
|
+
const html = await response?.text();
|
|
3815
|
+
|
|
3816
|
+
if(!html)
|
|
3817
|
+
return ""
|
|
3818
|
+
|
|
3819
|
+
const {body} = /<body[^>]*>(?<body>[\s\S]*?)<\/body>/i.exec(html)?.groups ?? {};
|
|
3820
|
+
|
|
3821
|
+
if(filterBodyContent)
|
|
3822
|
+
return body ?? html
|
|
3823
|
+
|
|
3824
|
+
return html
|
|
3825
|
+
} catch(error) {
|
|
3826
|
+
throw Sass.new(`Loading HTML from '${url}'.`, error)
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
/**
|
|
3831
|
+
* Sanitizes arbitrary HTML using DOMPurify.
|
|
3832
|
+
*
|
|
3833
|
+
* @param {string} text - HTML string to sanitize. Defaults to "".
|
|
3834
|
+
* @returns {string} Sanitized HTML.
|
|
3835
|
+
*/
|
|
3836
|
+
sanitise(text="") {
|
|
3837
|
+
const sanitizer = this.#resolveSanitizer();
|
|
3838
|
+
|
|
3839
|
+
return sanitizer(String(text ?? ""))
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
/**
|
|
3843
|
+
* Sanitizes an HTML string and replaces the element's children with the result.
|
|
3844
|
+
*
|
|
3845
|
+
* @param {Element} element - Target element to replace content within.
|
|
3846
|
+
* @param {string} htmlString - HTML string to sanitize and insert.
|
|
3847
|
+
*/
|
|
3848
|
+
setHTMLContent(element, htmlString) {
|
|
3849
|
+
if(!element)
|
|
3850
|
+
throw Sass.new("setHTMLContent requires a valid element.")
|
|
3851
|
+
|
|
3852
|
+
const sanitised = this.sanitise(htmlString);
|
|
3853
|
+
const doc = element.ownerDocument ?? globalThis.document;
|
|
3854
|
+
|
|
3855
|
+
if(doc?.createRange && typeof element.replaceChildren === "function") {
|
|
3856
|
+
const range = doc.createRange();
|
|
3857
|
+
const fragment = range.createContextualFragment(sanitised);
|
|
3858
|
+
|
|
3859
|
+
element.replaceChildren(fragment);
|
|
3860
|
+
|
|
3861
|
+
return
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
if("innerHTML" in element) {
|
|
3865
|
+
element.innerHTML = sanitised;
|
|
3866
|
+
|
|
3867
|
+
return
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
if(typeof element.replaceChildren === "function") {
|
|
3871
|
+
element.replaceChildren(sanitised);
|
|
3872
|
+
|
|
3873
|
+
return
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
throw Sass.new("Unable to set HTML content: unsupported element.")
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
/**
|
|
3880
|
+
* Removes all child nodes from the given element.
|
|
3881
|
+
*
|
|
3882
|
+
* @param {Element} element - Element to clear.
|
|
3883
|
+
*/
|
|
3884
|
+
clearHTMLContent(element) {
|
|
3885
|
+
if(!element)
|
|
3886
|
+
throw Sass.new("clearHTMLContent requires a valid element.")
|
|
3887
|
+
|
|
3888
|
+
if(typeof element.replaceChildren === "function") {
|
|
3889
|
+
element.replaceChildren();
|
|
3890
|
+
|
|
3891
|
+
return
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
if("innerHTML" in element) {
|
|
3895
|
+
element.innerHTML = "";
|
|
3896
|
+
|
|
3897
|
+
return
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
throw Sass.new("Unable to clear HTML content: unsupported element.")
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
/**
|
|
3904
|
+
* Resolves the DOMPurify sanitize function.
|
|
3905
|
+
*
|
|
3906
|
+
* @returns {(input: string) => string} Sanitizer function.
|
|
3907
|
+
*/
|
|
3908
|
+
#resolveSanitizer() {
|
|
3909
|
+
if(this.#domPurify?.sanitize)
|
|
3910
|
+
return this.#domPurify.sanitize
|
|
3911
|
+
|
|
3912
|
+
if(typeof this.#domPurify === "function") {
|
|
3913
|
+
try {
|
|
3914
|
+
const configured = this.#domPurify(globalThis.window ?? globalThis);
|
|
3915
|
+
|
|
3916
|
+
if(configured?.sanitize)
|
|
3917
|
+
return configured.sanitize
|
|
3918
|
+
} catch(error) {
|
|
3919
|
+
throw Sass.new("DOMPurify sanitization is unavailable in this environment.", error)
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
throw Sass.new("DOMPurify sanitization is unavailable in this environment.")
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
var HTML_default = new HTML();
|
|
3928
|
+
|
|
3929
|
+
/**
|
|
3930
|
+
* Thin wrapper around event dispatching to centralize emit/on/off helpers.
|
|
3931
|
+
* Uses `globalThis` for safe resolution in server-side build environments
|
|
3932
|
+
* (e.g. esm.sh) while defaulting to `window` at runtime.
|
|
3933
|
+
*/
|
|
3934
|
+
|
|
3935
|
+
/**
|
|
3936
|
+
* @typedef {object} NotifyEventOptions
|
|
3937
|
+
* @property {boolean} [bubbles] - Whether the event bubbles up the DOM tree.
|
|
3938
|
+
* @property {boolean} [cancelable] - Whether the event can be canceled.
|
|
3939
|
+
* @property {boolean} [composed] - Whether the event can cross the shadow DOM boundary.
|
|
3940
|
+
*/
|
|
3941
|
+
class Notify {
|
|
3942
|
+
/** @type {string} Display name for debugging. */
|
|
3943
|
+
name = "Notify"
|
|
3944
|
+
|
|
3945
|
+
/**
|
|
3946
|
+
* Returns the default event target (window or globalThis).
|
|
3947
|
+
*
|
|
3948
|
+
* @returns {EventTarget} The default event target.
|
|
3949
|
+
*/
|
|
3950
|
+
get #target() {
|
|
3951
|
+
return globalThis.window ?? globalThis
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
/**
|
|
3955
|
+
* Emits a CustomEvent without expecting a return value.
|
|
3956
|
+
*
|
|
3957
|
+
* @param {string} type - Event name to dispatch.
|
|
3958
|
+
* @param {unknown} [payload] - Value assigned to `event.detail`.
|
|
3959
|
+
* @param {boolean | NotifyEventOptions} [options] - CustomEvent options or boolean to set `bubbles`.
|
|
3960
|
+
* @returns {void}
|
|
3961
|
+
*/
|
|
3962
|
+
emit(type, payload=undefined, options=undefined) {
|
|
3963
|
+
const evt = new CustomEvent(type, this.#buildEventInit(payload, options));
|
|
3964
|
+
this.#target.dispatchEvent(evt);
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
/**
|
|
3968
|
+
* Emits a CustomEvent and returns the detail for simple request/response flows.
|
|
3969
|
+
*
|
|
3970
|
+
* @param {string} type - Event name to dispatch.
|
|
3971
|
+
* @param {unknown} [payload] - Value assigned to `event.detail`.
|
|
3972
|
+
* @param {boolean | NotifyEventOptions} [options] - CustomEvent options or boolean to set `bubbles`.
|
|
3973
|
+
* @returns {unknown} The detail placed on the CustomEvent.
|
|
3974
|
+
*/
|
|
3975
|
+
request(type, payload={}, options=undefined) {
|
|
3976
|
+
const evt = new CustomEvent(type, this.#buildEventInit(payload, options));
|
|
3977
|
+
this.#target.dispatchEvent(evt);
|
|
3978
|
+
|
|
3979
|
+
return evt.detail
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
/**
|
|
3983
|
+
* Registers a listener for the given event type on an EventTarget.
|
|
3984
|
+
* Defaults to window when no element is provided.
|
|
3985
|
+
*
|
|
3986
|
+
* @param {string} type - Event name to listen for.
|
|
3987
|
+
* @param {(evt: Event) => void} handler - Listener callback.
|
|
3988
|
+
* @param {EventTarget} [element] - The target to attach the handler to. Defaults to window.
|
|
3989
|
+
* @param {boolean | object} [options] - Options to pass to addEventListener.
|
|
3990
|
+
* @returns {() => void} Dispose function to unregister the handler.
|
|
3991
|
+
*/
|
|
3992
|
+
on(type, handler, element=undefined, options=undefined) {
|
|
3993
|
+
if(!(typeof type === "string" && type))
|
|
3994
|
+
throw new Error("No event 'type' specified to listen for.")
|
|
3995
|
+
|
|
3996
|
+
if(typeof handler !== "function")
|
|
3997
|
+
throw new Error("No handler function specified.")
|
|
3998
|
+
|
|
3999
|
+
const target = element ?? this.#target;
|
|
4000
|
+
target.addEventListener(type, handler, options);
|
|
4001
|
+
|
|
4002
|
+
return () => this.off(type, handler, target, options)
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
/**
|
|
4006
|
+
* Removes a previously registered listener for the given event type.
|
|
4007
|
+
*
|
|
4008
|
+
* @param {string} type - Event name to remove.
|
|
4009
|
+
* @param {(evt: Event) => void} handler - Listener callback to detach.
|
|
4010
|
+
* @param {EventTarget} [element] - The target to remove the handler from. Defaults to window.
|
|
4011
|
+
* @param {boolean | object} [options] - Options to pass to removeEventListener.
|
|
4012
|
+
* @returns {void}
|
|
4013
|
+
*/
|
|
4014
|
+
off(type, handler, element=undefined, options=undefined) {
|
|
4015
|
+
const target = element ?? this.#target;
|
|
4016
|
+
target.removeEventListener(type, handler, options);
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
/**
|
|
4020
|
+
* Builds the CustomEvent init object from detail and options.
|
|
4021
|
+
*
|
|
4022
|
+
* @param {unknown} detail - The event detail payload.
|
|
4023
|
+
* @param {boolean | NotifyEventOptions} [options] - Event options.
|
|
4024
|
+
* @returns {object} The event init object.
|
|
4025
|
+
*/
|
|
4026
|
+
#buildEventInit(detail, options) {
|
|
4027
|
+
if(typeof options === "boolean")
|
|
4028
|
+
return {detail, bubbles: options}
|
|
4029
|
+
|
|
4030
|
+
if(typeof options === "object" && options !== null)
|
|
4031
|
+
return {detail, ...options}
|
|
4032
|
+
|
|
4033
|
+
return {detail}
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
var Notify_default = new Notify();
|
|
4038
|
+
|
|
4039
|
+
/**
|
|
4040
|
+
* Utility class providing helper functions for working with Promises,
|
|
4041
|
+
* including settling, filtering, and extracting values from promise results.
|
|
4042
|
+
*/
|
|
4043
|
+
class Promised {
|
|
4044
|
+
/**
|
|
4045
|
+
* Asynchronously awaits all promises in parallel.
|
|
4046
|
+
* Wrapper around Promise.all for consistency with other utility methods.
|
|
4047
|
+
*
|
|
4048
|
+
* @param {Array<Promise<unknown>>} promises - Array of promises to await
|
|
4049
|
+
* @returns {Promise<Array<unknown>>} Results of all promises
|
|
4050
|
+
*/
|
|
4051
|
+
static async await(promises) {
|
|
4052
|
+
Valid.type(promises, "Promise[]");
|
|
4053
|
+
|
|
4054
|
+
return await Promise.all(promises)
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
/**
|
|
4058
|
+
* Returns the first promise to resolve or reject from an array of promises.
|
|
4059
|
+
* Wrapper around Promise.race for consistency with other utility methods.
|
|
4060
|
+
*
|
|
4061
|
+
* @param {Array<Promise<unknown>>} promises - Array of promises to race
|
|
4062
|
+
* @returns {Promise<unknown>} Result of the first settled promise
|
|
4063
|
+
*/
|
|
4064
|
+
static async race(promises) {
|
|
4065
|
+
Valid.type(promises, "Promise[]");
|
|
4066
|
+
|
|
4067
|
+
return await Promise.race(promises)
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
/**
|
|
4071
|
+
* Settles all promises (both fulfilled and rejected) in parallel.
|
|
4072
|
+
* Wrapper around Promise.allSettled for consistency with other utility methods.
|
|
4073
|
+
*
|
|
4074
|
+
* @param {Array<Promise<unknown>>} promises - Array of promises to settle
|
|
4075
|
+
* @returns {Promise<Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>>} Results of all settled promises with status and value/reason
|
|
4076
|
+
*/
|
|
4077
|
+
static async settle(promises) {
|
|
4078
|
+
Valid.type(promises, "Promise[]");
|
|
4079
|
+
|
|
4080
|
+
return await Promise.allSettled(promises)
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
/**
|
|
4084
|
+
* Checks if any result in the settled promise array is rejected.
|
|
4085
|
+
*
|
|
4086
|
+
* @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
|
|
4087
|
+
* @returns {boolean} True if any result is rejected, false otherwise
|
|
4088
|
+
*/
|
|
4089
|
+
static hasRejected(settled) {
|
|
4090
|
+
return settled.some(r => r.status === "rejected")
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
/**
|
|
4094
|
+
* Checks if any result in the settled promise array is fulfilled.
|
|
4095
|
+
*
|
|
4096
|
+
* @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
|
|
4097
|
+
* @returns {boolean} True if any result is fulfilled, false otherwise
|
|
4098
|
+
*/
|
|
4099
|
+
static hasFulfilled(settled) {
|
|
4100
|
+
return settled.some(r => r.status === "fulfilled")
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
/**
|
|
4104
|
+
* Filters and returns all rejected results from a settled promise array.
|
|
4105
|
+
*
|
|
4106
|
+
* @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
|
|
4107
|
+
* @returns {Array<{status: 'rejected', reason: unknown}>} Array of rejected results
|
|
4108
|
+
*/
|
|
4109
|
+
static rejected(settled) {
|
|
4110
|
+
return settled.filter(r => r.status === "rejected")
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
/**
|
|
4114
|
+
* Filters and returns all fulfilled results from a settled promise array.
|
|
4115
|
+
*
|
|
4116
|
+
* @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} result - Array of settled promise results
|
|
4117
|
+
* @returns {Array<{status: 'fulfilled', value: unknown}>} Array of fulfilled results
|
|
4118
|
+
*/
|
|
4119
|
+
static fulfilled(result) {
|
|
4120
|
+
return result.filter(r => r.status === "fulfilled")
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
/**
|
|
4124
|
+
* Extracts the rejection reasons from a settled promise array.
|
|
4125
|
+
*
|
|
4126
|
+
* @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
|
|
4127
|
+
* @returns {Array<unknown>} Array of rejection reasons
|
|
4128
|
+
*/
|
|
4129
|
+
static reasons(settled) {
|
|
4130
|
+
const rejected = this.rejected(settled);
|
|
4131
|
+
const reasons = rejected.map(e => e.reason);
|
|
4132
|
+
|
|
4133
|
+
return reasons
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
/**
|
|
4137
|
+
* Extracts the values from fulfilled results in a settled promise array.
|
|
4138
|
+
*
|
|
4139
|
+
* @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
|
|
4140
|
+
* @returns {Array<unknown>} Array of fulfilled values
|
|
4141
|
+
*/
|
|
4142
|
+
static values(settled) {
|
|
4143
|
+
const fulfilled = this.fulfilled(settled);
|
|
4144
|
+
const values = fulfilled.map(e => e.value);
|
|
4145
|
+
|
|
4146
|
+
return values
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
/**
|
|
4150
|
+
* Throws a Tantrum containing all rejection reasons from settled promises.
|
|
4151
|
+
*
|
|
4152
|
+
* @param {string} message - Error message. Defaults to "GIGO"
|
|
4153
|
+
* @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
|
|
4154
|
+
* @throws {Tantrum} Throws a Tantrum error with rejection reasons
|
|
4155
|
+
*/
|
|
4156
|
+
static throw(message, settled) {
|
|
4157
|
+
Valid.type(message, "Null|Undefined|String", {allowEmpty: false});
|
|
4158
|
+
Valid.type(settled, "Array");
|
|
4159
|
+
|
|
4160
|
+
message ??= "GIGO";
|
|
4161
|
+
|
|
4162
|
+
const rejected = this.rejected(settled);
|
|
4163
|
+
const reasons = this.reasons(rejected);
|
|
4164
|
+
|
|
4165
|
+
throw Tantrum.new(message, reasons)
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
/**
|
|
4170
|
+
* Utility class for timing operations and promise-based delays.
|
|
4171
|
+
* Provides methods for creating cancellable timeout promises.
|
|
4172
|
+
*/
|
|
4173
|
+
class Time {
|
|
4174
|
+
/**
|
|
4175
|
+
* Creates a promise that resolves after a specified delay.
|
|
4176
|
+
* The returned promise includes a timerId property that can be used with cancel().
|
|
4177
|
+
*
|
|
4178
|
+
* @param {number} delay - Delay in milliseconds before resolving (must be >= 0)
|
|
4179
|
+
* @param {unknown} [value] - Optional value to resolve with, or a function to invoke after the delay
|
|
4180
|
+
* @returns {Promise<unknown> & {timerId: number}} Promise that resolves with the value (or function result) after delay, extended with timerId property
|
|
4181
|
+
* @throws {Sass} If delay is not a number or is negative
|
|
4182
|
+
* @example
|
|
4183
|
+
* // Wait 1 second then continue
|
|
4184
|
+
* await Time.after(1000)
|
|
4185
|
+
*
|
|
4186
|
+
* // Debounce: only apply the latest input after the user stops typing
|
|
4187
|
+
* let pending = null
|
|
4188
|
+
* function onInput(text) {
|
|
4189
|
+
* Time.cancel(pending) // cancel() is a no-op if not a valid Time promise.
|
|
4190
|
+
* pending = Time.after(300, () => applySearch(text))
|
|
4191
|
+
* }
|
|
4192
|
+
*
|
|
4193
|
+
* // Timeout a fetch request
|
|
4194
|
+
* const result = await Promise.race([
|
|
4195
|
+
* fetch("/api/data"),
|
|
4196
|
+
* Time.after(5000, () => { throw new Error("Request timed out") })
|
|
4197
|
+
* ])
|
|
4198
|
+
*
|
|
4199
|
+
* // Cancellable delay
|
|
4200
|
+
* const promise = Time.after(5000, "data")
|
|
4201
|
+
* Time.cancel(promise) // Prevents resolution
|
|
4202
|
+
*/
|
|
4203
|
+
static after(delay, value) {
|
|
4204
|
+
Valid.type(delay, "Number", "delay");
|
|
4205
|
+
Valid.assert(delay >= 0, "delay must be non-negative", delay);
|
|
4206
|
+
|
|
4207
|
+
let timerId;
|
|
4208
|
+
const promise = new Promise((resolve, reject) => {
|
|
4209
|
+
// Cap at max 32-bit signed integer to avoid Node.js timeout overflow warning
|
|
4210
|
+
const safeDelay = Math.min(delay, 2147483647);
|
|
4211
|
+
timerId = setTimeout(() => {
|
|
4212
|
+
try {
|
|
4213
|
+
resolve(Data.isType(value, "Function") ? value() : value);
|
|
4214
|
+
} catch(e) {
|
|
4215
|
+
reject(e);
|
|
4216
|
+
}
|
|
4217
|
+
}, safeDelay);
|
|
4218
|
+
});
|
|
4219
|
+
promise.timerId = timerId;
|
|
4220
|
+
|
|
4221
|
+
return promise
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
/**
|
|
4225
|
+
* Cancels a promise created by Time.after() by clearing its timeout.
|
|
4226
|
+
* If the promise has already resolved or is not a Time.after() promise, this is a no-op.
|
|
4227
|
+
*
|
|
4228
|
+
* @param {Promise<unknown> & {timerId?: number}} promise - Promise returned from Time.after() to cancel
|
|
4229
|
+
* @returns {void}
|
|
4230
|
+
* @example
|
|
4231
|
+
* const promise = Time.after(5000, 'data')
|
|
4232
|
+
* Time.cancel(promise) // Prevents the promise from resolving
|
|
4233
|
+
*/
|
|
4234
|
+
static cancel(promise) {
|
|
4235
|
+
if(promise && typeof promise === "object" && "timerId" in promise)
|
|
4236
|
+
clearTimeout(promise.timerId);
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
exports.Collection = Collection;
|
|
4241
|
+
exports.Data = Data;
|
|
4242
|
+
exports.Disposer = Disposer_default;
|
|
4243
|
+
exports.DisposerClass = Disposer;
|
|
4244
|
+
exports.HTML = HTML_default;
|
|
4245
|
+
exports.HTMLClass = HTML;
|
|
4246
|
+
exports.Notify = Notify_default;
|
|
4247
|
+
exports.NotifyClass = Notify;
|
|
4248
|
+
exports.Promised = Promised;
|
|
4249
|
+
exports.Sass = Sass;
|
|
4250
|
+
exports.Tantrum = Tantrum;
|
|
4251
|
+
exports.Time = Time;
|
|
4252
|
+
exports.Type = TypeSpec;
|
|
4253
|
+
exports.Util = Util;
|
|
4254
|
+
exports.Valid = Valid;
|
|
4255
|
+
|
|
4256
|
+
}));
|