diff --git a/.gitignore b/.gitignore index 0f95e2b..08c3e19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +FirewallRemover/ + # Logs logs *.log @@ -102,4 +104,3 @@ dist # TernJS port file .tern-port -.vscode/settings.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..aaeb41b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "name": "vscode-jest-tests.v2.OhMonkey", + "request": "launch", + "args": [ + "--runInBand", + "--watchAll=false", + "--testNamePattern", + "${jest.testNamePattern}", + + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/node_modules/.bin/jest", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3e1dccb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "jest.runMode": "on-demand", + "jest.rootPath": ".", + "testing.automaticallyOpenPeekView": "never", + "jest.jestCommandLine": "npx jest", + "editor.trimWhitespaceOnDelete": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true +} \ No newline at end of file diff --git a/Amazon/AmazonNeutralProtectionButton.user.js b/Amazon/AmazonNeutralProtectionButton.user.js new file mode 100644 index 0000000..502dd8c --- /dev/null +++ b/Amazon/AmazonNeutralProtectionButton.user.js @@ -0,0 +1,16 @@ +// ==UserScript== +// @name Amazon Neutral Protection Button +// @description Remove the bright yellow background on button labeled "Add Protection" (extended warranty) +// @author https://github.com/mkazin +// @homepage https://github.com/mkazin/OhMonkey/tree/main/Amazon +// @version 0.1 +// @license BSD-3-Clause +// @namespace http://tampermonkey.net/ +// @match https://www.amazon.com/*/dp/* +// @icon https://www.google.com/s2/favicons?sz=64&domain=amazon.com +// @grant GM_addStyle +// ==/UserScript== + +const EITHER_BUTTON_SELECTOR = "div.attach-warranty-button-row span.a-button" + +GM_addStyle(`${EITHER_BUTTON_SELECTOR} { background-color: white; border-color: black; }`) diff --git a/Amazon/README.md b/Amazon/README.md index 9a5e366..abd342a 100644 --- a/Amazon/README.md +++ b/Amazon/README.md @@ -2,6 +2,15 @@ ## Amazon.com +### [Amazon Neutral Protection Button](AmazonNeutralProtectionButton.user.js) +Removes formatting highlighting the button to buy an extended warranty. + +Turns this: + +Into this: + + + ### [Amazon Variation Pricer](AmazonVariationPricer.user.js) Shows prices on every color/size option of item. diff --git a/Amazon/img/AmazonNeutralProtectionButton-1_Before.png b/Amazon/img/AmazonNeutralProtectionButton-1_Before.png new file mode 100644 index 0000000..2497bd4 Binary files /dev/null and b/Amazon/img/AmazonNeutralProtectionButton-1_Before.png differ diff --git a/Amazon/img/AmazonNeutralProtectionButton-2_After.png b/Amazon/img/AmazonNeutralProtectionButton-2_After.png new file mode 100644 index 0000000..38e8ab7 Binary files /dev/null and b/Amazon/img/AmazonNeutralProtectionButton-2_After.png differ diff --git a/BostonGlobe.com/Remove BostonGlobe Sports.user.js b/BostonGlobe.com/Remove BostonGlobe Sports.user.js index a5c7612..8029de5 100644 --- a/BostonGlobe.com/Remove BostonGlobe Sports.user.js +++ b/BostonGlobe.com/Remove BostonGlobe Sports.user.js @@ -7,8 +7,11 @@ // @license BSD-3-Clause // @match http://*.bostonglobe.com/* // @match https://www.bostonglobe.com/* +// @require https://github.com/mkazin/OhMonkey/raw/refs/tags/observer-v0.0.4/_Utils/Observer.js // ==/UserScript== +import { ObserverTracker } from 'https://github.com/mkazin/OhMonkey/raw/refs/tags/observer-v0.0.4/_Utils/Observer.js'; + const SPORTS_TERMS = ['Football', 'NFL', 'Damar Hamlin', 'Basketball', 'NBA', 'Celtics', 'Baseball', 'MLB', 'Major League', 'Sox', @@ -73,19 +76,12 @@ window.cleanSportsPosts = function() { removeMatchingElementsContainingText("h2.headline span", term, grandparentNavigator) }) }; +console.log(ObserverTracker) -// Observe changes in the page and reapply. -// Taken from Navneet Khare's fantastic "Remove Sponsored Posts" TamperMonkey script at: -// https://openuserjs.org/install/finitenessofinfinity/Remove_Sponsored_Posts.user.js -var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; -var target = document.getElementsByTagName("body")[0]; -var config = { attributes: true, childList: true, characterData: true }; - -var mutationObserver = new MutationObserver(function(mutations) { - cleanSportsPosts(); -}); - -mutationObserver.observe(target, config); +async function run() { + whenElementAppears("body", cleanSportsPosts); +} +run(); cleanSportsPosts(); diff --git a/Google/GoogleQuestionSuggestionRemover.user.js b/Google/GoogleQuestionSuggestionRemover.user.js index b8dc400..ad88691 100644 --- a/Google/GoogleQuestionSuggestionRemover.user.js +++ b/Google/GoogleQuestionSuggestionRemover.user.js @@ -5,31 +5,37 @@ // @version 1.0 // @description Removes the unhelpful sections in Google suggesting worse questions to ask // @license BSD-3-Clause -// @match https://google.com/search?* -// @match https://www.google.com/search?* +// @match https://google.com/search* +// @match https://www.google.com/search* // @icon https://www.google.com/s2/favicons?sz=64&domain=google.com -// @run-at document-end +// @run-at document-start // @grant GM_addStyle // ==/UserScript== -function hideSelector(selector) { - GM_addStyle(`${selector} { display: none !important; }`) -} -function run() { +const HEADER_SELECTOR = 'div.d0fCJc.BOZ6hd' +const QUESTION_SELECTOR = 'div[jsname="yEVEwb"]' +// Note- the parent part of the selector here is common to other sections +const PEOPLE_ALSO_ASK_SELECTOR = 'div.xfX4Ac.JI5uCe.qB9BY.yWNJXb :has(div.cUnQKe)' +//const WHAT_PEOPLE_ARE_SAYING_SELECTOR = 'div.ULSxyf' // This breaks regular search results, check if :has() is a possibile solution +const LEARN_MORE_LABEL_SELECTOR = 'div.ZKfmAf' +const LEARN_MORE_BOX_SELECTOR = 'div.kLMmLc' +const AI_OVERVIEW_IMAGE_SELECTOR = 'div.oLJ4Uc.l3foLb' +//const PEOPLE_ALSO_SEARCH_FOR_SELECTOR = 'div.ULSxyf' // This breaks regular search results (and is same as above selector) - const HEADER_SELECTOR = 'div.d0fCJc.BOZ6hd' - const QUESTION_SELECTOR = 'div[jsname="yEVEwb"]' - const PEOPLE_ALSO_ASK_SELECTOR = 'div.MjjYud div.cUnQKe' - const WHAT_PEOPLE_ARE_SAYING_SELECTOR = 'div.ULSxyf' - const ALL_SELECTORS = - [ - HEADER_SELECTOR - , QUESTION_SELECTOR - , PEOPLE_ALSO_ASK_SELECTOR - , WHAT_PEOPLE_ARE_SAYING_SELECTOR - ] +const SELECTORS_TO_HIDE = [ + HEADER_SELECTOR + , QUESTION_SELECTOR + , PEOPLE_ALSO_ASK_SELECTOR + // , WHAT_PEOPLE_ARE_SAYING_SELECTOR + , LEARN_MORE_LABEL_SELECTOR + , LEARN_MORE_BOX_SELECTOR + , AI_OVERVIEW_IMAGE_SELECTOR + // , PEOPLE_ALSO_SEARCH_FOR_SELECTOR +] +GM_addStyle(`${SELECTORS_TO_HIDE.join(', ')} { display: none !important; }`) - ALL_SELECTORS.forEach(selector => hideSelector(selector)) -} - -run() +// Allow AI Overview to take up full width now that the "Learn More" section is hidden +const AI_OVERVIEW_CONTAINER_SELECTOR = "div.UxeQfc" +const AI_OVERVIEW_SELECTOR = "div.LT6XE" +GM_addStyle(AI_OVERVIEW_CONTAINER_SELECTOR + ' { grid-template-columns: unset; }') +GM_addStyle(AI_OVERVIEW_SELECTOR + ' { max-width: unset; }') diff --git a/Google/YoutubeHideNextVideos.user.js b/Google/YoutubeHideNextVideos.user.js index e358e77..130de22 100644 --- a/Google/YoutubeHideNextVideos.user.js +++ b/Google/YoutubeHideNextVideos.user.js @@ -19,6 +19,10 @@ const SELECTORS_TO_HIDE = [ "div.ytp-endscreen-content", // "Related" panel "div#related", + // YouTube "Playables" + // The part preceeding ~ in this selector can also get rid of both Playables and "Shorts". + // Replace everything starting with ~ to get rid of both. + "ytd-rich-section-renderer.style-scope.ytd-rich-grid-renderer:has(h2) ~ ytd-rich-section-renderer.style-scope.ytd-rich-grid-renderer:has(h2)", ] GM_addStyle(`${SELECTORS_TO_HIDE.join(",")} { display:none; }`) diff --git a/Google/YoutubeNoComment.user.js b/Google/YoutubeNoComment.user.js index a9ae1e4..ba2011c 100644 --- a/Google/YoutubeNoComment.user.js +++ b/Google/YoutubeNoComment.user.js @@ -2,7 +2,7 @@ // @name YouTube No Comment // @namespace https://github.com/mkazin/OhMonkey // @author Michael Kazin -// @version 1.0 +// @version 1.1 // @description Hides comments to help you avoid engaging // @license BSD-3-Clause // @match https://*.youtube.com/* @@ -20,10 +20,12 @@ const SELECTORS = [ "#comments #action-buttons", // Entire comments section "#comments", + // Chat for live streams + "#chat-container", // Buttons on shorts videos (youtube.com/shorts/) "div#like-button", "div#comments-button", ] -GM_addStyle(`${SELECTORS.join(",")} { display:none; }`) +GM_addStyle(`${SELECTORS.join(", ")} { display:none; }`) diff --git a/README.md b/README.md index e8fdfaf..7e9751f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,27 @@ # OhMonkey -A collection of GreaseMonkey/TamperMonkey scripts I wrote +A collection of Userscripts I've written for my own use -## What are GreaseMonkey and TamperMonkey? +## What's a "Userscript"? +Userscripts are JavaScript programs which are run in a web browser and can modify webpage content, add new features, automate tasks, and enhance the overall user experience in ways the website owner did not provide. Take a look through my scripts to get an idea of the kind of changes I've introduced. -[GreaseMonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) and [TamperMonkey](https://www.tampermonkey.net/) are browser extensions that serve as user script managers. They allow users to customize the behavior of websites they visit by running user scripts, which are small snippets of JavaScript code. These scripts can modify webpage content, add new features, automate tasks, and enhance the overall user experience. +A couple good examples I like to show folks: +* [*America's Test Kitchen Amazon Pricing*](AmericasTestKitchen/README.md) - Adds the current Amazon price of the kitchen products reviewed by ATK. +* [*Amazon Variation Pricer*](Amazon/README.md#amazon-variation-pricer) - displays the prices for each color variation of a product +* [*YouTube No Comment*](Google/README.md#no-comment) - hides the comment section on YouTube videos. - GreaseMonkey is the original and ran only on Mozilla Firefox. TamperMonkey was historically used on Google Chrome and other Chromium-based browsers, but has since been implemented on all browsers and is in more active development. +## What's with "Monkey"? +[GreaseMonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/), [TamperMonkey](https://www.tampermonkey.net/), and [ViolentMonkey](https://violentmonkey.github.io/) are browser extensions that serve as userscript managers. They are responsible for running the appropriate scripts when the user visits a supported website. + GreaseMonkey is the original and ran only on Mozilla Firefox. TamperMonkey was historically used on Google Chrome and other Chromium-based browsers, but has since been implemented on all browsers and is in more active development. ViolentMonkey is the newest, is open source, and supports many browsers. + + Why are they called that? I never really thought much about it. "Grease Monkey" is an old slang term for an auto mechanic. Today "monkey" is a verb used by engineers to describe tinkering with technology, possibly tracing back to the original term. "Tamper" seems obvious, whereas "violent"...? Beats me. + +## Ok... and "Oh Monkey"? +Other than the obvious reference to these browser extensions, it's a phrase I adopted from a colleague I worked with long ago. It was a lovely little phrase he'd often use in place of cursing or as an exclamation of surprise or excitement. + +He's a guy I really enjoyed working with, learned a lot from, and still greatly admire as an engineer and human being. So I hope he doesn't mind me stealing and sharing it. Obviously, I didn't name him here out of respect for his privacy. + +Anyway, I found his phrase to be both professional and fun. I started using it later jobs and it tends to get positive- if sometimes confused- reactions from the folks I've worked with since. ## Support Feel free to create an issue to: @@ -18,7 +33,9 @@ Feel free to create an issue to: If you need something more complex, I might be available for hire. Check out my website for info. -Otherwise, if you like these and find them handy, let me know! + Another place you can try is the [/r/GreaseMonkey](https://www.reddit.com/r/GreaseMonkey/) subreddit. The folks there will sometimes help create a script if you ask nicely. + +* Otherwise, if you like these and find them handy, please let me know! Or feel free to hit that button on the right and buy me a coffee. ## In Remembrance diff --git a/Reddit/README.md b/Reddit/README.md new file mode 100644 index 0000000..f6eb814 --- /dev/null +++ b/Reddit/README.md @@ -0,0 +1,12 @@ +# Reddit Scripts + +## Reddit.com main page + +### [Subreddit Hider](RedditSubredditHider.user.js) +Adds a button to filter out subreddits you don't want to see. + +For example, this annoying karma-farming Subreddit: + + + +Does not require an account or being logged in. Stores the list of hidden subreddits in your browser. \ No newline at end of file diff --git a/Reddit/RedditSubredditHider.user.js b/Reddit/RedditSubredditHider.user.js new file mode 100644 index 0000000..5f4bcb1 --- /dev/null +++ b/Reddit/RedditSubredditHider.user.js @@ -0,0 +1,78 @@ +// ==UserScript== +// @name Reddit Subreddit Hider +// @namespace https://github.com/mkazin/OhMonkey +// @author Michael Kazin +// @version 1.0 +// @description Subreddit filter +// @license BSD-3-Clause +// @match https://www.reddit.com/ +// @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com +// @grant GM_setValue +// @grant GM_getValue +// @grant GM_addStyle +// @run-at document-end +// @require https://raw.githubusercontent.com/mkazin/OhMonkey/refs/heads/main/_Utils/Observer.js +// ==/UserScript== + +const POST_SELECTOR = 'shreddit-post[subreddit-name]'; +const CACHE_KEY = 'rsh-subreddit-list' + +async function init() { + // Hide previously saved subreddits + const subreddits = (await GM_getValue(CACHE_KEY))?.split(',') || []; + subreddits.forEach(name => hideSubreddit(name)); + // console.log(`Loaded list of ${subreddits.length} hidden subreddits: ${subreddits.join(',')}`); + + // Add filter button to posts on page load + document.querySelectorAll(POST_SELECTOR).forEach(postElement => { + const subredditName = postElement.getAttribute('subreddit-name'); + if (!subreddits.includes(subredditName) && !postElement.querySelector('#hide-subreddit-button')) { + createFilterButton(postElement); + } + }); + + // Observe for new posts being added and add a filter button to each + wheneverElementAppears( + POST_SELECTOR, + (postElement) => { createFilterButton(postElement); }, + document, + 0, + "RedditSubredditHider" + ); +} + +function hideSubreddit(subredditName) { + const rule = `article:has(shreddit-post[subreddit-name="${subredditName}"]) { display: none; }`; + GM_addStyle(rule); +} + +function createFilterButton(postElement) { + const newSection = document.createElement('div') + const creditBar = postElement.querySelector(`[id*="feed-post-credit-bar-"]`); + creditBar.prepend(newSection) + + const newButton = document.createElement('button'); + newButton.classList.add('button-primary'); + newButton.id = 'hide-subreddit-button'; + newButton.innerText = 'Hide Subreddit' + newButton.onclick = clickHandler; + newSection.appendChild(newButton); +} + +async function clickHandler(event) { + const subreddits = (await GM_getValue(CACHE_KEY))?.split(',') || []; + const postElement = event.target.closest('shreddit-post'); + const subredditName = postElement.getAttribute('subreddit-name'); + if (subreddits.includes(subredditName)) { + console.warn(`Subreddit ${subredditName} is already hidden.`); + return; + } + + hideSubreddit(subredditName); + subreddits.push(subredditName); + await GM_setValue(CACHE_KEY, subreddits.join(',')); + // console.log(`Subreddit ${subredditName} has been hidden.`); + // console.log(`New list of hidden subreddits: ${subreddits.join(',')}`); +} + +init(); diff --git a/Reddit/img/SubredditHider.png b/Reddit/img/SubredditHider.png new file mode 100644 index 0000000..02e3aac Binary files /dev/null and b/Reddit/img/SubredditHider.png differ diff --git a/_Utils/Observer.js b/_Utils/Observer.js index 11ffc6d..b475ae5 100644 --- a/_Utils/Observer.js +++ b/_Utils/Observer.js @@ -1,29 +1,38 @@ /** * ObserverTracker - A reusable, single-line MutationObserver utility class * - * Sets up a mutation observer to watch for the *first* appearance of a specified selector in the DOM. + * Sets up a mutation observer to watch for the appearance of a specified selector in the DOM. * - * When the selector is found, the provided callback function is invoked with the detected element as a parameter. + * When found, the provided callback function is invoked with the matching element as a parameter. * - * @version 0.0.3 + * @version 0.1.0 * * @param {string} selector - CSS selector to be monitored - * @param {function(Node)} fn - Callback function to invoke when the selector is observed. - * @param {Element} [observationTarget=document] - Optional node to observe for the appearance of the selector. If not provided, the entire DOM will be observed. + * @param {function(Node)} fn - Callback function to invoke when the selector is observed, takes an HTML Element. + * @param {Element} [observationTarget=document] - Optional node to observe for the appearance of the selector. If not provided, the entire document will be observed. * @param {Number} [timeout] - Optional timeout in ms to disable observer. If set to 0, mutationobserver will continue to run until the selector is seen. -*/ + * @param {Boolean} [disconnectOnDetect] - Optional flag to disable automatic disconnection of the observer after the first invocation. + * @param {string} [debugName] - Optional prefix text for debugging purposes, defaults to "ObserverTracker". + */ class ObserverTracker { - constructor(selector, fn, observationTarget, timeout) { - this.created = new Date().getUTCMilliseconds() + // To avoid garbage collection + static #TRACKER_LIST = [] + + constructor(selector, fn, observationTarget, timeout = null, disconnectOnDetect = true, debugName) { this.observer = null; this.selector = selector this.fn = fn this.observationTarget = observationTarget this.timeout = timeout - this.disconnected = false + this.disconnectOnDetect = disconnectOnDetect + this.debugName = `${debugName || "ObserverTracker"}:` + this.wasDisconnected = false this.shadowrootTrackers = [] + ObserverTracker.#TRACKER_LIST.push(this) + + this.uid = window.crypto.randomUUID() - // TBD: should an existing selector yield an immediate invocation and not set up an observer? + // TBD: should an existing selector yield an immediate invocation and not set up an observer if disconnectOnDetect is true? this.observer = new MutationObserver(async (mutations, mutationObserver) => { await this.handleMutations(mutations) @@ -37,7 +46,7 @@ class ObserverTracker { if (timeout) { setTimeout(() => { this.disconnect(); - console.log("Mutation Observer disconnected due to timeout"); + console.debug(`${this.debugName} disconnected on timeout`); }, timeout); } @@ -47,7 +56,7 @@ class ObserverTracker { // Recursively check for shadowRoots and set up observers for them checkShadowRoots(node) { if (node.shadowRoot) { - const shadowRootTracker = whenElementAppears(this.selector, this.fn, node.shadowRoot, this.timeout); + const shadowRootTracker = new ObserverTracker(this.selector, this.fn, node.shadowRoot, this.timeout, this.disconnectOnDetect, `${this.debugName}:SR${node.shadowRoot}`); this.shadowrootTrackers.push(shadowRootTracker); } node.childNodes.forEach(child => this.checkShadowRoots(child)); @@ -57,40 +66,49 @@ class ObserverTracker { this.observer?.disconnect() this.shadowrootTrackers.forEach(st => st.disconnect()); this.shadowrootTrackers = [] - this.disconnected = true + this.wasDisconnected = true this.observer = null + ObserverTracker.#TRACKER_LIST.splice(ObserverTracker.#TRACKER_LIST.indexOf(this), 1); } async handleMutations(mutations) { - // Search mutations where nodes were added for a node matching the selector - const foundTarget = mutations.flatMap( + if (this.wasDisconnected) { + console.debug(`${this.debugName} already disconnected, skipping mutation handling`); + return; + } + const foundTargets = mutations.flatMap( mutation => Array.from(mutation.addedNodes)) - .find(node => node.matches(this.selector)); + .filter(node => node.matches && (node.matches(this.selector) || node.querySelector(this.selector))) + .map(node => node.matches(this.selector) ? node : node.querySelector(this.selector)); - if (foundTarget) { - try { - console.debug(`whenElementAppears: observer of ${this.selector} invoking callback`); - this.fn(foundTarget); - this.disconnect(); - return; - } catch (error) { - // TBD: provide users with an error callback instead? - console.error('whenElementAppears: unexpected error...', error); - } + if (foundTargets.length > 0) { + foundTargets.forEach(foundTarget => { + try { + console.debug(`${this.debugName} invoking callback for ${this.selector}`); + this.fn(foundTarget); + if (this.disconnectOnDetect) { + this.disconnect(); + } + return; + } catch (error) { + // TBD: provide users with an error callback instead? + console.error(`${this.debugName} unexpected error...`, error); + } + }); } else { // TBD: I think I need to add handling in case disconnectAfterDetection is false mutations.forEach(mutation => { // For any new nodes with a shadowRoot, set up a new observer targeting the shadowRoot Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)).forEach(node => { if (node.shadowRoot) { // check if mode is open? - const shadowrootTracker = whenElementAppears(this.selector, this.fn, node.shadowRoot, this.timeout); + const shadowrootTracker = new ObserverTracker(this.selector, this.fn, node.shadowRoot, this.timeout, this.disconnectOnDetect, this.debugName); this.shadowrootTrackers.push(shadowrootTracker); } }); // Check if the mutation target itself has a new shadow root if (mutation.target.shadowRoot && !this.shadowrootTrackers.some(tracker => tracker.observationTarget === mutation.target.shadowRoot)) { - const shadowrootTracker = whenElementAppears(this.selector, this.fn, mutation.target.shadowRoot, this.timeout); + const shadowrootTracker = new ObserverTracker(this.selector, this.fn, mutation.target.shadowRoot, this.timeout, this.disconnectOnDetect, this.debugName); this.shadowrootTrackers.push(shadowrootTracker); } @@ -98,12 +116,38 @@ class ObserverTracker { } } + + static generateRandomUUID() { + return `observer#${ObserverTracker.#TRACKER_LIST.length}`; + } + + toString() { + return `ObserverTracker ${this.debugName}: watching for ${this.selector} on ${this.observationTarget}, wasDisconnected=${this.wasDisconnected})`; + } +} + + +function onceElementAppears(selector, fn, observationTarget = document, timeout = 0, debugName = "onceElementAppears") { + console.debug(`onceElementAppears(${selector}, (fn), ${observationTarget}, ${timeout}, "${debugName}")`) + return new ObserverTracker(selector, fn, observationTarget, timeout, true, debugName) } +function wheneverElementAppears(selector, fn, observationTarget = document, timeout = 0, debugName = "wheneverElementAppears") { + console.debug(`wheneverElementAppears(${selector}, (fn), ${observationTarget}, ${timeout}, "${debugName}")`) + return new ObserverTracker(selector, fn, observationTarget, timeout, false, debugName) +} -function whenElementAppears(selector, fn, observationTarget = document, timeout = 0) { - console.log(`whenElementAppears(${selector}, ${fn}, ${observationTarget}, ${timeout})`) - return new ObserverTracker(selector, fn, observationTarget, timeout) -}; +// Expose for userscript (@require) and Node +if (typeof window !== 'undefined') { + window.ObserverTracker = ObserverTracker; + window.onceElementAppears = onceElementAppears; + window.wheneverElementAppears = wheneverElementAppears; +} else if (typeof globalThis !== 'undefined') { + globalThis.ObserverTracker = ObserverTracker; + globalThis.onceElementAppears = onceElementAppears; + globalThis.wheneverElementAppears = wheneverElementAppears; +} -module.exports = whenElementAppears; +if (typeof module !== 'undefined' && module.exports) { + module.exports = { ObserverTracker, onceElementAppears, wheneverElementAppears }; +} diff --git a/_Utils/README.md b/_Utils/README.md index abe8b11..ca66347 100644 --- a/_Utils/README.md +++ b/_Utils/README.md @@ -5,11 +5,13 @@ This code is vanilla JS and imports no runtime dependencies ## Observer.js -Provides whenElementAppears() - A reusable, single-line MutationObserver utility function. +Provides: +* `ObserverTracker` - a utility class to monitor the DOM for the appearence of a selector. +* `onceElementAppears()` and `wheneverElementAppears()` - Easy-to-use functions to use ObserverTracker for one-time- or ongoing observation, respectively. -Allows scripts to wait for a specific selector to appear on the page prior to running a provided callback function. Whether due to lazy loading, or a high-latency script download. +These allow userscripts to wait for a specific selector to appear on the page prior to running a provided callback function. Whether due to lazy loading, high-latency script download, or scrolling content. -Replaces the numerous lines of fairly boilerplate code which is otherwise required to set up a single-mutation observer, a better technique than the less reliable timeout or interval implementation a developer would otherwise use as a hack. +This replaces the numerous lines of fairly boilerplate code which is otherwise required to set up a single-mutation observer, a better technique than the less reliable timeout or interval implementation some developers use as a hack. Returns an instance of the *ObserverTracker* which allows users to have manual control over disconnecting the mutationObserver and otherwise track its status. @@ -22,42 +24,53 @@ Version 0.0.3 introduced the ability to monitor within an existing [ShadowRoot]( 1. Import the code by adding the following to your userscript header: // @require https://raw.githubusercontent.com/mkazin/OhMonkey/main/_Utils/Observer.js -1. Pass delayed-execution code as a callback to `whenElementAppears()` +1. Pass the desired selector and a callback to `wheneverElementAppears()` or `onceElementAppears()` Parameters: - * @param {string} selector - CSS selector to be monitored - * @param {function} fn - The callback function to invoke the selector is observed. - * @param {DOM element} [observationTarget=document] - Optional node to observe for the appearance of the selector. If not provided, the entire DOM will be observed. Limiting the scope can yield better performance. - * @param {Number} [timeout] - Optional timeout in ms to disable observer. If set to 0, mutationobserver will continue to run until the selector is seen. - * @returns {ObserverTracker} + * `@param {string} selector` - CSS selector to be monitored + * `@param {function} fn` - The callback function to invoke the selector is observed. + * `@param {DOM element} [observationTarget=document]` - Optional node to observe for the appearance of the selector. If not provided, the entire DOM will be observed. Limiting the scope can yield better performance. + * `@param {Number} [timeout]` - Optional timeout in ms to disable observer. By default set to 0 which means it will continue to run until the selector is seen. + * `@param {Boolean} [disconnectOnDetect]` - Optional flag to disable automatic disconnection of the observer after the first invocation (unless already disconnected by the timeout). This parameter does not exist in `wheneverElementAppears()` and `onceElementAppears()` (these hard-code the parameter passed to `ObserverTracker`). + * `@param {string} [debugName]` - Optional prefix text for debugging purposes, defaults to "ObserverTracker". Useful when running multiple observers in your script(s). -### Example -See my [YoutubeAutoSpeed.user.js](../Google/YoutubeAutoSpeed.user.js) userscript where this code was first developed. + * `@returns {ObserverTracker}`, providing access to `disconnect()` which can be called manually. -The function's original code was: +### Example uses - const container = document.querySelector("div#start") - const button = document.createElement("button") - button.onclick = () => { - setSpeed(1.0) - button.textContent = `Speed = 1.0x` - } - button.textContent = `Speed = ${currentSpeed}x` - container.appendChild(button) +1. Pages which load content over time such as infinite scrolling. In this case, we want a long-running MutationObserver. For this use case we can use the `wheneverElementAppears` function, +which sets *ObserverTracker*'s `disconnectOnDetect` to `false`. -To delay the execution until after the `container` element appears in the DOM, it was wrapped with the following code which passes in that element: + See my [RedditSubredditHider.user.js](../Reddit/RedditSubredditHider.user.js) userscript in which newly-loaded posts have a "Hide Subreddit" button added to them to trigger the filtering. - whenElementAppears("div#start", (container) => { +2. Waiting for a site event - a frequent use of MutationObserver is waiting until a website loads asynchronously. Sometimes setting `@run-at` to `document-idle` is sufficient and the correct way to go. Other sites may have Javascript which runs even later. + + YouTube video pages are one example where a necessary element is not available at the time the GreaseMonkey script runs. My +[YoutubeAutoSpeed.user.js](../Google/YoutubeAutoSpeed.user.js) userscript adds a button on the page to allow quickly let a user reset the playback speed to 1x- handy for when the script's code makes a false negative decision on a video where playback shouldn't be adjusted. + + To delay the execution until after the `container` element appears in the DOM, it was wrapped with the following code which passes in that element: + + onceElementAppears("div#start", (container) => { const button = document.createElement("button") ... container.appendChild(button) } +3. Tracking changes to an element over time- On the Boston Globe's website the page for a newspaper story displays a counter of the number of reader comments and is updated as new comments come in. This code observes the comment counter's element and console logs the element's text each time it changes. Since we want to track more than a single change, we can also use the *wheneverElementAppears* function or set disconnectOnDetect to false. +window.wheneverElementAppears("span.sharebar_comment_count", (e) => console.log(`Comment count: ${e.textContent}`)); + + +### Security, Forward-compatablity -### Security Warning As with any browser script, importing code brings risk. Make sure you evaluate the code first. (Please do let me know if you find security concerns I can fix) -I recommend avoiding linking directly to the raw copy of the file on the main branch. Rather I prefer to use a tagged version or even locking to a commit identifier which can't be tampered with should my account get hacked. +Due to the nature of git repositories, the content of the above imported link can change, including changes which may break your code, or even mallicious code if the repository is compromised. The utility might even be moved to a different repository. + +I recommend avoiding linking directly to the raw copy of the file on the main branch. Instead, some possible alternatives: + # A tagged version to avoid future compatability issues, or; + # Lock to a commit identifier which can't be tampered with, even should my account get hacked; or + # Self-host your own copy (e.g. fork or copy to a repo you control) + I don't recommend including the code in your own script, but that's a technical option. ### Development / Contributions Pull requests, bug reports, and suggestions are welcome. @@ -68,11 +81,6 @@ Use the `npm run` commands: `test`, `watch`, and `coverage` Pull requests should pass the existing [unit tests](/__tests__/) and have additional tests to cover updates. -### Security, Forward-compatablity -Due to the nature of git repositories, the content of the above imported link can change, including changes which may break your code, or even mallicious code if the repository is compromised. The utility might even be moved to a different repository. - -To avoid this you may lock your code to a specific version by using a commit ID rather than the "main" branch name. - ### Possible future development This implementation serves several possible use-cases, even allowing for having multiple callbacks run on the same selector. That implementation may benefit from an upgrade which reuses the MutationObserver object. It may not be adequate for some more advanced use-cases. @@ -82,6 +90,3 @@ I expect code using it would also look better, especially as it's currently taki .then(waitForSelector("#desiredElement")) .then(desiredElement => ...) - -Timeouts are another possible addition to allow developers to improve user experience when the code will not execute, such as running some fallback or cleanup code. - diff --git a/__tests__/_Utils/TestObserver.js b/__tests__/_Utils/TestObserver.js index 6163c01..efff2e9 100644 --- a/__tests__/_Utils/TestObserver.js +++ b/__tests__/_Utils/TestObserver.js @@ -1,39 +1,195 @@ - -const whenElementAppears = require('../../_Utils/Observer') +let document; +const { ObserverTracker, onceElementAppears, wheneverElementAppears } = require('../../_Utils/Observer') + +/** + * Mock MutationObserver for testing ObserverTracker + * + * This mock allows you to simulate DOM mutations without relying on real DOM changes. + * It provides helper methods to simulate different types of mutations that would + * normally be detected by a real MutationObserver. + * + * Usage: + * 1. Replace global MutationObserver with this mock in beforeEach() + * 2. Create your ObserverTracker instance + * 3. Get the mock instance with MockMutationObserver.getLastInstance() + * 4. Use simulateAddedNodes(), simulateRemovedNodes(), etc. to trigger mutations + * + * Example: + * const tracker = onceElementAppears('#test', callback); + * const mockObserver = MockMutationObserver.getLastInstance(); + * document.body.appendChild(targetElement); + * mockObserver.simulateAddedNodes([targetElement]); + */ +class MockMutationObserver { + constructor(callback) { + this.callback = callback; + this.observedTargets = []; + this.isObserving = false; + MockMutationObserver.instances.push(this); + } + + observe(target, options) { + this.observedTargets.push({ target, options }); + this.isObserving = true; + } + + disconnect() { + this.isObserving = false; + this.observedTargets = []; + } + + takeRecords() { + return []; + } + + // Test helper methods + simulateAddedNodes(addedNodes, target = null) { + + if (!this.isObserving) return; + + const mutationRecord = { + type: 'childList', + target: target || this.observedTargets[0]?.target || document.body, + addedNodes: Array.isArray(addedNodes) ? addedNodes : [addedNodes], + removedNodes: [], + previousSibling: null, + nextSibling: null, + attributeName: null, + attributeNamespace: null, + oldValue: null + }; + + this.callback([mutationRecord], this); + } + + simulateRemovedNodes(removedNodes, target = null) { + if (!this.isObserving) return; + + const mutationRecord = { + type: 'childList', + target: target || this.observedTargets[0]?.target || document.body, + addedNodes: [], + removedNodes: Array.isArray(removedNodes) ? removedNodes : [removedNodes], + previousSibling: null, + nextSibling: null, + attributeName: null, + attributeNamespace: null, + oldValue: null + }; + + this.callback([mutationRecord], this); + } + + simulateAttributeChange(target, attributeName, oldValue = null) { + if (!this.isObserving) return; + + const mutationRecord = { + type: 'attributes', + target: target, + addedNodes: [], + removedNodes: [], + previousSibling: null, + nextSibling: null, + attributeName: attributeName, + attributeNamespace: null, + oldValue: oldValue + }; + + this.callback([mutationRecord], this); + } + + // Static methods for test management + static instances = []; + + static clearInstances() { + this.instances = []; + } + + static getLastInstance() { + return this.instances[this.instances.length - 1]; + } + + static getAllInstances() { + return this.instances; + } +} + +window.randomUUID = crypto.randomUUID || (() => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); +})); beforeEach(() => { // Suppress console output for function's debug messages jest.spyOn(console, 'warn').mockImplementation(() => { }); jest.spyOn(console, 'debug').mockImplementation(() => { }); - // Reset the DOM for test isolation, I'm seeing bleeding from one to another + // Use the Jest-provided global document/window so MutationObserver.observe + // receives Node instances from the same realm as the observer. + document = global.document; + + // Replace MutationObserver with our mock + global.MutationObserver = MockMutationObserver; + MockMutationObserver.clearInstances(); + + // Reset the DOM for test isolation document.body.innerHTML = ''; }); afterEach(() => { jest.restoreAllMocks(); + MockMutationObserver.clearInstances(); +}); + +describe('Exports', () => { + + test('should export onceElementAppears function', () => { + expect(typeof onceElementAppears).toBe('function'); + }); + test('window should have onceElementAppears function', () => { + expect(typeof window.onceElementAppears).toBe('function'); + }); + test('should export wheneverElementAppears function', () => { + expect(typeof wheneverElementAppears).toBe('function'); + }); + test('window should have wheneverElementAppears function', () => { + expect(typeof window.wheneverElementAppears).toBe('function'); + }); + test('should export ObserverTracker class', () => { + expect(typeof ObserverTracker).toBe('function'); + }); }); -describe('whenElementAppears', () => { +describe('onceElementAppears', () => { test('should observe and call function when element appears', async () => { const mockFn = jest.fn(); - const element = document.createElement('div'); - element.id = 'testElement'; + const targetElement = document.createElement('div'); + targetElement.id = 'testElement'; + document.body.appendChild(targetElement); - whenElementAppears('#testElement', mockFn); + // Create the observer + const tracker = onceElementAppears('#testElement', mockFn, document); expect(mockFn).not.toHaveBeenCalled(); - document.body.appendChild(element); - await new Promise(process.nextTick); + // Get the mock MutationObserver instance + const mockObserver = MockMutationObserver.getLastInstance(); + expect(mockObserver).toBeDefined(); + expect(mockObserver.isObserving).toBe(true); + + mockObserver.simulateAddedNodes([targetElement], document.body); expect(mockFn.mock.calls).toHaveLength(1); - expect(mockFn).toHaveBeenCalledWith(element); + expect(mockFn).toHaveBeenCalledWith(targetElement); - // Further DOM changes should not trigger invocations - element.style.display = 'block'; - document.body.removeChild(element); - await new Promise(process.nextTick); + // Verify observer was disconnected after first detection (onceElementAppears behavior) + expect(mockObserver.isObserving).toBe(false); + + // Further mutations should not trigger the callback since observer was disconnected + const anotherElement = document.createElement('div'); + anotherElement.id = 'testElement'; + document.body.appendChild(anotherElement); + mockObserver.simulateAddedNodes([anotherElement], document.body); expect(mockFn.mock.calls).toHaveLength(1); }); @@ -41,29 +197,33 @@ describe('whenElementAppears', () => { const mockFn = jest.fn(); const firstChild = document.createElement('div'); const secondChild = document.createElement('div'); - const targetElement = document.createElement('div') - document.body.appendChild(firstChild) - document.body.appendChild(secondChild) - document.body.id = 'body' - targetElement.id = "target" - firstChild.id = 'firstChild' - secondChild.id = 'secondChild' - whenElementAppears('#target', mockFn); + const targetElement = document.createElement('div'); + + targetElement.id = "target"; + firstChild.id = 'firstChild'; + secondChild.id = 'secondChild'; + + // Create the observer + onceElementAppears('#target', mockFn); + expect(mockFn).not.toHaveBeenCalled(); + + const mockObserver = MockMutationObserver.getLastInstance(); + + // Simulate multiple mutations - first without target, then with target + mockObserver.simulateAddedNodes([firstChild], document.body); + expect(mockFn).not.toHaveBeenCalled(); + + mockObserver.simulateAddedNodes([secondChild], document.body); expect(mockFn).not.toHaveBeenCalled(); - // The following changes generate three childList mutation records - firstChild.appendChild(targetElement) - secondChild.appendChild(targetElement) - await new Promise(process.nextTick); + // Now simulate adding the target element directly + mockObserver.simulateAddedNodes([targetElement], firstChild); expect(mockFn.mock.calls).toHaveLength(1); expect(mockFn).toHaveBeenCalledWith(targetElement); - // Further DOM changes should not trigger invocations - targetElement.style.display = 'block'; - targetElement.remove(); - await new Promise(process.nextTick); - expect(mockFn.mock.calls).toHaveLength(1); + // Verify observer was disconnected after first detection + expect(mockObserver.isObserving).toBe(false); }); it('should handle multiple calls with different selectors and functions', async () => { @@ -75,15 +235,17 @@ describe('whenElementAppears', () => { const element2 = document.createElement('div'); element2.id = 'testElement2'; - whenElementAppears('#testElement1', mockFn1); - whenElementAppears('#testElement2', mockFn2); + onceElementAppears('#testElement1', mockFn1); + const mockObserver1 = MockMutationObserver.getLastInstance(); + onceElementAppears('#testElement2', mockFn2); + const mockObserver2 = MockMutationObserver.getLastInstance(); expect(mockFn1).not.toHaveBeenCalled(); expect(mockFn2).not.toHaveBeenCalled(); - // Appending the first element should only trigger the first callback + // Simulate multiple mutations - first without target, then with target document.body.appendChild(element1); - await new Promise(process.nextTick); + mockObserver1.simulateAddedNodes([element1], document.body); expect(mockFn1).toHaveBeenCalled(); expect(mockFn1).toHaveBeenCalledWith(element1); @@ -92,15 +254,12 @@ describe('whenElementAppears', () => { // Appending the second element should only trigger the second call, // the first selector should no longer be observed document.body.appendChild(element2); - await new Promise(process.nextTick); + mockObserver2.simulateAddedNodes([element2], document.body); expect(mockFn2).toHaveBeenCalled(); expect(mockFn2).toHaveBeenCalledWith(element2); expect(mockFn1.mock.calls).toHaveLength(1); expect(mockFn2.mock.calls).toHaveLength(1); - - document.body.removeChild(element1); - document.body.removeChild(element2); }); it('should handle multiple calls with the same selector and separate functions', async () => { @@ -109,15 +268,18 @@ describe('whenElementAppears', () => { const element = document.createElement('div'); element.id = 'testElement'; - whenElementAppears('#testElement', mockFn1); - whenElementAppears('#testElement', mockFn2); + onceElementAppears('#testElement', mockFn1); + const mockObserver1 = MockMutationObserver.getLastInstance(); + onceElementAppears('#testElement', mockFn2); + const mockObserver2 = MockMutationObserver.getLastInstance(); expect(mockFn1).not.toHaveBeenCalled(); expect(mockFn2).not.toHaveBeenCalled(); // Appending the first element should only trigger the first callback + mockObserver1.simulateAddedNodes([element], document.body); + mockObserver2.simulateAddedNodes([element], document.body); document.body.appendChild(element); - await new Promise(process.nextTick); expect(mockFn1).toHaveBeenCalled(); expect(mockFn1).toHaveBeenCalledWith(element); @@ -144,7 +306,8 @@ describe('whenElementAppears', () => { observedElement.id = 'observedElement' observedElement.className = 'observationSelector' - whenElementAppears('.observationSelector', observedFn, observationTarget = observedBranch); + onceElementAppears('.observationSelector', observedFn, observedBranch); + const mockObserver = MockMutationObserver.getLastInstance(); expect(ignoredFn).not.toHaveBeenCalled(); expect(observedFn).not.toHaveBeenCalled(); @@ -152,7 +315,7 @@ describe('whenElementAppears', () => { // Append elements with the desired selector to their respective branches ignoredBranch.appendChild(ignoredElement); observedBranch.appendChild(observedElement); - await new Promise(process.nextTick); + mockObserver.simulateAddedNodes([observedElement], observedBranch); // Only one branch's should generate a mutation and have its callback invoked expect(observedFn.mock.calls).toHaveLength(1); @@ -166,19 +329,20 @@ describe('whenElementAppears', () => { const element = document.createElement('div'); element.id = 'testElement'; - const tracker = whenElementAppears('#testElement', mockFn); + const tracker = onceElementAppears('#testElement', mockFn); + const mockObserver = MockMutationObserver.getLastInstance(); expect(mockFn).not.toHaveBeenCalled(); element.style.display = 'block'; document.body.appendChild(element); - await new Promise(process.nextTick); + mockObserver.simulateAddedNodes([element], document.body); expect(mockFn.mock.calls).toHaveLength(1); expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledWith(element); - expect(tracker.disconnected).toBe(true); + expect(tracker.wasDisconnected).toBe(true); expect(tracker.observer).toBe(null); document.body.removeChild(element); @@ -190,11 +354,13 @@ describe('whenElementAppears', () => { element.id = 'testElement'; document.body.appendChild(element); - whenElementAppears('#testElement', mockFn); + onceElementAppears('#testElement', mockFn); + const mockObserver = MockMutationObserver.getLastInstance(); expect(mockFn).not.toHaveBeenCalled(); document.body.removeChild(element); + mockObserver.simulateRemovedNodes([element], document.body); element.style.display = 'block'; @@ -208,7 +374,13 @@ describe('whenElementAppears', () => { element.style.display = 'block'; document.body.appendChild(element); - whenElementAppears('#testElement', mockFn); + onceElementAppears('#testElement', mockFn); + const mockObserver = MockMutationObserver.getLastInstance(); + + const otherElement = document.createElement('div'); + otherElement.id = 'otherElement'; + document.body.appendChild(otherElement); + mockObserver.simulateAddedNodes([otherElement], document.body); expect(mockFn).not.toHaveBeenCalled(); @@ -223,28 +395,72 @@ describe('whenElementAppears', () => { element.style.display = 'block'; document.body.appendChild(element); - const tracker = whenElementAppears('#testElement', mockFn, document); + const tracker = onceElementAppears('#testElement', mockFn, document); + const mockObserver = MockMutationObserver.getLastInstance(); element.dataset.att = 'changed'; - await new Promise(process.nextTick); + mockObserver.simulateAttributeChange(element, 'data-att', 'original', 'changed'); expect(mockFn).not.toHaveBeenCalled(); tracker.disconnect(); }); + it('should detect and return a matching descendent of a mutated element ', async () => { + const mockFn = jest.fn(); + const parentElement = document.createElement('div'); + const childElement = document.createElement('div'); + childElement.id = 'testElement'; + parentElement.appendChild(childElement); + + const tracker = onceElementAppears('#testElement', mockFn, document); + const mockObserver = MockMutationObserver.getLastInstance(); + + // Simulate a mutation that adds the parent element + document.body.appendChild(parentElement); + mockObserver.simulateAddedNodes([parentElement], document.body); + + expect(mockFn).toHaveBeenCalled(); + expect(mockFn).toHaveBeenCalledWith(childElement); + + tracker.disconnect(); + }); + + it('should it detect a pre-existing selector element that is moved into an observed element?', async () => { + const mockFn = jest.fn(); + const originParentElement = document.createElement('div'); + const childElement = document.createElement('div'); + childElement.id = 'testElement'; + originParentElement.appendChild(childElement); + document.body.appendChild(originParentElement); + + const tracker = onceElementAppears('#testElement', mockFn, document); + const mockObserver = MockMutationObserver.getLastInstance(); + + // Simulate a mutation that moves the child element + const targetParentElement = document.createElement('div'); + targetParentElement.appendChild(childElement); + document.body.appendChild(targetParentElement); + mockObserver.simulateRemovedNodes([childElement], originParentElement); + mockObserver.simulateAddedNodes([targetParentElement], document.body); + + expect(mockFn).toHaveBeenCalled(); + expect(mockFn.mock.calls).toHaveLength(1); + expect(mockFn).toHaveBeenCalledWith(childElement); + }); + it('should detect elements that appear within a shadow DOM', async () => { const mockFn = jest.fn(); const hostElement = document.createElement('div'); const shadowRoot = hostElement.attachShadow({ mode: 'open' }); document.body.appendChild(hostElement); - whenElementAppears('#testElement', mockFn, document); - await new Promise(process.nextTick); + onceElementAppears('#testElement', mockFn, document); + const mockObserver = MockMutationObserver.getLastInstance(); const element = document.createElement('div'); element.id = 'testElement'; shadowRoot.appendChild(element); - await new Promise(process.nextTick); + mockObserver.simulateAddedNodes([element], shadowRoot); expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledWith(element); @@ -253,58 +469,76 @@ describe('whenElementAppears', () => { }); - describe('Interval testing', () => { - it('should disable the mutation observer when the specified interval elapses, having invoked the callback', async () => { + describe('Timeout testing', () => { + let tracker; + + beforeEach(() => { jest.useFakeTimers(); + }); - const TEST_INTERVAL = 1000; + afterEach(() => { + if (tracker) { + tracker.disconnect(); + } + jest.useRealTimers(); + jest.clearAllTimers(); + }); + + it('should disable the mutation observer when invoking the callback before the timeout elapses', () => { + const TEST_TIMEOUT = 1000; const mockFn = jest.fn(); const element = document.createElement('div'); element.id = 'testElement'; - whenElementAppears('#testElement', mockFn, observationTarget = document, interval = TEST_INTERVAL); - - // setTimeout(() => { document.body.appendChild(element); }, TEST_INTERVAL * 0.95); - - expect(mockFn).not.toHaveBeenCalled(); + tracker = onceElementAppears('#testElement', mockFn, document, TEST_TIMEOUT); + const mockObserver = MockMutationObserver.getLastInstance(); // Advance not enough time to trigger the disabling interval - jest.advanceTimersByTime(TEST_INTERVAL / 2); + jest.advanceTimersByTime(TEST_TIMEOUT / 2); expect(mockFn).not.toHaveBeenCalled(); + expect(tracker.wasDisconnected).toBe(false); // Trigger the callback by providing the observer a selector target - jest.useRealTimers(); document.body.appendChild(element); - await new Promise(process.nextTick); + mockObserver.simulateAddedNodes([element], document.body); - expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledWith(element); + expect(tracker.wasDisconnected).toBe(true); + expect(mockObserver.isObserving).toBe(false); + + // Advance past original timeout — nothing additional should happen + jest.advanceTimersByTime(TEST_TIMEOUT * 2); + expect(tracker.wasDisconnected).toBe(true); + expect(mockFn.mock.calls.length).toBe(1); + + // Try to trigger a second callback, which should not work since the observer is disconnected + const secondElement = element.cloneNode(true); + document.body.appendChild(secondElement); + mockObserver.simulateAddedNodes([secondElement], document.body); + expect(mockFn.mock.calls.length).toBe(1); }); - it('should disable the mutation observer when the specified interval elapses, without invoking the callback', async () => { - jest.useFakeTimers(); - - const TEST_INTERVAL = 1000; + it('should disable the mutation observer on timeout, without invoking the callback', () => { + const TEST_TIMEOUT = 1000; const mockFn = jest.fn(); const nonMatchingElement = document.createElement('div'); nonMatchingElement.id = 'testElement'; - whenElementAppears('#ImpossibleElement', mockFn, observationTarget = document, interval = TEST_INTERVAL); - - expect(mockFn).not.toHaveBeenCalled(); + tracker = onceElementAppears('#ImpossibleElement', mockFn, document, TEST_TIMEOUT); + const mockObserver = MockMutationObserver.getLastInstance(); - jest.advanceTimersByTime(TEST_INTERVAL / 2); + // Run time forward a bit, trigger the mutation observer, with a non-matching target + jest.advanceTimersByTime(TEST_TIMEOUT / 2); expect(mockFn).not.toHaveBeenCalled(); - - // Run time forward callback without providing the observer a selector target - jest.useRealTimers(); document.body.appendChild(nonMatchingElement); - await new Promise(process.nextTick); - - // jest.advanceTimersByTime(TEST_INTERVAL); + mockObserver.simulateAddedNodes([nonMatchingElement], document.body); + expect(tracker.wasDisconnected).toBe(false); + // Advance time past the timeout, the observer should disconnect without invoking the callback + jest.advanceTimersByTime(TEST_TIMEOUT * 2); expect(mockFn).not.toHaveBeenCalled(); + expect(tracker.wasDisconnected).toBe(true); + expect(mockObserver.isObserving).toBe(false); }); }); - }); diff --git a/__tests__/jest.setup.js b/__tests__/jest.setup.js new file mode 100644 index 0000000..5c2529f --- /dev/null +++ b/__tests__/jest.setup.js @@ -0,0 +1,15 @@ +// TextEncoder and TextDecoder are not available in Node.js by default +// and are required for jsdom. +// In production code, these would be available in the browser environment, +// but for testing we need to polyfill them. +const TextEncoder = require('util').TextEncoder; +const TextDecoder = require('util').TextDecoder; + +if (typeof global.TextEncoder === 'undefined') { + global.TextEncoder = TextEncoder; +} +if (typeof global.TextDecoder === 'undefined') { + global.TextDecoder = TextDecoder; +} +// This polyfill ensures that the randomUUID function is available in all environments +window.crypto.randomUUID = window.crypto.randomUUID ? window.crypto.randomUUID : crypto.randomUUID ? crypto.randomUUID : () => ObserverTracker.generateRandomUUID() diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..4c2b495 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + setupFilesAfterEnv: ['/__tests__/jest.setup.js'], + testEnvironment: 'jsdom', + // Exclude config files from being treated as test files + testPathIgnorePatterns: [ + 'node_modules/', + '__tests__/jest.config.js', + '__tests__/jest.setup.js' + ] +}; diff --git a/package-lock.json b/package-lock.json index 5625d9b..a555a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,77 +9,93 @@ "version": "0.0.1", "license": "ISC", "devDependencies": { + "brace-expansion": "^1.1.12", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "jsdom": "^23.2.0" + "jsdom": "^27.1.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "node_modules/@acemir/cssom": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.23.tgz", + "integrity": "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } + "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz", - "integrity": "sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.1", - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" } }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz", - "integrity": "sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", "dev": true, "license": "MIT", "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", - "css-tree": "^2.3.1", - "is-potential-custom-element-name": "^1.0.1" + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" } }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -87,22 +103,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", - "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.9", - "@babel/types": "^7.26.9", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -118,16 +134,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", - "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -135,14 +151,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -151,30 +167,40 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -184,9 +210,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { @@ -194,9 +220,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -204,9 +230,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -214,9 +240,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -224,27 +250,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -309,13 +335,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -493,48 +519,48 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", - "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -548,9 +574,9 @@ "license": "MIT" }, "node_modules/@csstools/color-helpers": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", - "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, "funding": [ { @@ -568,9 +594,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz", - "integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, "funding": [ { @@ -587,14 +613,14 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz", - "integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, "funding": [ { @@ -608,21 +634,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.0.1", - "@csstools/css-calc": "^2.1.1" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -639,13 +665,33 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.15.tgz", + "integrity": "sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -982,34 +1028,31 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -1024,9 +1067,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1086,9 +1129,9 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", "dependencies": { @@ -1107,13 +1150,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/graceful-fs": { @@ -1215,9 +1258,9 @@ "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1252,9 +1295,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -1334,28 +1377,6 @@ "dev": true, "license": "MIT" }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -1390,26 +1411,10 @@ "node": ">=8" } }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -1430,24 +1435,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/balanced-match": { @@ -1457,6 +1445,16 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", + "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -1468,9 +1466,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1492,9 +1490,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "dev": true, "funding": [ { @@ -1512,10 +1510,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -1541,6 +1540,20 @@ "dev": true, "license": "MIT" }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1562,9 +1575,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001699", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", - "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "dev": true, "funding": [ { @@ -1750,13 +1763,13 @@ } }, "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.0.30", + "mdn-data": "2.12.2", "source-map-js": "^1.0.1" }, "engines": { @@ -1771,44 +1784,38 @@ "license": "MIT" }, "node_modules/cssstyle": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", - "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^2.8.2", - "rrweb-cssom": "^0.8.0" + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" } }, - "node_modules/cssstyle/node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", "dev": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" + "whatwg-url": "^15.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1824,9 +1831,9 @@ } }, "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, "license": "MIT" }, @@ -1899,10 +1906,35 @@ "node": ">=12" } }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.100", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.100.tgz", - "integrity": "sha512-u1z9VuzDXV86X2r3vAns0/5ojfXBue9o0+JDUDBKYqGLjxLkSqsSUoPU/6kW0gx76V44frHaf6Zo+QF74TQCMg==", + "version": "1.5.248", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.248.tgz", + "integrity": "sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==", "dev": true, "license": "ISC" }, @@ -1949,6 +1981,55 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2120,14 +2201,16 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2186,6 +2269,31 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2196,6 +2304,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2231,14 +2353,17 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { @@ -2258,6 +2383,35 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2709,6 +2863,61 @@ } } }, + "node_modules/jest-config/node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -2919,6 +3128,22 @@ } } }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jest-environment-jsdom/node_modules/tr46": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", @@ -2945,6 +3170,16 @@ "node": ">=14" } }, + "node_modules/jest-environment-jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -3410,39 +3645,38 @@ } }, "node_modules/jsdom": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz", - "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==", + "version": "27.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.1.0.tgz", + "integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/dom-selector": "^2.0.1", - "cssstyle": "^4.0.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.0", + "@acemir/cssom": "^0.9.19", + "@asamuzakjp/dom-selector": "^6.7.3", + "cssstyle": "^5.3.2", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.3", + "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", + "webidl-conversions": "^8.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", - "ws": "^8.16.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "canvas": "^2.11.2" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -3450,6 +3684,32 @@ } } }, + "node_modules/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3572,10 +3832,20 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "dev": true, "license": "CC0-1.0" }, @@ -3668,9 +3938,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -3698,9 +3968,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", - "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "dev": true, "license": "MIT" }, @@ -3875,9 +4145,9 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, "license": "MIT", "engines": { @@ -4074,13 +4344,6 @@ "node": ">=10" } }, - "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", - "dev": true, - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4332,6 +4595,26 @@ "node": ">=8" } }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -4353,32 +4636,29 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^7.0.5" }, "engines": { - "node": ">=6" + "node": ">=16" } }, "node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/type-detect": { @@ -4422,9 +4702,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -4502,13 +4782,13 @@ } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=12" + "node": ">=20" } }, "node_modules/whatwg-encoding": { @@ -4535,17 +4815,17 @@ } }, "node_modules/whatwg-url": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", - "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/which": { @@ -4604,9 +4884,9 @@ } }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index c9b4584..de68a89 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,18 @@ "main": "utils.js", "scripts": { "coverage": "npx jest --coverage", - "test": "npx jest", + "test": "npx --experimental-vm-module jest", "watch": "npx jest --watch" }, - "jest": { - "testEnvironment": "jsdom" - }, + "engines": {}, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "jsdom": "^23.2.0" + "jsdom": "^27.1.0" + + ,"brace-expansion": "^1.1.12" } }