diff --git a/importExport.js b/importExport.js
index 3e55fca..19b9f4f 100644
--- a/importExport.js
+++ b/importExport.js
@@ -1,25 +1,20 @@
// Import/Export functionality for Video Speed Controller settings
+var speederShared =
+ typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
+var importExportUtils = speederShared.importExport || {};
function generateBackupFilename() {
- const now = new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, "0");
- const day = String(now.getDate()).padStart(2, "0");
- const hours = String(now.getHours()).padStart(2, "0");
- const minutes = String(now.getMinutes()).padStart(2, "0");
- const seconds = String(now.getSeconds()).padStart(2, "0");
- return `speeder-backup_${year}-${month}-${day}_${hours}.${minutes}.${seconds}.json`;
+ return importExportUtils.generateBackupFilename(new Date());
}
function exportSettings() {
chrome.storage.sync.get(null, function (storage) {
chrome.storage.local.get(null, function (localStorage) {
- const backup = {
- version: "1.1",
- exportDate: new Date().toISOString(),
- settings: storage,
- localSettings: localStorage || {}
- };
+ const backup = importExportUtils.buildBackupPayload(
+ storage,
+ localStorage,
+ new Date()
+ );
const dataStr = JSON.stringify(backup, null, 2);
const blob = new Blob([dataStr], { type: "application/json" });
@@ -50,25 +45,50 @@ function importSettings() {
const reader = new FileReader();
reader.onload = function (e) {
try {
- const backup = JSON.parse(e.target.result);
- let settingsToImport = null;
+ const parsedBackup = importExportUtils.parseImportText(e.target.result);
- // Detect backup format: check for 'settings' wrapper or raw storage keys
- if (backup.settings && typeof backup.settings === "object") {
- settingsToImport = backup.settings;
- } else if (typeof backup === "object" && (backup.keyBindings || backup.rememberSpeed !== undefined)) {
- settingsToImport = backup; // Raw storage object
- }
-
- if (!settingsToImport) {
+ if (!parsedBackup) {
showStatus("Error: Invalid backup file format", true);
return;
}
- var localToImport =
- backup.localSettings && typeof backup.localSettings === "object"
- ? backup.localSettings
- : null;
+ var settingsToImport = parsedBackup.settings;
+ var localToImport = parsedBackup.localSettings;
+
+ function importLocalSettings(callback) {
+ if (parsedBackup.isWrappedBackup !== true) {
+ callback();
+ return;
+ }
+
+ chrome.storage.local.clear(function () {
+ if (chrome.runtime.lastError) {
+ showStatus(
+ "Error: Failed to clear local extension data - " +
+ chrome.runtime.lastError.message,
+ true
+ );
+ return;
+ }
+
+ if (localToImport && Object.keys(localToImport).length > 0) {
+ chrome.storage.local.set(localToImport, function () {
+ if (chrome.runtime.lastError) {
+ showStatus(
+ "Error: Failed to save local extension data - " +
+ chrome.runtime.lastError.message,
+ true
+ );
+ return;
+ }
+ callback();
+ });
+ return;
+ }
+
+ callback();
+ });
+ }
function afterLocalImport() {
chrome.storage.sync.clear(function () {
@@ -93,21 +113,7 @@ function importSettings() {
});
}
- if (localToImport && Object.keys(localToImport).length > 0) {
- chrome.storage.local.set(localToImport, function () {
- if (chrome.runtime.lastError) {
- showStatus(
- "Error: Failed to save local extension data - " +
- chrome.runtime.lastError.message,
- true
- );
- return;
- }
- afterLocalImport();
- });
- } else {
- afterLocalImport();
- }
+ importLocalSettings(afterLocalImport);
} catch (err) {
showStatus("Error: Failed to parse backup file - " + err.message, true);
}
diff --git a/inject.js b/inject.js
index 8d90682..7f7318c 100644
--- a/inject.js
+++ b/inject.js
@@ -1,5 +1,10 @@
var isUserSeek = false; // Track if seek was user-initiated
var lastToggleSpeed = {}; // Store last toggle speeds per video
+var speederShared =
+ typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
+var controllerUtils = speederShared.controllerUtils || {};
+var keyBindingUtils = speederShared.keyBindings || {};
+var siteRuleUtils = speederShared.siteRules || {};
function getPrimaryVideoElement() {
if (!tc.mediaElements || tc.mediaElements.length === 0) return null;
@@ -59,17 +64,20 @@ var requestIdle =
: function(callback, options) {
return setTimeout(callback, (options && options.timeout) || 1);
};
-var controllerLocations = [
- "top-left",
- "top-center",
- "top-right",
- "middle-right",
- "bottom-right",
- "bottom-center",
- "bottom-left",
- "middle-left"
-];
-var defaultControllerLocation = controllerLocations[0];
+var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
+ ? controllerUtils.controllerLocations.slice()
+ : [
+ "top-left",
+ "top-center",
+ "top-right",
+ "middle-right",
+ "bottom-right",
+ "bottom-center",
+ "bottom-left",
+ "middle-left"
+ ];
+var defaultControllerLocation =
+ controllerUtils.defaultControllerLocation || controllerLocations[0];
var controllerLocationStyles = {
"top-left": {
top: "10px",
@@ -196,28 +204,20 @@ function ensureDefaultKeyBinding(action, code, value) {
}
function getLegacyKeyCode(binding) {
- if (!binding) return null;
- if (Number.isInteger(binding.keyCode)) return binding.keyCode;
- if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
- return binding.key;
- }
- return null;
+ return keyBindingUtils.getLegacyKeyCode(binding);
}
function normalizeControllerLocation(location) {
- if (controllerLocations.includes(location)) return location;
- return defaultControllerLocation;
+ return controllerUtils.normalizeControllerLocation(
+ location,
+ defaultControllerLocation
+ );
}
var CONTROLLER_MARGIN_MAX_PX = 200;
function normalizeControllerMarginPx(value, fallback) {
- var n = Number(value);
- if (!Number.isFinite(n)) return fallback;
- return Math.min(
- CONTROLLER_MARGIN_MAX_PX,
- Math.max(0, Math.round(n))
- );
+ return controllerUtils.clampControllerMarginPx(value, fallback);
}
function applyControllerMargins(controller) {
@@ -256,9 +256,7 @@ function applyControllerMargins(controller) {
}
function getNextControllerLocation(location) {
- var normalizedLocation = normalizeControllerLocation(location);
- var currentIndex = controllerLocations.indexOf(normalizedLocation);
- return controllerLocations[(currentIndex + 1) % controllerLocations.length];
+ return controllerUtils.getNextControllerLocation(location);
}
function getControllerElement(videoOrController) {
@@ -478,98 +476,19 @@ function cycleControllerLocation(video) {
}
function normalizeBindingKey(key) {
- if (typeof key !== "string" || key.length === 0) return null;
- if (key === "Spacebar") return " ";
- if (key === "Esc") return "Escape";
- if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
- return key;
+ return keyBindingUtils.normalizeBindingKey(key);
}
function legacyBindingKeyToCode(key) {
- var normalizedKey = normalizeBindingKey(key);
- if (!normalizedKey) return null;
- if (/^[A-Z]$/.test(normalizedKey)) return "Key" + normalizedKey;
- if (/^[0-9]$/.test(normalizedKey)) return "Digit" + normalizedKey;
- if (/^F([1-9]|1[0-2])$/.test(normalizedKey)) return normalizedKey;
-
- var keyMap = {
- " ": "Space",
- ArrowLeft: "ArrowLeft",
- ArrowUp: "ArrowUp",
- ArrowRight: "ArrowRight",
- ArrowDown: "ArrowDown",
- ";": "Semicolon",
- "<": "Comma",
- "-": "Minus",
- "+": "Equal",
- ">": "Period",
- "/": "Slash",
- "~": "Backquote",
- "[": "BracketLeft",
- "\\": "Backslash",
- "]": "BracketRight",
- "'": "Quote"
- };
-
- return keyMap[normalizedKey] || null;
+ return keyBindingUtils.legacyBindingKeyToCode(key);
}
function legacyKeyCodeToCode(keyCode) {
- if (!Number.isInteger(keyCode)) return null;
- if (keyCode >= 48 && keyCode <= 57) return "Digit" + String.fromCharCode(keyCode);
- if (keyCode >= 65 && keyCode <= 90) return "Key" + String.fromCharCode(keyCode);
- if (keyCode >= 96 && keyCode <= 105) return "Numpad" + (keyCode - 96);
- if (keyCode >= 112 && keyCode <= 123) return "F" + (keyCode - 111);
-
- var keyCodeMap = {
- 32: "Space",
- 37: "ArrowLeft",
- 38: "ArrowUp",
- 39: "ArrowRight",
- 40: "ArrowDown",
- 106: "NumpadMultiply",
- 107: "NumpadAdd",
- 109: "NumpadSubtract",
- 110: "NumpadDecimal",
- 111: "NumpadDivide",
- 186: "Semicolon",
- 188: "Comma",
- 189: "Minus",
- 187: "Equal",
- 190: "Period",
- 191: "Slash",
- 192: "Backquote",
- 219: "BracketLeft",
- 220: "Backslash",
- 221: "BracketRight",
- 222: "Quote",
- 59: "Semicolon",
- 61: "Equal",
- 173: "Minus"
- };
-
- return keyCodeMap[keyCode] || null;
+ return keyBindingUtils.legacyKeyCodeToCode(keyCode);
}
function inferBindingCode(binding, fallbackCode) {
- if (binding && typeof binding.code === "string" && binding.code.length > 0) {
- return binding.code;
- }
-
- if (binding && typeof binding.key === "string") {
- var codeFromKey = legacyBindingKeyToCode(binding.key);
- if (codeFromKey) return codeFromKey;
- }
-
- var legacyKeyCode = getLegacyKeyCode(binding);
- if (Number.isInteger(legacyKeyCode)) {
- var codeFromKeyCode = legacyKeyCodeToCode(legacyKeyCode);
- if (codeFromKeyCode) return codeFromKeyCode;
- }
-
- return typeof fallbackCode === "string" && fallbackCode.length > 0
- ? fallbackCode
- : null;
+ return keyBindingUtils.inferBindingCode(binding, fallbackCode);
}
function normalizeStoredBinding(binding, fallbackCode) {
@@ -2074,11 +1993,6 @@ function defineVideoController() {
};
}
-function escapeStringRegExp(str) {
- const m = /[|\\{}()[\]^$+*?.]/g;
- return str.replace(m, "\\$&");
-}
-
function applySiteRuleOverrides() {
resetSettingsFromSiteRuleBase();
@@ -2087,34 +2001,7 @@ function applySiteRuleOverrides() {
}
var currentUrl = location.href;
- var matchedRule = null;
-
- for (var i = 0; i < tc.settings.siteRules.length; i++) {
- var rule = tc.settings.siteRules[i];
- var pattern = rule.pattern;
- if (!pattern || pattern.length === 0) continue;
-
- var regex;
- if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
- try {
- var lastSlash = pattern.lastIndexOf("/");
- regex = new RegExp(
- pattern.substring(1, lastSlash),
- pattern.substring(lastSlash + 1)
- );
- } catch (e) {
- log(`Invalid site rule regex: ${pattern}. ${e.message}`, 2);
- continue;
- }
- } else {
- regex = new RegExp(escapeStringRegExp(pattern));
- }
-
- if (regex && regex.test(currentUrl)) {
- matchedRule = rule;
- break;
- }
- }
+ var matchedRule = siteRuleUtils.matchSiteRule(currentUrl, tc.settings.siteRules);
if (!matchedRule) return false;
@@ -2122,13 +2009,9 @@ function applySiteRuleOverrides() {
log(`Matched site rule: ${matchedRule.pattern}`, 4);
// Check if extension should be enabled/disabled on this site
- if (matchedRule.enabled === false) {
+ if (siteRuleUtils.isSiteRuleDisabled(matchedRule)) {
log(`Extension disabled for site: ${currentUrl}`, 4);
return true;
- } else if (matchedRule.disableExtension === true) {
- // Handle old format
- log(`Extension disabled (legacy) for site: ${currentUrl}`, 4);
- return true;
}
// Override general settings with site-specific overrides
diff --git a/manifest.json b/manifest.json
index 3d2b692..1c859c7 100644
--- a/manifest.json
+++ b/manifest.json
@@ -59,6 +59,9 @@
"inject.css"
],
"js": [
+ "shared/controller-utils.js",
+ "shared/key-bindings.js",
+ "shared/site-rules.js",
"ui-icons.js",
"inject.js"
]
diff --git a/options.html b/options.html
index 3d5be8d..0bd6c9e 100644
--- a/options.html
+++ b/options.html
@@ -5,9 +5,13 @@
Speeder Settings
+
+
+
+
diff --git a/options.js b/options.js
index 7851653..f26c5b9 100644
--- a/options.js
+++ b/options.js
@@ -1,4 +1,9 @@
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
+var speederShared =
+ typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
+var controllerUtils = speederShared.controllerUtils || {};
+var keyBindingUtils = speederShared.keyBindings || {};
+var popupControlUtils = speederShared.popupControls || {};
var keyBindings = [];
@@ -47,16 +52,18 @@ var modifierKeys = new Set([
"Shift"
]);
-var controllerLocations = [
- "top-left",
- "top-center",
- "top-right",
- "middle-right",
- "bottom-right",
- "bottom-center",
- "bottom-left",
- "middle-left"
-];
+var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
+ ? controllerUtils.controllerLocations.slice()
+ : [
+ "top-left",
+ "top-center",
+ "top-right",
+ "middle-right",
+ "bottom-right",
+ "bottom-center",
+ "bottom-left",
+ "middle-left"
+ ];
var controllerButtonDefs = {
rewind: { icon: "\u00AB", name: "Rewind" },
@@ -82,15 +89,11 @@ var lucideSubtitleNudgeActionLabels = {
};
function sanitizePopupButtonOrder(buttonIds) {
- if (!Array.isArray(buttonIds)) return [];
- var seen = new Set();
- return buttonIds.filter(function (id) {
- if (!controllerButtonDefs[id] || popupExcludedButtonIds.has(id) || seen.has(id)) {
- return false;
- }
- seen.add(id);
- return true;
- });
+ return popupControlUtils.sanitizeButtonOrder(
+ buttonIds,
+ controllerButtonDefs,
+ popupExcludedButtonIds
+ );
}
/** Cached custom Lucide SVGs (mirrors chrome.storage.local customButtonIcons). */
@@ -294,14 +297,14 @@ function ensureDefaultBinding(storage, action, code, value) {
}
function normalizeControllerLocation(location) {
- if (controllerLocations.includes(location)) return location;
- return tcDefaults.controllerLocation;
+ return controllerUtils.normalizeControllerLocation(
+ location,
+ tcDefaults.controllerLocation
+ );
}
function clampMarginPxInput(el, fallback) {
- var n = parseInt(el && el.value, 10);
- if (!Number.isFinite(n)) return fallback;
- return Math.min(200, Math.max(0, n));
+ return controllerUtils.clampControllerMarginPx(el && el.value, fallback);
}
function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
@@ -321,107 +324,23 @@ function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
}
function normalizeBindingKey(key) {
- if (typeof key !== "string" || key.length === 0) return null;
- if (key === "Spacebar") return " ";
- if (key === "Esc") return "Escape";
- if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
- return key;
+ return keyBindingUtils.normalizeBindingKey(key);
}
function getLegacyKeyCode(binding) {
- if (!binding) return null;
- if (Number.isInteger(binding.keyCode)) return binding.keyCode;
- if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
- return binding.key;
- }
- return null;
+ return keyBindingUtils.getLegacyKeyCode(binding);
}
function legacyBindingKeyToCode(key) {
- var normalizedKey = normalizeBindingKey(key);
- if (!normalizedKey) return null;
- if (/^[A-Z]$/.test(normalizedKey)) return "Key" + normalizedKey;
- if (/^[0-9]$/.test(normalizedKey)) return "Digit" + normalizedKey;
- if (/^F([1-9]|1[0-2])$/.test(normalizedKey)) return normalizedKey;
-
- var keyMap = {
- " ": "Space",
- ArrowLeft: "ArrowLeft",
- ArrowUp: "ArrowUp",
- ArrowRight: "ArrowRight",
- ArrowDown: "ArrowDown",
- ";": "Semicolon",
- "<": "Comma",
- "-": "Minus",
- "+": "Equal",
- ">": "Period",
- "/": "Slash",
- "~": "Backquote",
- "[": "BracketLeft",
- "\\": "Backslash",
- "]": "BracketRight",
- "'": "Quote"
- };
-
- return keyMap[normalizedKey] || null;
+ return keyBindingUtils.legacyBindingKeyToCode(key);
}
function legacyKeyCodeToCode(keyCode) {
- if (!Number.isInteger(keyCode)) return null;
- if (keyCode >= 48 && keyCode <= 57) return "Digit" + String.fromCharCode(keyCode);
- if (keyCode >= 65 && keyCode <= 90) return "Key" + String.fromCharCode(keyCode);
- if (keyCode >= 96 && keyCode <= 105) return "Numpad" + (keyCode - 96);
- if (keyCode >= 112 && keyCode <= 123) return "F" + (keyCode - 111);
-
- var keyCodeMap = {
- 32: "Space",
- 37: "ArrowLeft",
- 38: "ArrowUp",
- 39: "ArrowRight",
- 40: "ArrowDown",
- 106: "NumpadMultiply",
- 107: "NumpadAdd",
- 109: "NumpadSubtract",
- 110: "NumpadDecimal",
- 111: "NumpadDivide",
- 186: "Semicolon",
- 188: "Comma",
- 189: "Minus",
- 187: "Equal",
- 190: "Period",
- 191: "Slash",
- 192: "Backquote",
- 219: "BracketLeft",
- 220: "Backslash",
- 221: "BracketRight",
- 222: "Quote",
- 59: "Semicolon",
- 61: "Equal",
- 173: "Minus"
- };
-
- return keyCodeMap[keyCode] || null;
+ return keyBindingUtils.legacyKeyCodeToCode(keyCode);
}
function inferBindingCode(binding, fallbackCode) {
- if (binding && typeof binding.code === "string" && binding.code.length > 0) {
- return binding.code;
- }
-
- if (binding && typeof binding.key === "string") {
- var codeFromKey = legacyBindingKeyToCode(binding.key);
- if (codeFromKey) return codeFromKey;
- }
-
- var legacyKeyCode = getLegacyKeyCode(binding);
- if (Number.isInteger(legacyKeyCode)) {
- var codeFromKeyCode = legacyKeyCodeToCode(legacyKeyCode);
- if (codeFromKeyCode) return codeFromKeyCode;
- }
-
- return typeof fallbackCode === "string" && fallbackCode.length > 0
- ? fallbackCode
- : null;
+ return keyBindingUtils.inferBindingCode(binding, fallbackCode);
}
function createDisabledBinding() {
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..320da4a
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2128 @@
+{
+ "name": "speeder",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "speeder",
+ "devDependencies": {
+ "jsdom": "^26.1.0",
+ "vitest": "^3.2.4"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "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": [
+ {
+ "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-calc": {
+ "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": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "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": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "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": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "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": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/agent-base": {
+ "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": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "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"
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
+ "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "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/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.4",
+ "@vitest/mocker": "3.2.4",
+ "@vitest/pretty-format": "^3.2.4",
+ "@vitest/runner": "3.2.4",
+ "@vitest/snapshot": "3.2.4",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.4",
+ "@vitest/ui": "3.2.4",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d7d1ddf
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "speeder",
+ "private": true,
+ "scripts": {
+ "test": "vitest run",
+ "test:watch": "vitest"
+ },
+ "devDependencies": {
+ "jsdom": "^26.1.0",
+ "vitest": "^3.2.4"
+ }
+}
diff --git a/popup.html b/popup.html
index 35371f3..cae9e41 100644
--- a/popup.html
+++ b/popup.html
@@ -4,6 +4,8 @@
Speeder
+
+
diff --git a/popup.js b/popup.js
index 3154478..1eabdbd 100644
--- a/popup.js
+++ b/popup.js
@@ -1,5 +1,8 @@
document.addEventListener("DOMContentLoaded", function () {
- var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
+ var speederShared =
+ typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
+ var siteRuleUtils = speederShared.siteRules || {};
+ var popupControlUtils = speederShared.popupControls || {};
/* `label` is only used if ui-icons.js has no path for this action (fallback). */
var controllerButtonDefs = {
@@ -30,73 +33,20 @@ document.addEventListener("DOMContentLoaded", function () {
};
var renderToken = 0;
- function escapeStringRegExp(str) {
- const m = /[|\\{}()[\]^$+*?.]/g;
- return str.replace(m, "\\$&");
- }
-
function matchSiteRule(url, siteRules) {
- if (!url || !Array.isArray(siteRules)) return null;
- for (var i = 0; i < siteRules.length; i++) {
- var rule = siteRules[i];
- if (!rule || !rule.pattern) continue;
- var pattern = rule.pattern.replace(regStrip, "");
- if (pattern.length === 0) continue;
- var re;
- if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
- try {
- var ls = pattern.lastIndexOf("/");
- re = new RegExp(pattern.substring(1, ls), pattern.substring(ls + 1));
- } catch (e) {
- continue;
- }
- } else {
- re = new RegExp(escapeStringRegExp(pattern));
- }
- if (re && re.test(url)) return rule;
- }
- return null;
+ return siteRuleUtils.matchSiteRule(url, siteRules);
}
function isSiteRuleDisabled(rule) {
- return Boolean(
- rule &&
- (rule.enabled === false || rule.disableExtension === true)
- );
+ return siteRuleUtils.isSiteRuleDisabled(rule);
}
function resolvePopupButtons(storage, siteRule) {
- function sanitize(buttons) {
- if (!Array.isArray(buttons)) return [];
- var seen = new Set();
- return buttons.filter(function (id) {
- if (!controllerButtonDefs[id] || popupExcludedButtonIds.has(id) || seen.has(id)) {
- return false;
- }
- seen.add(id);
- return true;
- });
- }
-
- if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
- return sanitize(siteRule.popupControllerButtons);
- }
-
- if (storage.popupMatchHoverControls) {
- if (siteRule && Array.isArray(siteRule.controllerButtons)) {
- return sanitize(siteRule.controllerButtons);
- }
-
- if (Array.isArray(storage.controllerButtons)) {
- return sanitize(storage.controllerButtons);
- }
- }
-
- if (Array.isArray(storage.popupControllerButtons)) {
- return sanitize(storage.popupControllerButtons);
- }
-
- return sanitize(defaultButtons);
+ return popupControlUtils.resolvePopupButtons(storage, siteRule, {
+ controllerButtonDefs: controllerButtonDefs,
+ defaultButtons: defaultButtons,
+ excludedIds: popupExcludedButtonIds
+ });
}
function setControlBarVisible(visible) {
@@ -165,23 +115,7 @@ document.addEventListener("DOMContentLoaded", function () {
}
function pickBestFrameSpeedResult(results) {
- if (!results || !results.length) return null;
- var i;
- var r;
- var fallback = null;
- for (i = 0; i < results.length; i++) {
- r = results[i];
- if (!r || typeof r.speed !== "number") {
- continue;
- }
- if (r.preferred) {
- return { speed: r.speed };
- }
- if (!fallback) {
- fallback = { speed: r.speed };
- }
- }
- return fallback;
+ return popupControlUtils.pickBestFrameSpeedResult(results);
}
function querySpeed() {
diff --git a/shared/controller-utils.js b/shared/controller-utils.js
new file mode 100644
index 0000000..6988b70
--- /dev/null
+++ b/shared/controller-utils.js
@@ -0,0 +1,55 @@
+(function(root, factory) {
+ var exports = factory();
+
+ if (typeof module === "object" && module.exports) {
+ module.exports = exports;
+ }
+
+ root.SpeederShared = root.SpeederShared || {};
+ root.SpeederShared.controllerUtils = exports;
+})(typeof globalThis !== "undefined" ? globalThis : this, function() {
+ var CONTROLLER_MARGIN_MAX_PX = 200;
+ var controllerLocations = [
+ "top-left",
+ "top-center",
+ "top-right",
+ "middle-right",
+ "bottom-right",
+ "bottom-center",
+ "bottom-left",
+ "middle-left"
+ ];
+ var defaultControllerLocation = controllerLocations[0];
+
+ function normalizeControllerLocation(location, fallback) {
+ if (controllerLocations.includes(location)) return location;
+ return typeof fallback === "string"
+ ? fallback
+ : defaultControllerLocation;
+ }
+
+ function clampControllerMarginPx(value, fallback) {
+ var numericValue = Number(value);
+ if (!Number.isFinite(numericValue)) return fallback;
+
+ return Math.min(
+ CONTROLLER_MARGIN_MAX_PX,
+ Math.max(0, Math.round(numericValue))
+ );
+ }
+
+ function getNextControllerLocation(location) {
+ var normalizedLocation = normalizeControllerLocation(location);
+ var currentIndex = controllerLocations.indexOf(normalizedLocation);
+ return controllerLocations[(currentIndex + 1) % controllerLocations.length];
+ }
+
+ return {
+ CONTROLLER_MARGIN_MAX_PX: CONTROLLER_MARGIN_MAX_PX,
+ clampControllerMarginPx: clampControllerMarginPx,
+ controllerLocations: controllerLocations.slice(),
+ defaultControllerLocation: defaultControllerLocation,
+ getNextControllerLocation: getNextControllerLocation,
+ normalizeControllerLocation: normalizeControllerLocation
+ };
+});
diff --git a/shared/import-export.js b/shared/import-export.js
new file mode 100644
index 0000000..507d5f0
--- /dev/null
+++ b/shared/import-export.js
@@ -0,0 +1,85 @@
+(function(root, factory) {
+ var exports = factory();
+
+ if (typeof module === "object" && module.exports) {
+ module.exports = exports;
+ }
+
+ root.SpeederShared = root.SpeederShared || {};
+ root.SpeederShared.importExport = exports;
+})(typeof globalThis !== "undefined" ? globalThis : this, function() {
+ function generateBackupFilename(now) {
+ var date = now instanceof Date ? now : new Date(now || Date.now());
+ var year = date.getFullYear();
+ var month = String(date.getMonth() + 1).padStart(2, "0");
+ var day = String(date.getDate()).padStart(2, "0");
+ var hours = String(date.getHours()).padStart(2, "0");
+ var minutes = String(date.getMinutes()).padStart(2, "0");
+ var seconds = String(date.getSeconds()).padStart(2, "0");
+
+ return (
+ "speeder-backup_" +
+ year +
+ "-" +
+ month +
+ "-" +
+ day +
+ "_" +
+ hours +
+ "." +
+ minutes +
+ "." +
+ seconds +
+ ".json"
+ );
+ }
+
+ function buildBackupPayload(settings, localSettings, now) {
+ return {
+ version: "1.1",
+ exportDate: new Date(now || Date.now()).toISOString(),
+ settings: settings,
+ localSettings: localSettings || {}
+ };
+ }
+
+ function extractImportSettings(backup) {
+ var settingsToImport = null;
+ var isWrappedBackup = false;
+
+ if (backup && backup.settings && typeof backup.settings === "object") {
+ settingsToImport = backup.settings;
+ isWrappedBackup = true;
+ } else if (
+ backup &&
+ typeof backup === "object" &&
+ (backup.keyBindings || backup.rememberSpeed !== undefined)
+ ) {
+ settingsToImport = backup;
+ }
+
+ if (!settingsToImport) return null;
+
+ return {
+ isWrappedBackup: isWrappedBackup,
+ settings: settingsToImport,
+ localSettings:
+ backup &&
+ backup.localSettings &&
+ typeof backup.localSettings === "object"
+ ? backup.localSettings
+ : null
+ };
+ }
+
+ function parseImportText(text) {
+ return extractImportSettings(JSON.parse(text));
+ }
+
+ return {
+ buildBackupPayload: buildBackupPayload,
+ extractImportSettings: extractImportSettings,
+ generateBackupFilename: generateBackupFilename,
+ parseImportText: parseImportText
+ };
+});
diff --git a/shared/key-bindings.js b/shared/key-bindings.js
new file mode 100644
index 0000000..ec4faf3
--- /dev/null
+++ b/shared/key-bindings.js
@@ -0,0 +1,122 @@
+(function(root, factory) {
+ var exports = factory();
+
+ if (typeof module === "object" && module.exports) {
+ module.exports = exports;
+ }
+
+ root.SpeederShared = root.SpeederShared || {};
+ root.SpeederShared.keyBindings = exports;
+})(typeof globalThis !== "undefined" ? globalThis : this, function() {
+ function normalizeBindingKey(key) {
+ if (typeof key !== "string" || key.length === 0) return null;
+ if (key === "Spacebar") return " ";
+ if (key === "Esc") return "Escape";
+ if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
+ return key;
+ }
+
+ function getLegacyKeyCode(binding) {
+ if (!binding) return null;
+ if (Number.isInteger(binding.keyCode)) return binding.keyCode;
+ if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
+ return binding.key;
+ }
+ return null;
+ }
+
+ function legacyBindingKeyToCode(key) {
+ var normalizedKey = normalizeBindingKey(key);
+ if (!normalizedKey) return null;
+ if (/^[A-Z]$/.test(normalizedKey)) return "Key" + normalizedKey;
+ if (/^[0-9]$/.test(normalizedKey)) return "Digit" + normalizedKey;
+ if (/^F([1-9]|1[0-2])$/.test(normalizedKey)) return normalizedKey;
+
+ var keyMap = {
+ " ": "Space",
+ ArrowLeft: "ArrowLeft",
+ ArrowUp: "ArrowUp",
+ ArrowRight: "ArrowRight",
+ ArrowDown: "ArrowDown",
+ ";": "Semicolon",
+ "<": "Comma",
+ "-": "Minus",
+ "+": "Equal",
+ ">": "Period",
+ "/": "Slash",
+ "~": "Backquote",
+ "[": "BracketLeft",
+ "\\": "Backslash",
+ "]": "BracketRight",
+ "'": "Quote"
+ };
+
+ return keyMap[normalizedKey] || null;
+ }
+
+ function legacyKeyCodeToCode(keyCode) {
+ if (!Number.isInteger(keyCode)) return null;
+ if (keyCode >= 48 && keyCode <= 57) return "Digit" + String.fromCharCode(keyCode);
+ if (keyCode >= 65 && keyCode <= 90) return "Key" + String.fromCharCode(keyCode);
+ if (keyCode >= 96 && keyCode <= 105) return "Numpad" + (keyCode - 96);
+ if (keyCode >= 112 && keyCode <= 123) return "F" + (keyCode - 111);
+
+ var keyCodeMap = {
+ 32: "Space",
+ 37: "ArrowLeft",
+ 38: "ArrowUp",
+ 39: "ArrowRight",
+ 40: "ArrowDown",
+ 106: "NumpadMultiply",
+ 107: "NumpadAdd",
+ 109: "NumpadSubtract",
+ 110: "NumpadDecimal",
+ 111: "NumpadDivide",
+ 186: "Semicolon",
+ 188: "Comma",
+ 189: "Minus",
+ 187: "Equal",
+ 190: "Period",
+ 191: "Slash",
+ 192: "Backquote",
+ 219: "BracketLeft",
+ 220: "Backslash",
+ 221: "BracketRight",
+ 222: "Quote",
+ 59: "Semicolon",
+ 61: "Equal",
+ 173: "Minus"
+ };
+
+ return keyCodeMap[keyCode] || null;
+ }
+
+ function inferBindingCode(binding, fallbackCode) {
+ if (binding && typeof binding.code === "string" && binding.code.length > 0) {
+ return binding.code;
+ }
+
+ if (binding && typeof binding.key === "string") {
+ var codeFromKey = legacyBindingKeyToCode(binding.key);
+ if (codeFromKey) return codeFromKey;
+ }
+
+ var legacyKeyCode = getLegacyKeyCode(binding);
+ if (Number.isInteger(legacyKeyCode)) {
+ var codeFromKeyCode = legacyKeyCodeToCode(legacyKeyCode);
+ if (codeFromKeyCode) return codeFromKeyCode;
+ }
+
+ return typeof fallbackCode === "string" && fallbackCode.length > 0
+ ? fallbackCode
+ : null;
+ }
+
+ return {
+ getLegacyKeyCode: getLegacyKeyCode,
+ inferBindingCode: inferBindingCode,
+ legacyBindingKeyToCode: legacyBindingKeyToCode,
+ legacyKeyCodeToCode: legacyKeyCodeToCode,
+ normalizeBindingKey: normalizeBindingKey
+ };
+});
diff --git a/shared/popup-controls.js b/shared/popup-controls.js
new file mode 100644
index 0000000..c85e4e5
--- /dev/null
+++ b/shared/popup-controls.js
@@ -0,0 +1,85 @@
+(function(root, factory) {
+ var exports = factory();
+
+ if (typeof module === "object" && module.exports) {
+ module.exports = exports;
+ }
+
+ root.SpeederShared = root.SpeederShared || {};
+ root.SpeederShared.popupControls = exports;
+})(typeof globalThis !== "undefined" ? globalThis : this, function() {
+ function normalizeExcludedIds(excludedIds) {
+ if (excludedIds instanceof Set) return excludedIds;
+ return new Set(Array.isArray(excludedIds) ? excludedIds : []);
+ }
+
+ function sanitizeButtonOrder(buttonIds, controllerButtonDefs, excludedIds) {
+ if (!Array.isArray(buttonIds)) return [];
+
+ var seen = new Set();
+ var excluded = normalizeExcludedIds(excludedIds);
+
+ return buttonIds.filter(function(id) {
+ if (!controllerButtonDefs[id] || excluded.has(id) || seen.has(id)) {
+ return false;
+ }
+
+ seen.add(id);
+ return true;
+ });
+ }
+
+ function resolvePopupButtons(storage, siteRule, options) {
+ var settings = storage || {};
+ var config = options || {};
+ var controllerButtonDefs = config.controllerButtonDefs || {};
+ var defaultButtons = Array.isArray(config.defaultButtons)
+ ? config.defaultButtons
+ : [];
+ var excludedIds = config.excludedIds;
+
+ function sanitize(buttonIds) {
+ return sanitizeButtonOrder(buttonIds, controllerButtonDefs, excludedIds);
+ }
+
+ if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
+ return sanitize(siteRule.popupControllerButtons);
+ }
+
+ if (settings.popupMatchHoverControls) {
+ if (siteRule && Array.isArray(siteRule.controllerButtons)) {
+ return sanitize(siteRule.controllerButtons);
+ }
+
+ if (Array.isArray(settings.controllerButtons)) {
+ return sanitize(settings.controllerButtons);
+ }
+ }
+
+ if (Array.isArray(settings.popupControllerButtons)) {
+ return sanitize(settings.popupControllerButtons);
+ }
+
+ return sanitize(defaultButtons);
+ }
+
+ function pickBestFrameSpeedResult(results) {
+ if (!results || !results.length) return null;
+
+ var fallback = null;
+ for (var i = 0; i < results.length; i++) {
+ var result = results[i];
+ if (!result || typeof result.speed !== "number") continue;
+ if (result.preferred) return { speed: result.speed };
+ if (!fallback) fallback = { speed: result.speed };
+ }
+
+ return fallback;
+ }
+
+ return {
+ pickBestFrameSpeedResult: pickBestFrameSpeedResult,
+ resolvePopupButtons: resolvePopupButtons,
+ sanitizeButtonOrder: sanitizeButtonOrder
+ };
+});
diff --git a/shared/site-rules.js b/shared/site-rules.js
new file mode 100644
index 0000000..1d327f1
--- /dev/null
+++ b/shared/site-rules.js
@@ -0,0 +1,69 @@
+(function(root, factory) {
+ var exports = factory();
+
+ if (typeof module === "object" && module.exports) {
+ module.exports = exports;
+ }
+
+ root.SpeederShared = root.SpeederShared || {};
+ root.SpeederShared.siteRules = exports;
+})(typeof globalThis !== "undefined" ? globalThis : this, function() {
+ var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
+
+ function escapeStringRegExp(str) {
+ return String(str).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
+ }
+
+ function compileSiteRulePattern(pattern) {
+ if (typeof pattern !== "string") return null;
+
+ var normalizedPattern = pattern.replace(regStrip, "");
+ if (normalizedPattern.length === 0) return null;
+
+ if (
+ normalizedPattern.startsWith("/") &&
+ normalizedPattern.lastIndexOf("/") > 0
+ ) {
+ var lastSlash = normalizedPattern.lastIndexOf("/");
+ return new RegExp(
+ normalizedPattern.substring(1, lastSlash),
+ normalizedPattern.substring(lastSlash + 1)
+ );
+ }
+
+ return new RegExp(escapeStringRegExp(normalizedPattern));
+ }
+
+ function matchSiteRule(url, siteRules) {
+ if (!url || !Array.isArray(siteRules)) return null;
+
+ for (var i = 0; i < siteRules.length; i++) {
+ var rule = siteRules[i];
+ if (!rule || !rule.pattern) continue;
+
+ try {
+ var re = compileSiteRulePattern(rule.pattern);
+ if (re && re.test(url)) {
+ return rule;
+ }
+ } catch (e) {
+ }
+ }
+
+ return null;
+ }
+
+ function isSiteRuleDisabled(rule) {
+ return Boolean(
+ rule &&
+ (rule.enabled === false || rule.disableExtension === true)
+ );
+ }
+
+ return {
+ compileSiteRulePattern: compileSiteRulePattern,
+ escapeStringRegExp: escapeStringRegExp,
+ isSiteRuleDisabled: isSiteRuleDisabled,
+ matchSiteRule: matchSiteRule
+ };
+});
diff --git a/tests/helpers/browser.js b/tests/helpers/browser.js
new file mode 100644
index 0000000..bf864bf
--- /dev/null
+++ b/tests/helpers/browser.js
@@ -0,0 +1,164 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { vi } from "vitest";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const repoRoot = path.resolve(__dirname, "..", "..");
+
+function readRepoFile(relPath) {
+ return fs.readFileSync(path.join(repoRoot, relPath), "utf8");
+}
+
+export function loadHtml(relPath) {
+ document.open();
+ document.write(readRepoFile(relPath));
+ document.close();
+}
+
+export function loadScript(relPath) {
+ window.eval(
+ "var chrome = window.chrome || globalThis.chrome;\n" +
+ readRepoFile(relPath) +
+ "\n//# sourceURL=" +
+ relPath
+ );
+}
+
+export async function flushAsyncWork() {
+ await Promise.resolve();
+ await Promise.resolve();
+}
+
+export function triggerDomContentLoaded() {
+ document.dispatchEvent(
+ new window.Event("DOMContentLoaded", {
+ bubbles: true,
+ cancelable: true
+ })
+ );
+}
+
+function createEvent() {
+ const listeners = [];
+ return {
+ addListener(listener) {
+ listeners.push(listener);
+ },
+ trigger(...args) {
+ listeners.forEach((listener) => listener(...args));
+ },
+ listeners
+ };
+}
+
+function createStorageArea(initialState = {}) {
+ const state = { ...initialState };
+
+ function resolveGet(keys) {
+ if (keys == null) return { ...state };
+ if (Array.isArray(keys)) {
+ return keys.reduce((acc, key) => {
+ if (Object.prototype.hasOwnProperty.call(state, key)) {
+ acc[key] = state[key];
+ }
+ return acc;
+ }, {});
+ }
+ if (typeof keys === "string") {
+ return Object.prototype.hasOwnProperty.call(state, keys)
+ ? { [keys]: state[keys] }
+ : {};
+ }
+ if (typeof keys === "object") {
+ const result = { ...keys };
+ Object.keys(state).forEach((key) => {
+ result[key] = state[key];
+ });
+ return result;
+ }
+ return {};
+ }
+
+ return {
+ __state: state,
+ get: vi.fn((keys, callback) => {
+ callback(resolveGet(keys));
+ }),
+ set: vi.fn((items, callback) => {
+ Object.assign(state, items);
+ if (callback) callback();
+ }),
+ remove: vi.fn((keys, callback) => {
+ const list = Array.isArray(keys) ? keys : [keys];
+ list.forEach((key) => {
+ delete state[key];
+ });
+ if (callback) callback();
+ }),
+ clear: vi.fn((callback) => {
+ Object.keys(state).forEach((key) => delete state[key]);
+ if (callback) callback();
+ })
+ };
+}
+
+export function createChromeMock(options = {}) {
+ const syncArea = createStorageArea(options.sync ?? {});
+ const localArea = createStorageArea(options.local ?? {});
+ const tabsOnActivated = createEvent();
+ const tabsOnUpdated = createEvent();
+ const storageOnChanged = createEvent();
+
+ const chrome = {
+ runtime: {
+ lastError: null,
+ getManifest: vi.fn(() => ({
+ version: options.manifestVersion || "9.9.9"
+ })),
+ getURL: vi.fn((url) => "moz-extension://speeder/" + url)
+ },
+ storage: {
+ sync: syncArea,
+ local: localArea,
+ onChanged: storageOnChanged
+ },
+ tabs: {
+ query: vi.fn((queryInfo, callback) => {
+ callback(
+ options.tabs ??
+ [
+ {
+ id: 1,
+ active: true,
+ url: "https://example.com/watch"
+ }
+ ]
+ );
+ }),
+ sendMessage: vi.fn((tabId, message, callback) => {
+ if (callback) {
+ callback(options.sendMessageResponse ?? { speed: 1.25 });
+ }
+ }),
+ executeScript: vi.fn((tabId, details, callback) => {
+ if (callback) {
+ callback(
+ options.executeScriptResponse ?? [
+ { speed: 1.25, preferred: true }
+ ]
+ );
+ }
+ }),
+ create: vi.fn(),
+ onActivated: tabsOnActivated,
+ onUpdated: tabsOnUpdated
+ },
+ browserAction: {
+ setIcon: vi.fn()
+ }
+ };
+
+ return chrome;
+}
diff --git a/tests/helpers/extension-test-utils.js b/tests/helpers/extension-test-utils.js
new file mode 100644
index 0000000..dba6668
--- /dev/null
+++ b/tests/helpers/extension-test-utils.js
@@ -0,0 +1,240 @@
+const fs = require("fs");
+const path = require("path");
+const { vi } = require("vitest");
+
+const ROOT = path.resolve(__dirname, "..", "..");
+
+function clone(value) {
+ if (value === undefined) return undefined;
+ return JSON.parse(JSON.stringify(value));
+}
+
+function workspacePath(relPath) {
+ return path.join(ROOT, relPath);
+}
+
+function readWorkspaceFile(relPath) {
+ return fs.readFileSync(workspacePath(relPath), "utf8");
+}
+
+function loadHtmlFile(relPath) {
+ document.open();
+ document.write(readWorkspaceFile(relPath));
+ document.close();
+}
+
+function loadHtmlString(html) {
+ document.open();
+ document.write(html);
+ document.close();
+}
+
+function evaluateScript(relPath) {
+ const source = readWorkspaceFile(relPath);
+ window.eval(
+ `${source}\n//# sourceURL=${workspacePath(relPath).replace(/\\/g, "/")}`
+ );
+}
+
+function fireDOMContentLoaded() {
+ document.dispatchEvent(
+ new window.Event("DOMContentLoaded", {
+ bubbles: true,
+ cancelable: true
+ })
+ );
+}
+
+async function flushAsyncWork(turns) {
+ const count = turns || 2;
+ for (let i = 0; i < count; i += 1) {
+ await Promise.resolve();
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+}
+
+function pickStorageValues(data, keys) {
+ if (keys == null) return clone(data || {});
+
+ if (typeof keys === "string") {
+ return { [keys]: clone(data ? data[keys] : undefined) };
+ }
+
+ if (Array.isArray(keys)) {
+ const result = {};
+ keys.forEach((key) => {
+ result[key] = clone(data ? data[key] : undefined);
+ });
+ return result;
+ }
+
+ if (typeof keys === "object") {
+ const result = clone(keys) || {};
+ Object.keys(keys).forEach((key) => {
+ if (data && Object.prototype.hasOwnProperty.call(data, key)) {
+ result[key] = clone(data[key]);
+ }
+ });
+ return result;
+ }
+
+ return {};
+}
+
+function createChromeEvent() {
+ const listeners = [];
+ return {
+ addListener(listener) {
+ listeners.push(listener);
+ },
+ removeListener(listener) {
+ const index = listeners.indexOf(listener);
+ if (index >= 0) {
+ listeners.splice(index, 1);
+ }
+ },
+ hasListener(listener) {
+ return listeners.includes(listener);
+ },
+ emit(...args) {
+ listeners.slice().forEach((listener) => listener(...args));
+ },
+ listeners
+ };
+}
+
+function createStorageArea(areaName, initialData, onChangedEvent) {
+ let data = clone(initialData) || {};
+
+ function emitChanges(changes) {
+ if (changes && Object.keys(changes).length > 0) {
+ onChangedEvent.emit(changes, areaName);
+ }
+ }
+
+ return {
+ get: vi.fn((keys, callback) => {
+ if (callback) callback(pickStorageValues(data, keys));
+ }),
+ set: vi.fn((items, callback) => {
+ const nextItems = items || {};
+ const changes = {};
+
+ Object.keys(nextItems).forEach((key) => {
+ const oldValue = clone(data[key]);
+ const newValue = clone(nextItems[key]);
+ data[key] = newValue;
+ changes[key] = { oldValue, newValue };
+ });
+
+ emitChanges(changes);
+ if (callback) callback();
+ }),
+ remove: vi.fn((keys, callback) => {
+ const list = Array.isArray(keys) ? keys : [keys];
+ const changes = {};
+
+ list.forEach((key) => {
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
+ changes[key] = {
+ oldValue: clone(data[key]),
+ newValue: undefined
+ };
+ delete data[key];
+ }
+ });
+
+ emitChanges(changes);
+ if (callback) callback();
+ }),
+ clear: vi.fn((callback) => {
+ const changes = {};
+ Object.keys(data).forEach((key) => {
+ changes[key] = {
+ oldValue: clone(data[key]),
+ newValue: undefined
+ };
+ });
+ data = {};
+ emitChanges(changes);
+ if (callback) callback();
+ }),
+ _dump() {
+ return clone(data);
+ }
+ };
+}
+
+function createChromeMock(options) {
+ const config = options || {};
+ const storageOnChanged = createChromeEvent();
+ const tabsOnActivated = createChromeEvent();
+ const tabsOnUpdated = createChromeEvent();
+ const runtimeOnMessage = createChromeEvent();
+
+ const chrome = {
+ runtime: {
+ lastError: null,
+ getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
+ getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
+ onMessage: runtimeOnMessage
+ },
+ browserAction: {
+ setIcon: vi.fn()
+ },
+ tabs: {
+ query: vi.fn((queryInfo, callback) => {
+ const tabs = clone(config.tabsQueryResult) || [
+ { id: 1, active: true, url: "https://example.com/" }
+ ];
+ if (callback) callback(tabs);
+ }),
+ sendMessage: vi.fn((tabId, message, callback) => {
+ if (callback) callback(null);
+ }),
+ executeScript: vi.fn((tabId, details, callback) => {
+ if (callback) callback([]);
+ }),
+ create: vi.fn(),
+ onActivated: tabsOnActivated,
+ onUpdated: tabsOnUpdated
+ },
+ storage: {
+ onChanged: storageOnChanged,
+ sync: null,
+ local: null
+ }
+ };
+
+ chrome.storage.sync = createStorageArea(
+ "sync",
+ config.syncData,
+ storageOnChanged
+ );
+ chrome.storage.local = createStorageArea(
+ "local",
+ config.localData,
+ storageOnChanged
+ );
+
+ return chrome;
+}
+
+function installCommonWindowMocks() {
+ window.open = vi.fn();
+ window.close = vi.fn();
+ window.requestAnimationFrame = vi.fn((callback) => setTimeout(callback, 0));
+ window.cancelAnimationFrame = vi.fn((id) => clearTimeout(id));
+}
+
+module.exports = {
+ createChromeMock,
+ evaluateScript,
+ fireDOMContentLoaded,
+ flushAsyncWork,
+ installCommonWindowMocks,
+ loadHtmlFile,
+ loadHtmlString,
+ readWorkspaceFile,
+ workspacePath
+};
diff --git a/tests/importExport.integration.test.js b/tests/importExport.integration.test.js
new file mode 100644
index 0000000..f934be5
--- /dev/null
+++ b/tests/importExport.integration.test.js
@@ -0,0 +1,224 @@
+import {
+ createChromeMock,
+ flushAsyncWork,
+ loadHtml,
+ loadScript
+} from "./helpers/browser.js";
+
+async function setupImportExport(overrides = {}) {
+ loadHtml("options.html");
+ globalThis.chrome = createChromeMock(overrides);
+ window.chrome = globalThis.chrome;
+ globalThis.restore_options = vi.fn();
+ loadScript("shared/import-export.js");
+ loadScript("importExport.js");
+ await flushAsyncWork();
+ return globalThis.chrome;
+}
+
+describe("import/export flows", () => {
+ it("exports sync and local settings as a JSON download", async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date(2026, 3, 4, 8, 9, 10));
+ const chrome = await setupImportExport({
+ sync: { rememberSpeed: true },
+ local: { customButtonIcons: { faster: { slug: "rocket" } } }
+ });
+ const OriginalBlob = globalThis.Blob;
+ globalThis.Blob = class TestBlob {
+ constructor(parts, options) {
+ this.parts = parts;
+ this.options = options;
+ }
+
+ async text() {
+ return this.parts.join("");
+ }
+ };
+
+ let capturedBlob = null;
+ let clickedDownload = null;
+ Object.defineProperty(URL, "createObjectURL", {
+ configurable: true,
+ value: vi.fn((blob) => {
+ capturedBlob = blob;
+ return "blob:test";
+ })
+ });
+ Object.defineProperty(URL, "revokeObjectURL", {
+ configurable: true,
+ value: vi.fn(() => {})
+ });
+ vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function () {
+ clickedDownload = this.download;
+ });
+
+ document.getElementById("exportSettings").click();
+
+ expect(clickedDownload).toBe("speeder-backup_2026-04-04_08.09.10.json");
+ expect(capturedBlob).not.toBeNull();
+ const blobText = await capturedBlob.text();
+ expect(JSON.parse(blobText)).toEqual({
+ version: "1.1",
+ exportDate: "2026-04-04T12:09:10.000Z",
+ settings: { rememberSpeed: true },
+ localSettings: { customButtonIcons: { faster: { slug: "rocket" } } }
+ });
+ expect(document.getElementById("status").textContent).toBe(
+ "Settings exported successfully"
+ );
+ expect(chrome.storage.sync.get).toHaveBeenCalled();
+ expect(chrome.storage.local.get).toHaveBeenCalled();
+ globalThis.Blob = OriginalBlob;
+ });
+
+ it("imports wrapped backup payloads and refreshes options", async () => {
+ vi.useFakeTimers();
+ const chrome = await setupImportExport();
+
+ const originalCreateElement = document.createElement.bind(document);
+ let createdInput = null;
+ vi.spyOn(document, "createElement").mockImplementation((tagName) => {
+ const el = originalCreateElement(tagName);
+ if (tagName === "input") {
+ createdInput = el;
+ el.click = vi.fn();
+ }
+ return el;
+ });
+
+ globalThis.FileReader = class MockFileReader {
+ readAsText(file) {
+ this.onload({
+ target: {
+ result: file.__text
+ }
+ });
+ }
+ };
+
+ globalThis.importSettings();
+ createdInput.onchange({
+ target: {
+ files: [
+ {
+ __text: JSON.stringify({
+ settings: { rememberSpeed: true },
+ localSettings: { customButtonIcons: { faster: { slug: "rocket" } } }
+ })
+ }
+ ]
+ }
+ });
+
+ expect(chrome.storage.local.set).toHaveBeenCalledWith(
+ { customButtonIcons: { faster: { slug: "rocket" } } },
+ expect.any(Function)
+ );
+ expect(chrome.storage.sync.clear).toHaveBeenCalled();
+ expect(chrome.storage.sync.set).toHaveBeenCalledWith(
+ { rememberSpeed: true },
+ expect.any(Function)
+ );
+ expect(document.getElementById("status").textContent).toBe(
+ "Settings imported successfully. Reloading..."
+ );
+
+ vi.advanceTimersByTime(500);
+ expect(globalThis.restore_options).toHaveBeenCalled();
+ });
+
+ it("clears stale local data when a wrapped backup has empty local settings", async () => {
+ vi.useFakeTimers();
+ const chrome = await setupImportExport({
+ local: {
+ customButtonIcons: { faster: { slug: "rocket" } },
+ lucideTagsCacheV1: { stale: true }
+ }
+ });
+
+ const originalCreateElement = document.createElement.bind(document);
+ let createdInput = null;
+ vi.spyOn(document, "createElement").mockImplementation((tagName) => {
+ const el = originalCreateElement(tagName);
+ if (tagName === "input") {
+ createdInput = el;
+ el.click = vi.fn();
+ }
+ return el;
+ });
+
+ globalThis.FileReader = class MockFileReader {
+ readAsText(file) {
+ this.onload({
+ target: {
+ result: file.__text
+ }
+ });
+ }
+ };
+
+ globalThis.importSettings();
+ createdInput.onchange({
+ target: {
+ files: [
+ {
+ __text: JSON.stringify({
+ settings: { rememberSpeed: true },
+ localSettings: {}
+ })
+ }
+ ]
+ }
+ });
+
+ expect(chrome.storage.local.clear).toHaveBeenCalled();
+ expect(chrome.storage.local.set).not.toHaveBeenCalled();
+ expect(chrome.storage.sync.set).toHaveBeenCalledWith(
+ { rememberSpeed: true },
+ expect.any(Function)
+ );
+ });
+
+ it("shows an error for invalid backup files", async () => {
+ vi.useFakeTimers();
+ const chrome = await setupImportExport();
+
+ const originalCreateElement = document.createElement.bind(document);
+ let createdInput = null;
+ vi.spyOn(document, "createElement").mockImplementation((tagName) => {
+ const el = originalCreateElement(tagName);
+ if (tagName === "input") {
+ createdInput = el;
+ el.click = vi.fn();
+ }
+ return el;
+ });
+
+ globalThis.FileReader = class MockFileReader {
+ readAsText(file) {
+ this.onload({
+ target: {
+ result: file.__text
+ }
+ });
+ }
+ };
+
+ globalThis.importSettings();
+ createdInput.onchange({
+ target: {
+ files: [
+ {
+ __text: JSON.stringify({ enabled: true })
+ }
+ ]
+ }
+ });
+
+ expect(document.getElementById("status").textContent).toBe(
+ "Error: Invalid backup file format"
+ );
+ expect(chrome.storage.sync.set).not.toHaveBeenCalled();
+ });
+});
diff --git a/tests/importExport.spec.js b/tests/importExport.spec.js
new file mode 100644
index 0000000..8694b31
--- /dev/null
+++ b/tests/importExport.spec.js
@@ -0,0 +1,189 @@
+const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
+const {
+ createChromeMock,
+ evaluateScript,
+ flushAsyncWork,
+ installCommonWindowMocks,
+ loadHtmlString
+} = require("./helpers/extension-test-utils");
+
+function bootImportExport(options) {
+ const config = options || {};
+
+ loadHtmlString(`
+
+
+
+ `);
+
+ installCommonWindowMocks();
+
+ const chrome = createChromeMock({
+ syncData: config.syncData,
+ localData: config.localData
+ });
+
+ global.chrome = chrome;
+ window.chrome = chrome;
+
+ const createObjectURL = vi.fn(() => "blob:test");
+ const revokeObjectURL = vi.fn();
+ vi.stubGlobal("URL", {
+ createObjectURL,
+ revokeObjectURL
+ });
+
+ evaluateScript("importExport.js");
+ return { chrome, createObjectURL, revokeObjectURL };
+}
+
+describe("importExport.js", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ delete global.chrome;
+ });
+
+ it("generates timestamped backup filenames", () => {
+ vi.setSystemTime(new Date("2026-04-04T13:14:15Z"));
+ bootImportExport();
+
+ expect(window.generateBackupFilename()).toBe(
+ "speeder-backup_2026-04-04_13.14.15.json"
+ );
+ });
+
+ it("exports sync and local settings into a downloadable backup", async () => {
+ const clickSpy = vi
+ .spyOn(window.HTMLAnchorElement.prototype, "click")
+ .mockImplementation(() => {});
+ const { createObjectURL, revokeObjectURL } = bootImportExport({
+ syncData: {
+ rememberSpeed: true,
+ keyBindings: [{ action: "faster", code: "KeyD", value: 0.1 }]
+ },
+ localData: {
+ customButtonIcons: {
+ faster: { slug: "rocket", svg: "" }
+ }
+ }
+ });
+
+ document.querySelector("#exportSettings").click();
+ await flushAsyncWork();
+
+ expect(createObjectURL).toHaveBeenCalledTimes(1);
+ const blob = createObjectURL.mock.calls[0][0];
+ const backup = JSON.parse(await blob.text());
+
+ expect(backup.settings.rememberSpeed).toBe(true);
+ expect(backup.localSettings.customButtonIcons.faster.slug).toBe("rocket");
+ expect(clickSpy).toHaveBeenCalledTimes(1);
+ expect(revokeObjectURL).toHaveBeenCalledWith("blob:test");
+ expect(document.querySelector("#status").textContent).toContain("exported");
+ });
+
+ it("imports wrapped backups, restores local data, and refreshes the options page", async () => {
+ const { chrome } = bootImportExport();
+ window.restore_options = vi.fn();
+
+ const realCreateElement = document.createElement.bind(document);
+ const fakeInput = realCreateElement("input");
+ Object.defineProperty(fakeInput, "files", {
+ configurable: true,
+ value: [
+ {
+ __contents: JSON.stringify({
+ settings: {
+ rememberSpeed: true,
+ enabled: false
+ },
+ localSettings: {
+ customButtonIcons: {
+ faster: { slug: "rocket", svg: "" }
+ }
+ }
+ })
+ }
+ ]
+ });
+ fakeInput.click = vi.fn(() => {
+ fakeInput.onchange({ target: fakeInput });
+ });
+
+ vi.spyOn(document, "createElement").mockImplementation((tagName) => {
+ if (String(tagName).toLowerCase() === "input") {
+ return fakeInput;
+ }
+ return realCreateElement(tagName);
+ });
+
+ class FakeFileReader {
+ readAsText(file) {
+ this.onload({ target: { result: file.__contents } });
+ }
+ }
+
+ vi.stubGlobal("FileReader", FakeFileReader);
+
+ document.querySelector("#importSettings").click();
+ await flushAsyncWork();
+
+ expect(chrome.storage.local.set).toHaveBeenCalledWith(
+ {
+ customButtonIcons: {
+ faster: { slug: "rocket", svg: "" }
+ }
+ },
+ expect.any(Function)
+ );
+ expect(chrome.storage.sync.clear).toHaveBeenCalled();
+ expect(chrome.storage.sync.set).toHaveBeenCalledWith(
+ { rememberSpeed: true, enabled: false },
+ expect.any(Function)
+ );
+
+ vi.advanceTimersByTime(500);
+ expect(window.restore_options).toHaveBeenCalled();
+ });
+
+ it("shows an error for malformed backups", async () => {
+ bootImportExport();
+
+ const realCreateElement = document.createElement.bind(document);
+ const fakeInput = realCreateElement("input");
+ Object.defineProperty(fakeInput, "files", {
+ configurable: true,
+ value: [{ __contents: "{bad json" }]
+ });
+ fakeInput.click = vi.fn(() => {
+ fakeInput.onchange({ target: fakeInput });
+ });
+
+ vi.spyOn(document, "createElement").mockImplementation((tagName) => {
+ if (String(tagName).toLowerCase() === "input") {
+ return fakeInput;
+ }
+ return realCreateElement(tagName);
+ });
+
+ class FakeFileReader {
+ readAsText(file) {
+ this.onload({ target: { result: file.__contents } });
+ }
+ }
+
+ vi.stubGlobal("FileReader", FakeFileReader);
+
+ document.querySelector("#importSettings").click();
+ await flushAsyncWork();
+
+ expect(document.querySelector("#status").textContent).toContain(
+ "Failed to parse backup file"
+ );
+ });
+});
diff --git a/tests/inject.spec.js b/tests/inject.spec.js
new file mode 100644
index 0000000..95006f2
--- /dev/null
+++ b/tests/inject.spec.js
@@ -0,0 +1,141 @@
+const { afterEach, describe, expect, it, vi } = require("vitest");
+const {
+ createChromeMock,
+ evaluateScript,
+ flushAsyncWork,
+ loadHtmlString
+} = require("./helpers/extension-test-utils");
+
+function bootInject(options) {
+ const config = options || {};
+
+ loadHtmlString("");
+
+ const chrome = createChromeMock({
+ syncData: config.syncData,
+ localData: config.localData
+ });
+
+ global.chrome = chrome;
+ window.chrome = chrome;
+ window.requestIdleCallback = (callback, opts) =>
+ setTimeout(
+ () =>
+ callback({
+ didTimeout: false,
+ timeRemaining() {
+ return 1;
+ }
+ }),
+ (opts && opts.timeout) || 0
+ );
+ window.cancelIdleCallback = (id) => clearTimeout(id);
+
+ evaluateScript("ui-icons.js");
+ evaluateScript("inject.js");
+
+ return chrome;
+}
+
+describe("inject.js helper logic", () => {
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ delete global.chrome;
+ });
+
+ it("normalizes bindings from legacy formats", async () => {
+ bootInject();
+ await flushAsyncWork(3);
+
+ expect(
+ window.normalizeStoredBinding({
+ action: "faster",
+ key: "g",
+ value: 1.8,
+ force: false
+ }).code
+ ).toBe("KeyG");
+
+ expect(
+ window.normalizeStoredBinding({
+ action: "pause",
+ code: null,
+ key: null,
+ keyCode: null,
+ value: 0
+ })
+ ).toEqual({
+ action: "pause",
+ code: null,
+ disabled: true,
+ value: 0,
+ force: "false",
+ predefined: false
+ });
+
+ expect(window.defaultKeyBindings({ speedStep: 0.25, rewindTime: 5 })[0]).toEqual(
+ {
+ action: "slower",
+ code: "KeyS",
+ value: 0.25,
+ force: false,
+ predefined: true
+ }
+ );
+ });
+
+ it("clamps controller margins and ignores stale source-specific target speeds", async () => {
+ bootInject();
+ await flushAsyncWork(3);
+
+ expect(window.normalizeControllerMarginPx(250, 0)).toBe(200);
+ expect(window.normalizeControllerMarginPx(-5, 65)).toBe(0);
+ expect(window.normalizeControllerMarginPx("bad", 65)).toBe(65);
+
+ const staleVideo = {
+ currentSrc: "fresh.mp4",
+ vsc: {
+ targetSpeed: 1.75,
+ targetSpeedSourceKey: "old.mp4"
+ }
+ };
+
+ expect(window.getControllerTargetSpeed(staleVideo)).toBeNull();
+
+ window.tc.settings.rememberSpeed = true;
+ window.tc.settings.forceLastSavedSpeed = false;
+ window.tc.settings.lastSpeed = 1.3;
+ window.tc.settings.speeds = { "fresh.mp4": 1.6 };
+
+ expect(window.getRememberedSpeed({ currentSrc: "fresh.mp4" })).toBe(1.6);
+ expect(window.getDesiredSpeed(staleVideo)).toBe(1.6);
+ });
+
+ it("applies site rule overrides and detects disabled sites", async () => {
+ bootInject();
+ await flushAsyncWork(3);
+
+ window.tc.settings.siteRules = [{ pattern: "localhost", enabled: false }];
+ window.captureSiteRuleBase();
+ expect(window.applySiteRuleOverrides()).toBe(true);
+
+ window.resetSettingsFromSiteRuleBase();
+ window.tc.settings.siteRules = [
+ {
+ pattern: "localhost",
+ controllerLocation: "bottom-left",
+ controllerMarginTop: 300,
+ controllerMarginBottom: -10,
+ rememberSpeed: true
+ }
+ ];
+ window.captureSiteRuleBase();
+
+ expect(window.applySiteRuleOverrides()).toBe(false);
+ expect(window.tc.settings.controllerLocation).toBe("bottom-left");
+ expect(window.tc.settings.controllerMarginTop).toBe(200);
+ expect(window.tc.settings.controllerMarginBottom).toBe(0);
+ expect(window.tc.settings.rememberSpeed).toBe(true);
+ });
+});
diff --git a/tests/lucide-client.spec.js b/tests/lucide-client.spec.js
new file mode 100644
index 0000000..b5b439b
--- /dev/null
+++ b/tests/lucide-client.spec.js
@@ -0,0 +1,61 @@
+const { afterEach, describe, expect, it } = require("vitest");
+const {
+ evaluateScript,
+ loadHtmlString
+} = require("./helpers/extension-test-utils");
+
+describe("lucide-client.js", () => {
+ afterEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ it("builds icon URLs and rejects invalid slugs", () => {
+ loadHtmlString("");
+ evaluateScript("ui-icons.js");
+ evaluateScript("lucide-client.js");
+
+ expect(window.lucideIconSvgUrl("alarm-clock")).toContain(
+ "/icons/alarm-clock.svg"
+ );
+ expect(window.lucideIconSvgUrl("bad slug!!")).toBe("");
+ expect(window.lucideTagsJsonUrl()).toContain("/tags.json");
+ });
+
+ it("sanitizes SVG before persisting a Lucide icon", () => {
+ loadHtmlString("");
+ evaluateScript("ui-icons.js");
+ evaluateScript("lucide-client.js");
+
+ const sanitized = window.sanitizeLucideSvg(`
+
+ `);
+
+ expect(sanitized).toContain("