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