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