Compare commits

...

23 Commits

Author SHA1 Message Date
joshpatra ab7ae99807 v5.1.9.0-beta.1 2026-04-07 14:54:34 -04:00
joshpatra 5d47c511be Bump version to 5.1.9.0 2026-04-07 14:54:33 -04:00
joshpatra 7713ba5bad Fix Firefox extension link in README
Updated Firefox extension link in README.md.
2026-04-07 14:53:49 -04:00
joshpatra ce0b28de4f Update shortcut validation and stop tracking VS Code settings.
Allow empty keybinds for optional shortcuts while requiring Show/Hide controller, Decrease speed, and Increase speed, and remove tracked .vscode files while keeping the folder gitignored.
2026-04-07 14:52:50 -04:00
joshpatra a2b041e225 v5.1.8.0-beta.1 2026-04-07 14:33:07 -04:00
joshpatra 4f87aadfd7 Bump version to 5.1.8.0 2026-04-07 14:32:21 -04:00
joshpatra 8456111bf0 fix: correct AMO URL and lucide spec object key 2026-04-07 14:31:45 -04:00
joshpatra 6efe92a036 Refine site rule toggle and override UI 2026-04-07 14:31:41 -04:00
joshpatra 0cb13905ff Fix subtitle nudge site gating 2026-04-07 14:31:39 -04:00
joshpatra f32d1b3f71 Accept raw settings backups during import 2026-04-07 14:31:36 -04:00
joshpatra e6c56bcecb Allow zero controller opacity in settings 2026-04-07 14:31:34 -04:00
joshpatra a7a0aafd68 Add Vitest suite and fix wrapped local import restore 2026-04-07 14:31:27 -04:00
joshpatra 3cf1a4acd1 style(inject): normalize formatting 2026-04-04 16:14:56 -04:00
joshpatra a9956831c4 refactor(shortcuts): switch shortcut bindings to event.code 2026-04-04 13:33:11 -04:00
joshpatra 25d3acf576 v5.1.7.0-beta.1 2026-04-02 22:34:00 -04:00
joshpatra 7b8b4324af Bump version to 5.1.7.0 2026-04-02 22:33:59 -04:00
joshpatra 8b9e4bea1d fix: refresh site rules on DOM video changes 2026-04-02 21:16:37 -04:00
joshpatra 8c94cc2088 fix: clarify site rule auto-hide copy 2026-04-02 21:08:15 -04:00
joshpatra 19d3af02a2 refactor: store settings as sparse diffs 2026-04-02 21:07:31 -04:00
joshpatra 306e0e3ea0 Exclude Lucide cache from backups 2026-04-02 20:47:32 -04:00
joshpatra 1536c13c3e v5.1.6-beta.1 2026-04-02 18:20:49 -04:00
joshpatra 6bd319c8cc Bump version to 5.1.6 2026-04-02 18:20:48 -04:00
joshpatra 3aee8c8f9a fix: errors from web-ext 2026-04-02 18:20:33 -04:00
35 changed files with 6225 additions and 947 deletions
+2
View File
@@ -10,6 +10,8 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
WEB_EXT_IGNORE_FILES: scripts/**
steps:
- uses: actions/checkout@v4
-3
View File
@@ -1,3 +0,0 @@
{
"kiroAgent.configureMCP": "Disabled"
}
+3 -3
View File
@@ -1,5 +1,5 @@
# Available for Firefox
[![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org//firefox/addon/speeder/)
[![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org/firefox/addon/speeder/)
# The science of accelerated playback
@@ -33,7 +33,7 @@ last point to listen to it a few more times.
![Player](https://cloud.githubusercontent.com/assets/2400185/24076745/5723e6ae-0c41-11e7-820c-1d8e814a2888.png)
#### *Install [Chrome](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk) or [Firefox](https://addons.mozilla.org/en-us/firefox/addon/videospeed/) Extension*
#### *Install [Chrome](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk) or [Firefox](https://addons.mozilla.org/en-us/firefox/addon/speeder/) Extension*
\*\* Once the extension is installed simply navigate to any page that offers
HTML5 video ([example](https://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll
@@ -56,7 +56,7 @@ shortcuts with different values, which will allow you to quickly toggle between
your most commonly used speeds. To add a new shortcut, open extension settings
and click "Add New".
![settings Add New shortcut](https://user-images.githubusercontent.com/121805/50726471-50242200-1172-11e9-902f-0e5958387617.jpg)
<img width="1760" height="1330" alt="image" src="https://github.com/user-attachments/assets/32e814dd-93ea-4943-8ec9-3eca735447ac" />
Some sites may assign other functionality to one of the assigned shortcut keys —
these collisions are inevitable, unfortunately. As a workaround, the extension
+49 -43
View File
@@ -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);
}
+234 -311
View File
@@ -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;
@@ -15,7 +20,6 @@ var tc = {
lastSpeed: 1.0,
enabled: true,
speeds: {},
displayKeyCode: 86,
rememberSpeed: false,
forceLastSavedSpeed: false,
audioBoolean: false,
@@ -34,7 +38,7 @@ var tc = {
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
defaultLogLevel: 3,
logLevel: 3,
enableSubtitleNudge: true, // Enabled by default, but only activates on YouTube
enableSubtitleNudge: false,
subtitleNudgeInterval: 50, // Default 50ms balances subtitle tracking with CPU cost
subtitleNudgeAmount: 0.001,
customButtonIcons: {}
@@ -57,20 +61,23 @@ var vscObservedRoots = new WeakSet();
var requestIdle =
typeof window.requestIdleCallback === "function"
? window.requestIdleCallback.bind(window)
: function (callback, options) {
: 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",
@@ -116,74 +123,24 @@ var controllerLocationStyles = {
/* `label` fallback only when ui-icons has no path for the action. */
var controllerButtonDefs = {
rewind: { label: "", className: "rw" },
slower: { label: "", className: "" },
faster: { label: "", className: "" },
advance: { label: "", className: "rw" },
display: { label: "", className: "hideButton" },
reset: { label: "\u21BB", className: "" },
fast: { label: "", className: "" },
rewind: { label: "", className: "rw" },
slower: { label: "", className: "" },
faster: { label: "", className: "" },
advance: { label: "", className: "rw" },
display: { label: "", className: "hideButton" },
reset: { label: "\u21BB", className: "" },
fast: { label: "", className: "" },
settings: { label: "", className: "" },
pause: { label: "", className: "" },
muted: { label: "", className: "" },
mark: { label: "", className: "" },
jump: { label: "", className: "" }
pause: { label: "", className: "" },
muted: { label: "", className: "" },
mark: { label: "", className: "" },
jump: { label: "", className: "" }
};
var keyCodeToEventKey = {
32: " ",
37: "ArrowLeft",
38: "ArrowUp",
39: "ArrowRight",
40: "ArrowDown",
96: "0",
97: "1",
98: "2",
99: "3",
100: "4",
101: "5",
102: "6",
103: "7",
104: "8",
105: "9",
106: "*",
107: "+",
109: "-",
110: ".",
111: "/",
112: "F1",
113: "F2",
114: "F3",
115: "F4",
116: "F5",
117: "F6",
118: "F7",
119: "F8",
120: "F9",
121: "F10",
122: "F11",
123: "F12",
186: ";",
188: "<",
189: "-",
187: "+",
190: ">",
191: "/",
192: "~",
219: "[",
220: "\\",
221: "]",
222: "'",
59: ";",
61: "+",
173: "-"
};
function createDefaultBinding(action, key, keyCode, value) {
function createDefaultBinding(action, code, value) {
return {
action: action,
key: key,
keyCode: keyCode,
code: code,
value: value,
force: false,
predefined: true
@@ -194,89 +151,73 @@ function defaultKeyBindings(storage) {
return [
createDefaultBinding(
"slower",
"S",
Number(storage.slowerKeyCode) || 83,
"KeyS",
Number(storage.speedStep) || 0.1
),
createDefaultBinding(
"faster",
"D",
Number(storage.fasterKeyCode) || 68,
"KeyD",
Number(storage.speedStep) || 0.1
),
createDefaultBinding(
"rewind",
"Z",
Number(storage.rewindKeyCode) || 90,
"KeyZ",
Number(storage.rewindTime) || 10
),
createDefaultBinding(
"advance",
"X",
Number(storage.advanceKeyCode) || 88,
"KeyX",
Number(storage.advanceTime) || 10
),
createDefaultBinding(
"reset",
"R",
Number(storage.resetKeyCode) || 82,
"KeyR",
1.0
),
createDefaultBinding(
"fast",
"G",
Number(storage.fastKeyCode) || 71,
"KeyG",
Number(storage.fastSpeed) || 1.8
),
createDefaultBinding(
"move",
"P",
80,
"KeyP",
0
),
createDefaultBinding(
"toggleSubtitleNudge",
"N",
78,
"KeyN",
0
)
];
}
function ensureDefaultKeyBinding(action, key, keyCode, value) {
function ensureDefaultKeyBinding(action, code, value) {
if (tc.settings.keyBindings.some((binding) => binding.action === action)) {
return false;
}
tc.settings.keyBindings.push(
createDefaultBinding(action, key, keyCode, value)
createDefaultBinding(action, code, value)
);
return true;
}
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) {
@@ -315,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) {
@@ -447,7 +386,7 @@ function captureSiteRuleBase() {
? tc.settings.controllerButtons.slice()
: tc.settings.controllerButtons,
keyBindings: Array.isArray(tc.settings.keyBindings)
? tc.settings.keyBindings.map(function (binding) {
? tc.settings.keyBindings.map(function(binding) {
return Object.assign({}, binding);
})
: tc.settings.keyBindings
@@ -473,7 +412,7 @@ function resetSettingsFromSiteRuleBase() {
? base.controllerButtons.slice()
: base.controllerButtons;
tc.settings.keyBindings = Array.isArray(base.keyBindings)
? base.keyBindings.map(function (binding) {
? base.keyBindings.map(function(binding) {
return Object.assign({}, binding);
})
: base.keyBindings;
@@ -537,44 +476,41 @@ 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 legacyKeyCodeToBinding(keyCode) {
if (!Number.isInteger(keyCode)) return null;
var key = keyCodeToEventKey[keyCode];
if (!key && keyCode >= 48 && keyCode <= 57) {
key = String.fromCharCode(keyCode);
}
if (!key && keyCode >= 65 && keyCode <= 90) {
key = String.fromCharCode(keyCode);
}
return {
key: normalizeBindingKey(key),
keyCode: keyCode,
code: null,
disabled: false
};
function legacyBindingKeyToCode(key) {
return keyBindingUtils.legacyBindingKeyToCode(key);
}
function normalizeStoredBinding(binding, fallbackKeyCode) {
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
if (!binding) return fallbackBinding;
function legacyKeyCodeToCode(keyCode) {
return keyBindingUtils.legacyKeyCodeToCode(keyCode);
}
function inferBindingCode(binding, fallbackCode) {
return keyBindingUtils.inferBindingCode(binding, fallbackCode);
}
function normalizeStoredBinding(binding, fallbackCode) {
if (!binding) {
if (!fallbackCode) return null;
return {
code: fallbackCode,
disabled: false,
value: 0,
force: "false",
predefined: false
};
}
if (
binding.disabled === true ||
(binding.key === null &&
binding.keyCode === null &&
binding.code === null)
(binding.code === null &&
binding.key === null &&
binding.keyCode === null)
) {
return {
action: binding.action,
key: null,
keyCode: null,
code: null,
disabled: true,
value: Number(binding.value),
@@ -583,46 +519,20 @@ function normalizeStoredBinding(binding, fallbackKeyCode) {
};
}
var normalizedCode = inferBindingCode(binding, fallbackCode);
if (!normalizedCode) {
return null;
}
var normalized = {
action: binding.action,
key: null,
keyCode: null,
code:
typeof binding.code === "string" && binding.code.length > 0
? binding.code
: null,
code: normalizedCode,
disabled: false,
value: Number(binding.value),
force: String(binding.force) === "true" ? "true" : "false",
predefined: Boolean(binding.predefined)
};
if (typeof binding.key === "string") {
normalized.key = normalizeBindingKey(binding.key);
}
var legacyKeyCode = getLegacyKeyCode(binding);
if (Number.isInteger(legacyKeyCode)) {
var legacyBinding = legacyKeyCodeToBinding(legacyKeyCode);
if (legacyBinding) {
normalized.key = normalized.key || legacyBinding.key;
normalized.keyCode = legacyKeyCode;
}
}
if (Number.isInteger(binding.keyCode)) {
normalized.keyCode = binding.keyCode;
}
if (!normalized.key && fallbackBinding) {
normalized.key = fallbackBinding.key;
if (normalized.keyCode === null) normalized.keyCode = fallbackBinding.keyCode;
}
if (!normalized.key && !normalized.code && normalized.keyCode === null) {
return null;
}
return normalized;
}
@@ -739,19 +649,32 @@ function isSubtitleNudgeSupported(video) {
return Boolean(video);
}
function isSubtitleNudgeAvailableForVideo(video) {
return isSubtitleNudgeSupported(video) && Boolean(tc.settings.enableSubtitleNudge);
}
function isSubtitleNudgeEnabledForVideo(video) {
if (!video || !video.vsc) return tc.settings.enableSubtitleNudge;
if (!isSubtitleNudgeAvailableForVideo(video)) return false;
if (!video || !video.vsc) return true;
if (typeof video.vsc.subtitleNudgeEnabledOverride === "boolean") {
return video.vsc.subtitleNudgeEnabledOverride;
}
return tc.settings.enableSubtitleNudge;
return true;
}
function setSubtitleNudgeEnabledForVideo(video, enabled) {
if (!video || !video.vsc) return false;
if (!isSubtitleNudgeAvailableForVideo(video)) {
video.vsc.subtitleNudgeEnabledOverride = null;
video.vsc.stopSubtitleNudge();
updateSubtitleNudgeIndicator(video);
return false;
}
var normalizedEnabled = Boolean(enabled);
video.vsc.subtitleNudgeEnabledOverride = normalizedEnabled;
@@ -768,7 +691,7 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
if (flashEl) {
flashEl.classList.add("visible");
clearTimeout(flashEl._flashTimer);
flashEl._flashTimer = setTimeout(function () {
flashEl._flashTimer = setTimeout(function() {
flashEl.classList.remove("visible");
}, 1500);
}
@@ -776,50 +699,67 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
return normalizedEnabled;
}
function subtitleNudgeIconMarkup(isEnabled) {
function renderSubtitleNudgeIndicatorContent(target, isEnabled) {
if (!target) return;
var action = isEnabled ? "subtitleNudgeOn" : "subtitleNudgeOff";
var custom =
tc.settings.customButtonIcons &&
tc.settings.customButtonIcons[action] &&
tc.settings.customButtonIcons[action].svg;
vscClearElement(target);
if (custom) {
return (
'<span class="vsc-btn-icon" aria-hidden="true">' + custom + "</span>"
var customWrap = vscCreateSvgWrap(
target.ownerDocument || document,
custom,
"vsc-btn-icon"
);
if (customWrap) {
target.appendChild(customWrap);
return;
}
}
if (typeof vscIconSvgString !== "function") {
return isEnabled ? "✓" : "×";
target.textContent = isEnabled ? "✓" : "×";
return;
}
var svg = vscIconSvgString(action, 14);
if (!svg) {
return isEnabled ? "✓" : "×";
target.textContent = isEnabled ? "✓" : "×";
return;
}
return (
'<span class="vsc-btn-icon" aria-hidden="true">' + svg + "</span>"
);
var wrap = vscCreateSvgWrap(target.ownerDocument || document, svg, "vsc-btn-icon");
if (wrap) {
target.appendChild(wrap);
return;
}
target.textContent = isEnabled ? "✓" : "×";
}
function updateSubtitleNudgeIndicator(video) {
if (!video || !video.vsc) return;
var isAvailable = isSubtitleNudgeAvailableForVideo(video);
var isEnabled = isSubtitleNudgeEnabledForVideo(video);
var title = isEnabled ? "Subtitle nudge enabled" : "Subtitle nudge disabled";
var mark = subtitleNudgeIconMarkup(isEnabled);
var title = !isAvailable
? "Subtitle nudge unavailable on this site"
: isEnabled
? "Subtitle nudge enabled"
: "Subtitle nudge disabled";
var indicator = video.vsc.subtitleNudgeIndicator;
if (indicator) {
indicator.innerHTML = mark;
renderSubtitleNudgeIndicatorContent(indicator, isEnabled);
indicator.dataset.enabled = isEnabled ? "true" : "false";
indicator.dataset.supported = "true";
indicator.dataset.supported = isAvailable ? "true" : "false";
indicator.title = title;
indicator.setAttribute("aria-label", title);
}
var flashEl = video.vsc.nudgeFlashIndicator;
if (flashEl) {
flashEl.innerHTML = mark;
renderSubtitleNudgeIndicatorContent(flashEl, isEnabled);
flashEl.dataset.enabled = isEnabled ? "true" : "false";
flashEl.dataset.supported = "true";
flashEl.dataset.supported = isAvailable ? "true" : "false";
flashEl.setAttribute("aria-label", title);
}
}
@@ -830,7 +770,7 @@ function schedulePersistLastSpeed(speed) {
tc.pendingLastSpeedValue = speed;
if (tc.pendingLastSpeedSave !== null) return;
tc.pendingLastSpeedSave = setTimeout(function () {
tc.pendingLastSpeedSave = setTimeout(function() {
var speedToPersist = tc.pendingLastSpeedValue;
tc.pendingLastSpeedSave = null;
@@ -838,7 +778,8 @@ function schedulePersistLastSpeed(speed) {
return;
}
chrome.storage.sync.set({ lastSpeed: speedToPersist }, function () { });
chrome.storage.sync.set({ lastSpeed: speedToPersist }, function() {
});
tc.persistedLastSpeed = speedToPersist;
}, 250);
}
@@ -920,7 +861,7 @@ function scheduleSpeedRestore(video, desiredSpeed, reason) {
clearTimeout(video.vsc.restoreSpeedTimer);
}
video.vsc.restoreSpeedTimer = setTimeout(function () {
video.vsc.restoreSpeedTimer = setTimeout(function() {
if (!video.vsc) return;
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
@@ -967,19 +908,13 @@ function takePendingRateChange(video, currentSpeed) {
}
function matchesKeyBinding(binding, event) {
if (!binding || binding.disabled) return false;
var normalizedEventKey = normalizeBindingKey(event.key);
if (binding.key && normalizedEventKey) {
return binding.key === normalizedEventKey;
}
if (binding.code && event.code) {
return binding.code === event.code;
}
var legacyKeyCode = getLegacyKeyCode(binding);
return Number.isInteger(legacyKeyCode) && legacyKeyCode === event.keyCode;
return Boolean(
binding &&
binding.disabled !== true &&
typeof binding.code === "string" &&
binding.code.length > 0 &&
binding.code === event.code
);
}
function mediaSelector() {
@@ -1009,7 +944,7 @@ function hasUsableMediaSource(node) {
}
if (node.querySelectorAll) {
return Array.from(node.querySelectorAll("source[src]")).some(function (
return Array.from(node.querySelectorAll("source[src]")).some(function(
source
) {
var src = source.getAttribute("src");
@@ -1148,7 +1083,7 @@ function patchAttachShadow() {
}
var originalAttachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function () {
Element.prototype.attachShadow = function() {
var shadowRoot = originalAttachShadow.apply(this, arguments);
try {
if (shadowRoot) {
@@ -1179,7 +1114,7 @@ function log(message, level) {
}
}
chrome.storage.sync.get(tc.settings, function (storage) {
chrome.storage.sync.get(tc.settings, function(storage) {
var storedBindings = Array.isArray(storage.keyBindings)
? storage.keyBindings
: [];
@@ -1194,7 +1129,6 @@ chrome.storage.sync.get(tc.settings, function (storage) {
chrome.storage.sync.set({
keyBindings: tc.settings.keyBindings,
version: tc.settings.version,
displayKeyCode: tc.settings.displayKeyCode,
rememberSpeed: tc.settings.rememberSpeed,
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed,
audioBoolean: tc.settings.audioBoolean,
@@ -1214,7 +1148,6 @@ chrome.storage.sync.get(tc.settings, function (storage) {
tc.settings.lastSpeed = 1.0;
}
tc.persistedLastSpeed = tc.settings.lastSpeed;
tc.settings.displayKeyCode = Number(storage.displayKeyCode);
tc.settings.rememberSpeed = Boolean(storage.rememberSpeed);
tc.settings.forceLastSavedSpeed = Boolean(storage.forceLastSavedSpeed);
tc.settings.audioBoolean = Boolean(storage.audioBoolean);
@@ -1269,14 +1202,13 @@ chrome.storage.sync.get(tc.settings, function (storage) {
addedDefaultBinding =
ensureDefaultKeyBinding(
"display",
"V",
Number(storage.displayKeyCode) || 86,
"KeyV",
0
) || addedDefaultBinding;
addedDefaultBinding =
ensureDefaultKeyBinding("move", "P", 80, 0) || addedDefaultBinding;
ensureDefaultKeyBinding("move", "KeyP", 0) || addedDefaultBinding;
addedDefaultBinding =
ensureDefaultKeyBinding("toggleSubtitleNudge", "N", 78, 0) ||
ensureDefaultKeyBinding("toggleSubtitleNudge", "KeyN", 0) ||
addedDefaultBinding;
if (addedDefaultBinding) {
@@ -1288,7 +1220,7 @@ chrome.storage.sync.get(tc.settings, function (storage) {
// We use a global flag to ensure the listener is only attached once.
if (!window.vscMessageListener) {
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
function(request, sender, sendResponse) {
if (request.action === "rescan_page") {
log("Re-scan command received from popup.", 4);
initializeWhenReady(document, true);
@@ -1329,7 +1261,7 @@ chrome.storage.sync.get(tc.settings, function (storage) {
// Set the flag to prevent adding the listener again.
window.vscMessageListener = true;
}
chrome.storage.local.get(["customButtonIcons"], function (loc) {
chrome.storage.local.get(["customButtonIcons"], function(loc) {
tc.settings.customButtonIcons =
loc &&
loc.customButtonIcons &&
@@ -1339,30 +1271,33 @@ chrome.storage.sync.get(tc.settings, function (storage) {
if (!window.vscCustomIconListener) {
window.vscCustomIconListener = true;
chrome.storage.onChanged.addListener(function (changes, area) {
chrome.storage.onChanged.addListener(function(changes, area) {
if (area !== "local" || !changes.customButtonIcons) return;
var nv = changes.customButtonIcons.newValue;
tc.settings.customButtonIcons =
nv && typeof nv === "object" ? nv : {};
if (tc.mediaElements && tc.mediaElements.length) {
tc.mediaElements.forEach(function (video) {
tc.mediaElements.forEach(function(video) {
if (!video.vsc || !video.vsc.div) return;
var doc = video.ownerDocument;
var shadow = video.vsc.div.shadowRoot;
if (!shadow) return;
shadow.querySelectorAll("button[data-action]").forEach(function (btn) {
shadow.querySelectorAll("button[data-action]").forEach(function(btn) {
var act = btn.dataset.action;
if (!act) return;
var svg =
tc.settings.customButtonIcons &&
tc.settings.customButtonIcons[act] &&
tc.settings.customButtonIcons[act].svg;
btn.innerHTML = "";
vscClearElement(btn);
if (svg) {
var cw = doc.createElement("span");
cw.className = "vsc-btn-icon";
cw.innerHTML = svg;
btn.appendChild(cw);
var cw = vscCreateSvgWrap(doc, svg, "vsc-btn-icon");
if (cw) {
btn.appendChild(cw);
} else {
var cdf = controllerButtonDefs[act];
btn.textContent = (cdf && cdf.label) || "?";
}
} else if (typeof vscIconWrap === "function") {
var wrap = vscIconWrap(doc, act, 14);
if (wrap) {
@@ -1392,6 +1327,7 @@ function getKeyBindings(action, what = "value") {
return false;
}
}
function setKeyBindings(action, value) {
tc.settings.keyBindings.find((item) => item.action === action)["value"] =
value;
@@ -1405,10 +1341,12 @@ function createControllerButton(doc, action, label, className) {
tc.settings.customButtonIcons[action] &&
tc.settings.customButtonIcons[action].svg;
if (custom) {
var customWrap = doc.createElement("span");
customWrap.className = "vsc-btn-icon";
customWrap.innerHTML = custom;
button.appendChild(customWrap);
var customWrap = vscCreateSvgWrap(doc, custom, "vsc-btn-icon");
if (customWrap) {
button.appendChild(customWrap);
} else {
button.textContent = label || "?";
}
} else if (typeof vscIconWrap === "function") {
var wrap = vscIconWrap(doc, action, 14);
if (wrap) {
@@ -1426,7 +1364,7 @@ function createControllerButton(doc, action, label, className) {
}
function defineVideoController() {
tc.videoController = function (target, parent) {
tc.videoController = function(target, parent) {
if (target.vsc) return target.vsc;
tc.mediaElements.push(target);
target.vsc = this;
@@ -1446,7 +1384,7 @@ function defineVideoController() {
tc.settings.controllerLocation
);
log(`Creating video controller for ${target.tagName} with src: ${target.src || target.currentSrc || 'none'}`, 4);
log(`Creating video controller for ${target.tagName} with src: ${target.src || target.currentSrc || "none"}`, 4);
let storedSpeed = sanitizeSpeed(resolveTargetSpeed(target), 1.0);
this.targetSpeed = storedSpeed;
@@ -1466,9 +1404,9 @@ function defineVideoController() {
return;
}
log(`Controller created and attached to DOM. Hidden: ${this.div.classList.contains('vsc-hidden')}`, 4);
log(`Controller created and attached to DOM. Hidden: ${this.div.classList.contains("vsc-hidden")}`, 4);
var mediaEventAction = function (event) {
var mediaEventAction = function(event) {
if (
event.type === "loadedmetadata" ||
event.type === "loadeddata" ||
@@ -1586,7 +1524,7 @@ function defineVideoController() {
this.startSubtitleNudge();
};
tc.videoController.prototype.remove = function () {
tc.videoController.prototype.remove = function() {
this.stopSubtitleNudge();
if (this.youTubeAutoHideObserver) {
this.youTubeAutoHideObserver.disconnect();
@@ -1618,7 +1556,7 @@ function defineVideoController() {
if (idx != -1) tc.mediaElements.splice(idx, 1);
};
tc.videoController.prototype.startSubtitleNudge = function () {
tc.videoController.prototype.startSubtitleNudge = function() {
if (
!isSubtitleNudgeSupported(this.video) ||
!isSubtitleNudgeEnabledForVideo(this.video) ||
@@ -1691,7 +1629,7 @@ function defineVideoController() {
log(`Nudge: Starting with interval ${tc.settings.subtitleNudgeInterval}ms.`, 5);
};
tc.videoController.prototype.stopSubtitleNudge = function () {
tc.videoController.prototype.stopSubtitleNudge = function() {
if (this.nudgeAnimationId !== null) {
clearTimeout(this.nudgeAnimationId);
this.nudgeAnimationId = null;
@@ -1709,7 +1647,7 @@ function defineVideoController() {
// doesn't lose the user's intended speed if the site hijacks it.
};
tc.videoController.prototype.performImmediateNudge = function () {
tc.videoController.prototype.performImmediateNudge = function() {
if (
!isSubtitleNudgeSupported(this.video) ||
!isSubtitleNudgeEnabledForVideo(this.video) ||
@@ -1739,7 +1677,7 @@ function defineVideoController() {
log(`Immediate nudge performed at rate ${targetRate.toFixed(2)}`, 5);
};
tc.videoController.prototype.setupYouTubeAutoHide = function (wrapper) {
tc.videoController.prototype.setupYouTubeAutoHide = function(wrapper) {
if (!wrapper || !isOnYouTube()) return;
const video = this.video;
@@ -1755,7 +1693,7 @@ function defineVideoController() {
// The vsc-hidden class (from V key) takes precedence via CSS specificity
if (ytPlayer.classList.contains("ytp-autohide")) {
wrapper.classList.add("ytp-autohide");
// Immediately end any temporary "vsc-show" state to hide with YouTube
// UNLESS it was forced by a shortcut (vsc-forced-show)
if (!wrapper.classList.contains("vsc-forced-show")) {
@@ -1765,7 +1703,7 @@ function defineVideoController() {
wrapper.showTimeOut = undefined;
}
}
log("YouTube controls hidden, hiding controller", 5);
} else {
wrapper.classList.remove("ytp-autohide");
@@ -1815,7 +1753,7 @@ function defineVideoController() {
};
};
tc.videoController.prototype.setupGenericAutoHide = function (wrapper) {
tc.videoController.prototype.setupGenericAutoHide = function(wrapper) {
if (!wrapper) return;
const video = this.video;
@@ -1874,7 +1812,7 @@ function defineVideoController() {
log(`Generic auto-hide setup complete with ${tc.settings.hideWithControlsTimer}s timer`, 4);
};
tc.videoController.prototype.initializeControls = function () {
tc.videoController.prototype.initializeControls = function() {
const doc = this.video.ownerDocument;
const speed = this.video.playbackRate.toFixed(2);
var wrapper = doc.createElement("div");
@@ -1912,7 +1850,7 @@ function defineVideoController() {
var subtitleNudgeIndicator = null;
buttonConfig.forEach(function (btnId) {
buttonConfig.forEach(function(btnId) {
if (btnId === "nudge") {
subtitleNudgeIndicator = doc.createElement("span");
subtitleNudgeIndicator.id = "nudge-indicator";
@@ -1944,20 +1882,22 @@ function defineVideoController() {
this.subtitleNudgeIndicator = subtitleNudgeIndicator;
this.nudgeFlashIndicator = nudgeFlashIndicator;
this.resetButtonEl =
shadow.querySelector('button[data-action="reset"]') || null;
shadow.querySelector("button[data-action=\"reset\"]") || null;
this.resetToggleArmed = false;
if (subtitleNudgeIndicator) {
updateSubtitleNudgeIndicator(this.video);
}
function blurAfterPointerTap(target, e) {
if (!target || typeof target.blur !== "function") return;
var pt = e.pointerType;
if (pt === "mouse" || pt === "touch" || (!pt && e.detail > 0)) {
requestAnimationFrame(function () {
requestAnimationFrame(function() {
target.blur();
});
}
}
dragHandle.addEventListener(
"mousedown",
(e) => {
@@ -2005,7 +1945,7 @@ function defineVideoController() {
this.setupGenericAutoHide(wrapper);
}
}
var fragment = doc.createDocumentFragment();
fragment.appendChild(wrapper);
const parentEl = this.parent || this.video.parentElement;
@@ -2071,10 +2011,6 @@ function defineVideoController() {
};
}
function escapeStringRegExp(str) {
const m = /[|\\{}()[\]^$+*?.]/g;
return str.replace(m, "\\$&");
}
function applySiteRuleOverrides() {
resetSettingsFromSiteRuleBase();
@@ -2083,34 +2019,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;
@@ -2118,13 +2027,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
@@ -2153,7 +2058,7 @@ function applySiteRuleOverrides() {
[
"controllerMarginTop",
"controllerMarginBottom"
].forEach(function (key) {
].forEach(function(key) {
tc.settings[key] = normalizeControllerMarginPx(tc.settings[key], 0);
});
@@ -2184,7 +2089,7 @@ function applySiteRuleOverrides() {
/** Apply current tc.settings controller layout/opacity to every attached controller (after site rules). */
function refreshAllControllerGeometry() {
tc.mediaElements.forEach(function (video) {
tc.mediaElements.forEach(function(video) {
if (!video || !video.vsc) return;
applyControllerLocation(video.vsc, tc.settings.controllerLocation);
var controllerEl = getControllerElement(video.vsc);
@@ -2218,6 +2123,7 @@ function shouldPreserveDesiredSpeed(video, speed) {
function setupListener(root) {
root = root || document;
if (root.vscRateListenerAttached) return;
function updateSpeedFromEvent(video, skipResetDisarm) {
if (!video.vsc || !video.vsc.speedIndicator) return;
if (!skipResetDisarm) {
@@ -2238,9 +2144,10 @@ function setupListener(root) {
else video.vsc.startSubtitleNudge();
}
}
root.addEventListener(
"ratechange",
function (event) {
function(event) {
if (tc.isNudging) return;
var video = event.target;
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
@@ -2284,6 +2191,7 @@ function setupListener(root) {
}
var vscInitializedDocuments = new Set();
function clearPendingInitialization(doc) {
if (!doc || !doc.vscPendingInitializeHandler) return;
@@ -2320,7 +2228,7 @@ function initializeWhenReady(doc, forceReinit = false) {
if (doc.vscPendingInitializeHandler) return;
var pendingInitializeHandler = function () {
var pendingInitializeHandler = function() {
tryInitializeDocument(doc, doc.vscPendingForceReinit === true);
};
@@ -2335,6 +2243,7 @@ function initializeWhenReady(doc, forceReinit = false) {
setTimeout(pendingInitializeHandler, 0);
}
}
function inIframe() {
try {
return window.self !== window.top;
@@ -2347,13 +2256,14 @@ function attachKeydownListeners(doc) {
var docs = [doc];
try {
if (inIframe() && window.top.document !== doc) docs.push(window.top.document);
} catch (e) { }
} catch (e) {
}
docs.forEach(function (keyDoc) {
docs.forEach(function(keyDoc) {
if (keyDoc.vscKeydownListenerAttached) return;
keyDoc.addEventListener(
"keydown",
function (event) {
function(event) {
if (
!event.getModifierState ||
event.getModifierState("Alt") ||
@@ -2376,7 +2286,7 @@ function attachKeydownListeners(doc) {
if (!tc.mediaElements.length) return;
var item = tc.settings.keyBindings.find(function (binding) {
var item = tc.settings.keyBindings.find(function(binding) {
return matchesKeyBinding(binding, event);
});
@@ -2400,24 +2310,24 @@ function attachMutationObserver(root) {
var pendingMutations = [];
var mutationProcessingScheduled = false;
var observer = new MutationObserver(function (mutations) {
var observer = new MutationObserver(function(mutations) {
pendingMutations.push(...mutations);
if (mutationProcessingScheduled) return;
mutationProcessingScheduled = true;
requestIdle(
function () {
function() {
var mutationsToProcess = pendingMutations.splice(0);
mutationProcessingScheduled = false;
mutationsToProcess.forEach(function (mutation) {
mutationsToProcess.forEach(function(mutation) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(function (node) {
mutation.addedNodes.forEach(function(node) {
// Skip text nodes, comments, etc. — only elements can contain media
if (node.nodeType !== Node.ELEMENT_NODE) return;
scanNodeForMedia(node, node.parentNode || mutation.target, true);
});
mutation.removedNodes.forEach(function (node) {
mutation.removedNodes.forEach(function(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
scanNodeForMedia(node, node.parentNode || mutation.target, false);
});
@@ -2466,7 +2376,7 @@ function attachMutationObserver(root) {
function attachMediaDetectionListeners(root) {
if (root.vscMediaEventListenersAttached) return;
var handleDetectedMedia = function (event) {
var handleDetectedMedia = function(event) {
var target = event.target;
if (!isMediaElement(target)) return;
ensureController(target, target.parentElement || target.parentNode);
@@ -2479,21 +2389,22 @@ function attachMediaDetectionListeners(root) {
"canplay",
"playing",
"play"
].forEach(function (eventName) {
].forEach(function(eventName) {
root.addEventListener(eventName, handleDetectedMedia, true);
});
root.vscMediaEventListenersAttached = true;
}
function attachIframeListeners(doc) {
Array.from(doc.getElementsByTagName("iframe")).forEach(function (frame) {
Array.from(doc.getElementsByTagName("iframe")).forEach(function(frame) {
if (!frame.vscLoadListenerAttached) {
frame.addEventListener("load", function () {
frame.addEventListener("load", function() {
try {
if (frame.contentDocument) {
initializeWhenReady(frame.contentDocument, true);
}
} catch (e) { }
} catch (e) {
}
});
frame.vscLoadListenerAttached = true;
}
@@ -2502,24 +2413,25 @@ function attachIframeListeners(doc) {
if (frame.contentDocument) {
initializeWhenReady(frame.contentDocument);
}
} catch (e) { }
} catch (e) {
}
});
}
function attachNavigationListeners() {
if (window.vscNavigationListenersAttached) return;
var scheduleRescan = function () {
var scheduleRescan = function() {
clearTimeout(window.vscNavigationRescanTimer);
window.vscNavigationRescanTimer = setTimeout(function () {
window.vscNavigationRescanTimer = setTimeout(function() {
initializeWhenReady(document, true);
}, 300);
};
["pushState", "replaceState"].forEach(function (method) {
["pushState", "replaceState"].forEach(function(method) {
if (typeof history[method] !== "function") return;
var original = history[method];
history[method] = function () {
history[method] = function() {
var result = original.apply(this, arguments);
scheduleRescan();
return result;
@@ -2666,7 +2578,7 @@ function runAction(action, value, e) {
);
}
mediaTagsToProcess.forEach(function (v) {
mediaTagsToProcess.forEach(function(v) {
if (!v.vsc) return; // Don't process videos without a controller
var controller = v.vsc.div;
const userDrivenActionsThatShowController = [
@@ -2686,7 +2598,14 @@ function runAction(action, value, e) {
"toggleSubtitleNudge",
"display"
];
if (userDrivenActionsThatShowController.includes(action) && action !== "display") {
var subtitleNudgeActionBlocked =
(action === "toggleSubtitleNudge" || action === "nudge") &&
!isSubtitleNudgeAvailableForVideo(v);
if (
userDrivenActionsThatShowController.includes(action) &&
action !== "display" &&
!subtitleNudgeActionBlocked
) {
showController(controller, 2000, true);
}
if (v.classList.contains("vsc-cancelled")) return;
@@ -2855,15 +2774,18 @@ function resetSpeed(v, target, isFastKey = false) {
function muted(v) {
v.muted = !v.muted;
}
function setMark(v) {
v.vsc.mark = v.currentTime;
}
function jumpToMark(v) {
if (v.vsc && typeof v.vsc.mark === "number") {
extendSpeedRestoreWindow(v);
v.currentTime = v.vsc.mark;
}
}
function handleDrag(video, e) {
const c = video.vsc.div;
const sC = convertControllerToManualPosition(video.vsc);
@@ -2873,7 +2795,7 @@ function handleDrag(video, e) {
pE.parentNode &&
pE.parentNode.offsetHeight === pE.offsetHeight &&
pE.parentNode.offsetWidth === pE.offsetWidth
)
)
pE = pE.parentNode;
video.classList.add("vcs-dragging");
sC.classList.add("dragging");
@@ -2902,6 +2824,7 @@ function handleDrag(video, e) {
pE.addEventListener("mouseleave", eD);
pE.addEventListener("mousemove", sD);
}
function showController(controller, duration = 2000, forced = false) {
if (!controller || typeof controller.classList === "undefined") return;
var restoreHidden =
@@ -2919,7 +2842,7 @@ function showController(controller, duration = 2000, forced = false) {
clearTimeout(controller.showTimeOut);
}
controller.showTimeOut = setTimeout(function () {
controller.showTimeOut = setTimeout(function() {
controller.classList.remove("vsc-show");
controller.classList.remove("vsc-forced-show");
if (controller.restoreHiddenAfterShow === true) {
+2 -25
View File
@@ -31,32 +31,9 @@ function sanitizeLucideSvg(svgText) {
var t = String(svgText).replace(/\0/g, "").trim();
if (!/<svg[\s>]/i.test(t)) return null;
var doc = new DOMParser().parseFromString(t, "image/svg+xml");
var svg = doc.querySelector("svg");
if (doc.querySelector("parsererror")) return null;
var svg = vscSanitizeSvgTree(doc.querySelector("svg"));
if (!svg) return null;
svg.querySelectorAll("script").forEach(function (n) {
n.remove();
});
svg.querySelectorAll("style").forEach(function (n) {
n.remove();
});
svg.querySelectorAll("*").forEach(function (el) {
for (var i = el.attributes.length - 1; i >= 0; i--) {
var attr = el.attributes[i];
var name = attr.name.toLowerCase();
var val = attr.value;
if (name.indexOf("on") === 0) {
el.removeAttribute(attr.name);
continue;
}
if (
(name === "href" || name === "xlink:href") &&
/^javascript:/i.test(val)
) {
el.removeAttribute(attr.name);
}
}
});
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.removeAttribute("width");
svg.removeAttribute("height");
svg.setAttribute("width", "100%");
+4 -1
View File
@@ -1,7 +1,7 @@
{
"name": "Speeder",
"short_name": "Speeder",
"version": "5.1.4",
"version": "5.1.9.0",
"manifest_version": 2,
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
"homepage_url": "https://github.com/SoPat712/speeder",
@@ -59,6 +59,9 @@
"inject.css"
],
"js": [
"shared/controller-utils.js",
"shared/key-bindings.js",
"shared/site-rules.js",
"ui-icons.js",
"inject.js"
]
+119 -16
View File
@@ -7,6 +7,17 @@
--text: #17191c;
--muted: #626b76;
--accent: #111827;
--switch-track-off: #c1cad6;
--switch-track-off-border: #aeb8c5;
--switch-track-on: #111827;
--switch-track-on-border: #111827;
--switch-thumb-off: #ffffff;
--switch-thumb-on: #ffffff;
--toggle-open-fg: #111827;
--toggle-open-bg: #eef2f6;
--toggle-open-border: #c5ccd5;
--toggle-open-hover-bg: #e4eaf1;
--toggle-open-hover-border: #b5c0cc;
--danger: #b42318;
}
@@ -210,6 +221,7 @@ button:active {
}
button:focus-visible,
input[type="checkbox"]:focus-visible,
input[type="text"]:focus,
select:focus,
textarea:focus {
@@ -247,10 +259,49 @@ textarea:focus {
}
input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 2px 0 0;
accent-color: var(--accent);
appearance: none;
-webkit-appearance: none;
position: relative;
width: 46px;
min-width: 46px;
height: 28px;
margin: 0;
border: 1px solid var(--switch-track-off-border);
border-radius: 999px;
background: var(--switch-track-off);
cursor: pointer;
transition: background-color 120ms ease, border-color 120ms ease,
box-shadow 120ms ease;
flex-shrink: 0;
}
input[type="checkbox"]::before {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--switch-thumb-off);
box-shadow: 0 1px 2px rgba(17, 24, 39, 0.18),
inset 0 0 0 1px rgba(17, 24, 39, 0.08);
transition: transform 120ms ease, background-color 120ms ease;
}
input[type="checkbox"]:checked {
background: var(--switch-track-on);
border-color: var(--switch-track-on-border);
}
input[type="checkbox"]:checked::before {
transform: translateX(18px);
background: var(--switch-thumb-on);
}
input[type="checkbox"]:disabled {
cursor: default;
opacity: 0.7;
}
label {
@@ -322,6 +373,39 @@ label em {
.toggle-site-rule {
font-weight: 400;
color: var(--muted);
}
.toggle-site-rule:hover {
color: var(--toggle-open-fg);
background: var(--toggle-open-hover-bg);
border-color: var(--toggle-open-hover-border);
}
.toggle-site-rule .site-rule-toggle-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
pointer-events: none;
}
.toggle-site-rule .site-rule-toggle-icon svg {
width: 18px;
height: 18px;
}
.site-rule:not(.collapsed) .toggle-site-rule {
color: var(--toggle-open-fg);
background: var(--toggle-open-bg);
border-color: var(--toggle-open-border);
}
.site-rule:not(.collapsed) .toggle-site-rule:hover {
color: var(--toggle-open-fg);
background: var(--toggle-open-hover-bg);
border-color: var(--toggle-open-hover-border);
}
.row {
@@ -339,7 +423,7 @@ label em {
}
.row.row-checkbox {
grid-template-columns: minmax(0, 1fr) 24px;
grid-template-columns: minmax(0, 1fr) auto;
}
.row.row-checkbox input[type="checkbox"] {
@@ -403,9 +487,9 @@ label em {
.site-override-lead {
display: grid;
grid-template-columns: minmax(0, 1fr) 24px;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: flex-start;
align-items: center;
font-weight: 600;
margin-bottom: 8px;
cursor: pointer;
@@ -414,7 +498,6 @@ label em {
.site-override-lead input[type="checkbox"] {
justify-self: end;
margin-top: 3px;
}
.site-override-lead span {
@@ -427,10 +510,19 @@ label em {
.site-rule-override-section .site-autohide-container,
.site-rule-override-section .site-playback-container,
.site-rule-override-section .site-opacity-container,
.site-rule-override-section .site-subtitleNudge-container {
.site-rule-override-section .site-subtitleNudge-container,
.site-controlbar-container,
.site-popup-controlbar-container,
.site-shortcuts-container {
padding-left: 4px;
}
.site-override-disabled {
opacity: 0.48;
pointer-events: none;
user-select: none;
}
.cb-editor {
display: flex;
flex-direction: column;
@@ -803,7 +895,7 @@ button.lucide-result-tile.lucide-picked {
}
.site-rule-option-checkbox {
grid-template-columns: minmax(0, 1fr) 24px;
grid-template-columns: minmax(0, 1fr) auto;
}
.site-rule-option-checkbox > input[type="checkbox"] {
@@ -833,7 +925,7 @@ button.lucide-result-tile.lucide-picked {
.site-rule-split-label {
display: grid;
grid-template-columns: minmax(0, 1fr) 24px;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: flex-start;
width: 100%;
@@ -845,7 +937,7 @@ button.lucide-result-tile.lucide-picked {
.site-rule-split-label input[type="checkbox"] {
justify-self: end;
margin-top: 3px;
margin-top: 0;
}
.site-rule-option-checkbox > .site-rule-split-label {
@@ -889,8 +981,8 @@ button.lucide-result-tile.lucide-picked {
.force-label {
display: flex;
align-items: flex-start;
gap: 8px;
align-items: center;
gap: 10px;
width: auto;
margin: 0;
color: var(--muted);
@@ -898,7 +990,7 @@ button.lucide-result-tile.lucide-picked {
}
.force-label input {
margin-top: 2px;
margin-top: 0;
}
.action-row {
@@ -956,7 +1048,7 @@ button.lucide-result-tile.lucide-picked {
}
.site-override-lead {
grid-template-columns: minmax(0, 1fr) 24px;
grid-template-columns: minmax(0, 1fr) auto;
}
.action-row button,
@@ -1005,6 +1097,17 @@ button.lucide-result-tile.lucide-picked {
--text: #f2f4f6;
--muted: #a0a8b2;
--accent: #f2f4f6;
--switch-track-off: #374151;
--switch-track-off-border: #4b5563;
--switch-track-on: #aab7c6;
--switch-track-on-border: #aab7c6;
--switch-thumb-off: #f8fafc;
--switch-thumb-on: #111315;
--toggle-open-fg: #f2f4f6;
--toggle-open-bg: #2b3138;
--toggle-open-border: #4b5563;
--toggle-open-hover-bg: #374151;
--toggle-open-hover-border: #64748b;
--danger: #ff8a80;
}
+22 -10
View File
@@ -5,9 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Speeder Settings</title>
<link rel="stylesheet" href="options.css" />
<script src="shared/controller-utils.js"></script>
<script src="shared/key-bindings.js"></script>
<script src="shared/popup-controls.js"></script>
<script src="ui-icons.js"></script>
<script src="lucide-client.js"></script>
<script src="options.js"></script>
<script src="shared/import-export.js"></script>
<script src="importExport.js"></script>
</head>
<body>
@@ -479,7 +483,15 @@
<template id="siteRuleTemplate">
<div class="site-rule">
<div class="site-rule-header">
<button type="button" class="toggle-site-rule" title="Expand/Collapse">&plus;</button>
<button
type="button"
class="toggle-site-rule"
title="Expand site rule"
aria-label="Expand site rule"
aria-expanded="false"
>
<span class="site-rule-toggle-icon" aria-hidden="true">&hellip;</span>
</button>
<input
type="text"
class="site-pattern"
@@ -500,7 +512,7 @@
<span>Override placement for this site</span>
<input type="checkbox" class="override-placement" />
</label>
<div class="site-placement-container" style="display: none">
<div class="site-placement-container">
<div class="site-rule-option site-rule-option-field">
<label>Default controller location:</label>
<select class="site-controllerLocation">
@@ -538,7 +550,7 @@
<span>Override hide-by-default for this site</span>
<input type="checkbox" class="override-visibility" />
</label>
<div class="site-visibility-container" style="display: none">
<div class="site-visibility-container">
<div class="site-rule-option site-rule-option-checkbox">
<label>Hide controller by default:</label>
<input type="checkbox" class="site-startHidden" />
@@ -550,7 +562,7 @@
<span>Override auto-hide for this site</span>
<input type="checkbox" class="override-autohide" />
</label>
<div class="site-autohide-container" style="display: none">
<div class="site-autohide-container">
<div class="site-rule-option site-rule-option-checkbox">
<label class="site-rule-split-label">
<span>Hide with controls (idle-based)</span>
@@ -568,7 +580,7 @@
<span>Override playback for this site</span>
<input type="checkbox" class="override-playback" />
</label>
<div class="site-playback-container" style="display: none">
<div class="site-playback-container">
<div class="site-rule-option site-rule-option-checkbox">
<label>Remember playback speed:</label>
<input type="checkbox" class="site-rememberSpeed" />
@@ -588,7 +600,7 @@
<span>Override opacity for this site</span>
<input type="checkbox" class="override-opacity" />
</label>
<div class="site-opacity-container" style="display: none">
<div class="site-opacity-container">
<div class="site-rule-option site-rule-option-field">
<label>Controller opacity:</label>
<input type="text" class="site-controllerOpacity" />
@@ -600,7 +612,7 @@
<span>Override subtitle nudge for this site</span>
<input type="checkbox" class="override-subtitleNudge" />
</label>
<div class="site-subtitleNudge-container" style="display: none">
<div class="site-subtitleNudge-container">
<div class="site-rule-option site-rule-option-checkbox">
<label>Enable subtitle nudge:</label>
<input type="checkbox" class="site-enableSubtitleNudge" />
@@ -616,7 +628,7 @@
<span>Override in-player control bar for this site</span>
<input type="checkbox" class="override-controlbar" />
</label>
<div class="site-controlbar-container" style="display: none">
<div class="site-controlbar-container">
<div class="cb-editor">
<div class="cb-zone">
<div class="cb-zone-label">Active</div>
@@ -634,7 +646,7 @@
<span>Override extension popup for this site</span>
<input type="checkbox" class="override-popup-controlbar" />
</label>
<div class="site-popup-controlbar-container" style="display: none">
<div class="site-popup-controlbar-container">
<div class="site-rule-option site-rule-option-checkbox">
<label>Show popup control bar</label>
<input type="checkbox" class="site-showPopupControlBar" />
@@ -656,7 +668,7 @@
<span>Override shortcuts for this site</span>
<input type="checkbox" class="override-shortcuts" />
</label>
<div class="site-shortcuts-container" style="display: none"></div>
<div class="site-shortcuts-container"></div>
</div>
</div>
</div>
+378 -445
View File
@@ -1,106 +1,44 @@
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 = [];
var keyCodeAliases = {
0: "null",
null: "null",
undefined: "null",
32: "Space",
37: "Left",
38: "Up",
39: "Right",
40: "Down",
96: "Num 0",
97: "Num 1",
98: "Num 2",
99: "Num 3",
100: "Num 4",
101: "Num 5",
102: "Num 6",
103: "Num 7",
104: "Num 8",
105: "Num 9",
106: "Num *",
107: "Num +",
109: "Num -",
110: "Num .",
111: "Num /",
112: "F1",
113: "F2",
114: "F3",
115: "F4",
116: "F5",
117: "F6",
118: "F7",
119: "F8",
120: "F9",
121: "F10",
122: "F11",
123: "F12",
186: ";",
188: "<",
189: "-",
187: "+",
190: ">",
191: "/",
192: "~",
219: "[",
220: "\\",
221: "]",
222: "'",
59: ";",
61: "+",
173: "-"
};
var keyCodeToKey = {
32: " ",
37: "ArrowLeft",
38: "ArrowUp",
39: "ArrowRight",
40: "ArrowDown",
96: "0",
97: "1",
98: "2",
99: "3",
100: "4",
101: "5",
102: "6",
103: "7",
104: "8",
105: "9",
106: "*",
107: "+",
109: "-",
110: ".",
111: "/",
112: "F1",
113: "F2",
114: "F3",
115: "F4",
116: "F5",
117: "F6",
118: "F7",
119: "F8",
120: "F9",
121: "F10",
122: "F11",
123: "F12",
186: ";",
188: "<",
189: "-",
187: "+",
190: ">",
191: "/",
192: "~",
219: "[",
220: "\\",
221: "]",
222: "'",
59: ";",
61: "+",
173: "-"
var bindingCodeAliases = {
Space: "Space",
ArrowLeft: "Left",
ArrowUp: "Up",
ArrowRight: "Right",
ArrowDown: "Down",
Numpad0: "Num 0",
Numpad1: "Num 1",
Numpad2: "Num 2",
Numpad3: "Num 3",
Numpad4: "Num 4",
Numpad5: "Num 5",
Numpad6: "Num 6",
Numpad7: "Num 7",
Numpad8: "Num 8",
Numpad9: "Num 9",
NumpadMultiply: "Num *",
NumpadAdd: "Num +",
NumpadSubtract: "Num -",
NumpadDecimal: "Num .",
NumpadDivide: "Num /",
Backquote: "`",
Minus: "-",
Equal: "=",
BracketLeft: "[",
BracketRight: "]",
Backslash: "\\",
Semicolon: ";",
Quote: "'",
Comma: ",",
Period: ".",
Slash: "/"
};
var modifierKeys = new Set([
@@ -114,23 +52,18 @@ var modifierKeys = new Set([
"Shift"
]);
var displayKeyAliases = {
" ": "Space",
ArrowLeft: "Left",
ArrowUp: "Up",
ArrowRight: "Right",
ArrowDown: "Down"
};
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" },
@@ -156,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). */
@@ -172,8 +101,9 @@ var customButtonIconsLive = {};
function fillControlBarIconElement(icon, buttonId) {
if (!icon || !buttonId) return;
var doc = icon.ownerDocument || document;
if (buttonId === "nudge") {
icon.innerHTML = "";
vscClearElement(icon);
icon.className = "cb-icon cb-icon-nudge-pair";
function nudgeChipMarkup(action) {
var c = customButtonIconsLive[action];
@@ -189,10 +119,10 @@ function fillControlBarIconElement(icon, buttonId) {
sp.setAttribute("data-nudge-state", stateKey);
var inner = nudgeChipMarkup(action);
if (inner) {
var wrap = document.createElement("span");
wrap.className = "vsc-btn-icon";
wrap.innerHTML = inner;
sp.appendChild(wrap);
var wrap = vscCreateSvgWrap(doc, inner, "vsc-btn-icon");
if (wrap) {
sp.appendChild(wrap);
}
}
icon.appendChild(sp);
}
@@ -207,25 +137,23 @@ function fillControlBarIconElement(icon, buttonId) {
icon.className = "cb-icon";
var custom = customButtonIconsLive[buttonId];
if (custom && custom.svg) {
icon.innerHTML = custom.svg;
return;
if (vscSetSvgContent(icon, custom.svg)) return;
}
if (typeof vscIconSvgString === "function") {
var svgHtml = vscIconSvgString(buttonId, 16);
if (svgHtml) {
icon.innerHTML = svgHtml;
return;
if (vscSetSvgContent(icon, svgHtml)) return;
}
}
vscClearElement(icon);
var def = controllerButtonDefs[buttonId];
icon.textContent = (def && def.icon) || "?";
}
function createDefaultBinding(action, key, keyCode, value) {
function createDefaultBinding(action, code, value) {
return {
action: action,
key: key,
keyCode: keyCode,
code: code,
value: value,
force: false,
predefined: true
@@ -235,7 +163,6 @@ function createDefaultBinding(action, key, keyCode, value) {
var tcDefaults = {
speed: 1.0,
lastSpeed: 1.0,
displayKeyCode: 86,
rememberSpeed: false,
audioBoolean: false,
startHidden: false,
@@ -251,15 +178,15 @@ var tcDefaults = {
controllerMarginBottom: 65,
controllerMarginLeft: 0,
keyBindings: [
createDefaultBinding("display", "V", 86, 0),
createDefaultBinding("move", "P", 80, 0),
createDefaultBinding("slower", "S", 83, 0.1),
createDefaultBinding("faster", "D", 68, 0.1),
createDefaultBinding("rewind", "Z", 90, 10),
createDefaultBinding("advance", "X", 88, 10),
createDefaultBinding("reset", "R", 82, 1),
createDefaultBinding("fast", "G", 71, 1.8),
createDefaultBinding("toggleSubtitleNudge", "N", 78, 0)
createDefaultBinding("display", "KeyV", 0),
createDefaultBinding("move", "KeyP", 0),
createDefaultBinding("slower", "KeyS", 0.1),
createDefaultBinding("faster", "KeyD", 0.1),
createDefaultBinding("rewind", "KeyZ", 10),
createDefaultBinding("advance", "KeyX", 10),
createDefaultBinding("reset", "KeyR", 1),
createDefaultBinding("fast", "KeyG", 1.8),
createDefaultBinding("toggleSubtitleNudge", "KeyN", 0)
],
siteRules: [
{
@@ -302,6 +229,7 @@ const actionLabels = {
};
const speedBindingActions = ["slower", "faster", "fast"];
const requiredShortcutActions = new Set(["display", "slower", "faster"]);
function formatSpeedBindingDisplay(action, value) {
if (!speedBindingActions.includes(action)) {
@@ -363,21 +291,91 @@ function refreshAddShortcutSelector() {
}
}
function ensureDefaultBinding(storage, action, key, keyCode, value) {
function ensureDefaultBinding(storage, action, code, value) {
if (storage.keyBindings.some((item) => item.action === action)) return;
storage.keyBindings.push(createDefaultBinding(action, key, keyCode, value));
storage.keyBindings.push(createDefaultBinding(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 parseFiniteNumberOrFallback(value, fallback) {
var numericValue = parseFloat(value);
return Number.isFinite(numericValue) ? numericValue : fallback;
}
function updateSiteRuleToggleIcon(toggleButton, action) {
if (!toggleButton) return;
var iconEl = toggleButton.querySelector(".site-rule-toggle-icon");
if (!iconEl) return;
if (typeof vscIconSvgString === "function" && typeof vscSetSvgContent === "function") {
var svgHtml = vscIconSvgString(action, 16);
if (svgHtml && vscSetSvgContent(iconEl, svgHtml)) {
return;
}
}
iconEl.textContent = action === "chevronUp" ? "\u2212" : "\u2026";
}
function setSiteRuleExpandedState(ruleEl, expanded) {
if (!ruleEl) return;
var ruleBody = ruleEl.querySelector(".site-rule-body");
var toggleButton = ruleEl.querySelector(".toggle-site-rule");
if (ruleBody) {
ruleBody.style.display = expanded ? "block" : "none";
}
ruleEl.classList.toggle("collapsed", !expanded);
if (!toggleButton) return;
var label = expanded ? "Collapse site rule" : "Expand site rule";
toggleButton.title = label;
toggleButton.setAttribute("aria-label", label);
toggleButton.setAttribute("aria-expanded", expanded ? "true" : "false");
updateSiteRuleToggleIcon(toggleButton, expanded ? "chevronUp" : "moreHorizontal");
}
function setSiteOverrideContainerState(container, enabled) {
if (!container) return;
container.classList.toggle("site-override-disabled", !enabled);
container.setAttribute("aria-disabled", enabled ? "false" : "true");
Array.prototype.forEach.call(
container.querySelectorAll("input, select, textarea, button"),
function (control) {
control.disabled = !enabled;
}
);
Array.prototype.forEach.call(
container.querySelectorAll(".cb-block"),
function (block) {
block.draggable = enabled;
}
);
}
function applySiteRuleOverrideState(ruleEl, checkboxClass, containerClass) {
if (!ruleEl) return;
var checkbox = ruleEl.querySelector("." + checkboxClass);
var container = ruleEl.querySelector("." + containerClass);
if (!container) return;
container.style.display = "block";
setSiteOverrideContainerState(container, checkbox ? checkbox.checked : false);
}
function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
@@ -397,128 +395,88 @@ 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 legacyKeyCodeToBinding(keyCode) {
if (!Number.isInteger(keyCode)) return null;
var normalizedKey = keyCodeToKey[keyCode];
if (!normalizedKey && keyCode >= 48 && keyCode <= 57) {
normalizedKey = String.fromCharCode(keyCode);
}
if (!normalizedKey && keyCode >= 65 && keyCode <= 90) {
normalizedKey = String.fromCharCode(keyCode);
}
return {
key: normalizeBindingKey(normalizedKey),
keyCode: keyCode,
code: null,
disabled: false
};
function legacyBindingKeyToCode(key) {
return keyBindingUtils.legacyBindingKeyToCode(key);
}
function legacyKeyCodeToCode(keyCode) {
return keyBindingUtils.legacyKeyCodeToCode(keyCode);
}
function inferBindingCode(binding, fallbackCode) {
return keyBindingUtils.inferBindingCode(binding, fallbackCode);
}
function createDisabledBinding() {
return {
key: null,
keyCode: null,
code: null,
disabled: true
};
}
function normalizeStoredBinding(binding, fallbackKeyCode) {
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
function normalizeStoredBinding(binding, fallbackCode) {
if (!binding) {
return fallbackBinding;
if (!fallbackCode) return null;
return {
code: fallbackCode,
disabled: false
};
}
if (
binding.disabled === true ||
(binding.key === null &&
binding.keyCode === null &&
binding.code === null)
(binding.code === null &&
binding.key === null &&
binding.keyCode === null)
) {
return createDisabledBinding();
}
var normalized = {
key: null,
keyCode: null,
code:
typeof binding.code === "string" && binding.code.length > 0
? binding.code
: null,
disabled: false
};
if (typeof binding.key === "string") {
normalized.key = normalizeBindingKey(binding.key);
}
var legacyKeyCode = getLegacyKeyCode(binding);
if (Number.isInteger(legacyKeyCode)) {
var legacyBinding = legacyKeyCodeToBinding(legacyKeyCode);
if (legacyBinding) {
normalized.key = normalized.key || legacyBinding.key;
normalized.keyCode = legacyKeyCode;
}
}
if (Number.isInteger(binding.keyCode)) {
normalized.keyCode = binding.keyCode;
}
if (!normalized.key && fallbackBinding) {
normalized.key = fallbackBinding.key;
if (normalized.keyCode === null) normalized.keyCode = fallbackBinding.keyCode;
}
if (!normalized.key && !normalized.code && normalized.keyCode === null) {
var normalizedCode = inferBindingCode(binding, fallbackCode);
if (!normalizedCode) {
return null;
}
var normalized = {
code: normalizedCode,
disabled: false
};
return normalized;
}
function formatBindingCode(code) {
if (typeof code !== "string" || code.length === 0) return "";
if (bindingCodeAliases[code]) return bindingCodeAliases[code];
if (/^Key[A-Z]$/.test(code)) return code.substring(3);
if (/^Digit[0-9]$/.test(code)) return code.substring(5);
if (/^F([1-9]|1[0-2])$/.test(code)) return code;
return code;
}
function getBindingLabel(binding) {
if (!binding) return "";
if (binding.disabled) return "";
if (binding.key) {
return displayKeyAliases[binding.key] || binding.key;
}
var legacyKeyCode = getLegacyKeyCode(binding);
if (keyCodeAliases[legacyKeyCode]) return keyCodeAliases[legacyKeyCode];
if (Number.isInteger(legacyKeyCode)) return String.fromCharCode(legacyKeyCode);
return "";
return formatBindingCode(binding.code);
}
function setShortcutInputBinding(input, binding) {
input.vscBinding = binding ? Object.assign({}, binding) : null;
input.keyCode =
binding && Number.isInteger(binding.keyCode) ? binding.keyCode : null;
input.value = getBindingLabel(binding);
}
function captureBindingFromEvent(event) {
var normalizedKey = normalizeBindingKey(event.key);
if (!normalizedKey || modifierKeys.has(normalizedKey)) return null;
if (modifierKeys.has(event.key)) return null;
if (typeof event.code !== "string" || event.code.length === 0) return null;
return {
key: normalizedKey,
keyCode: Number.isInteger(event.keyCode) ? event.keyCode : null,
code: event.code || null,
code: event.code,
disabled: false
};
}
@@ -549,8 +507,10 @@ function recordKeyPress(event) {
}
function inputFilterNumbersOnly(event) {
var char = String.fromCharCode(event.keyCode);
var char = event.key;
if (
typeof char !== "string" ||
char.length !== 1 ||
!/[\d\.]$/.test(char) ||
!/^\d+(\.\d*)?$/.test(event.target.value + char)
) {
@@ -577,7 +537,15 @@ function updateCustomShortcutInputText(inputItem, bindingOrKeyCode) {
return;
}
setShortcutInputBinding(inputItem, legacyKeyCodeToBinding(bindingOrKeyCode));
if (typeof bindingOrKeyCode === "string") {
setShortcutInputBinding(inputItem, { code: bindingOrKeyCode, disabled: false });
return;
}
setShortcutInputBinding(
inputItem,
normalizeStoredBinding({ keyCode: bindingOrKeyCode })
);
}
function appendSelectOptions(select, options) {
@@ -637,23 +605,33 @@ function createKeyBindings(item) {
var input = item.querySelector(".customKey");
var valueInput = item.querySelector(".customValue");
var predefined = !!item.id;
var fallbackKeyCode =
predefined && action === "display"
? tcDefaults.displayKeyCode
: undefined;
var binding = normalizeStoredBinding(input.vscBinding, fallbackKeyCode);
var binding = normalizeStoredBinding(input.vscBinding);
if (!binding) {
if (requiredShortcutActions.has(action)) {
return {
valid: false,
message:
"Error: Shortcut for " +
(actionLabels[action] || action) +
" cannot be empty. Unable to save"
};
}
binding = createDisabledBinding();
}
if (binding.disabled === true && requiredShortcutActions.has(action)) {
return {
valid: false,
message: "Error: Shortcut for " + action + " is invalid. Unable to save"
message:
"Error: Shortcut for " +
(actionLabels[action] || action) +
" cannot be empty. Unable to save"
};
}
keyBindings.push({
action: action,
key: binding.key,
keyCode: binding.keyCode,
code: binding.code,
disabled: binding.disabled === true,
value: customActionsNoValues.includes(action)
@@ -733,8 +711,10 @@ function save_options() {
document.getElementById("controllerLocation").value
);
settings.controllerOpacity =
parseFloat(document.getElementById("controllerOpacity").value) ||
tcDefaults.controllerOpacity;
parseFiniteNumberOrFallback(
document.getElementById("controllerOpacity").value,
tcDefaults.controllerOpacity
);
settings.controllerMarginTop = clampMarginPxInput(
document.getElementById("controllerMarginTop"),
@@ -822,8 +802,10 @@ function save_options() {
if (ruleEl.querySelector(".override-opacity").checked) {
rule.controllerOpacity =
parseFloat(ruleEl.querySelector(".site-controllerOpacity").value) ||
settings.controllerOpacity;
parseFiniteNumberOrFallback(
ruleEl.querySelector(".site-controllerOpacity").value,
settings.controllerOpacity
);
}
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
@@ -863,26 +845,43 @@ function save_options() {
if (ruleEl.querySelector(".override-shortcuts").checked) {
var shortcuts = [];
ruleEl.querySelectorAll(".site-shortcuts-container .customs").forEach((shortcutRow) => {
if (saveError) return;
var action = shortcutRow.dataset.action;
var keyInput = shortcutRow.querySelector(".customKey");
var valueInput = shortcutRow.querySelector(".customValue");
var forceCheckbox = shortcutRow.querySelector(".customForce");
var binding = normalizeStoredBinding(keyInput.vscBinding);
if (binding) {
shortcuts.push({
action: action,
key: binding.key,
keyCode: binding.keyCode,
code: binding.code,
disabled: binding.disabled === true,
value: customActionsNoValues.includes(action)
? 0
: Number(valueInput.value),
force: forceCheckbox ? forceCheckbox.checked : false
});
if (!binding) {
if (requiredShortcutActions.has(action)) {
saveError =
"Error: Site rule shortcut for " +
(actionLabels[action] || action) +
" cannot be empty. Unable to save";
return;
}
binding = createDisabledBinding();
}
if (binding.disabled === true && requiredShortcutActions.has(action)) {
saveError =
"Error: Site rule shortcut for " +
(actionLabels[action] || action) +
" cannot be empty. Unable to save";
return;
}
shortcuts.push({
action: action,
code: binding.code,
disabled: binding.disabled === true,
value: customActionsNoValues.includes(action)
? 0
: Number(valueInput.value),
force: forceCheckbox ? forceCheckbox.checked : false
});
});
if (saveError) return;
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
}
@@ -908,18 +907,7 @@ function save_options() {
function ensureAllDefaultBindings(storage) {
tcDefaults.keyBindings.forEach((binding) => {
// Special case for "display" to support legacy displayKeyCode
if (binding.action === "display" && storage.displayKeyCode) {
ensureDefaultBinding(storage, "display", "V", storage.displayKeyCode, 0);
} else {
ensureDefaultBinding(
storage,
binding.action,
binding.key,
binding.keyCode,
binding.value
);
}
ensureDefaultBinding(storage, binding.action, binding.code, binding.value);
});
}
@@ -1007,9 +995,7 @@ function createSiteRule(rule) {
ruleEl.querySelector(".site-pattern").value = pattern;
// Make the rule body collapsed by default
var ruleBody = ruleEl.querySelector(".site-rule-body");
ruleBody.style.display = "none";
ruleEl.classList.add("collapsed");
setSiteRuleExpandedState(ruleEl, false);
var enabledCheckbox = ruleEl.querySelector(".site-enabled");
var contentEl = ruleEl.querySelector(".site-rule-content");
@@ -1044,114 +1030,99 @@ function createSiteRule(rule) {
];
var hasPlacementOverride =
rule && placementKeys.some(function (k) { return rule[k] !== undefined; });
if (hasPlacementOverride) {
ruleEl.querySelector(".override-placement").checked = true;
ruleEl.querySelector(".site-placement-container").style.display = "block";
}
ruleEl.querySelector(".override-placement").checked = Boolean(hasPlacementOverride);
syncSiteRuleField(ruleEl, rule, "controllerLocation", false);
syncSiteRuleField(ruleEl, rule, "controllerMarginTop", false);
syncSiteRuleField(ruleEl, rule, "controllerMarginBottom", false);
applySiteRuleOverrideState(ruleEl, "override-placement", "site-placement-container");
if (rule && rule.startHidden !== undefined) {
ruleEl.querySelector(".override-visibility").checked = true;
ruleEl.querySelector(".site-visibility-container").style.display = "block";
}
ruleEl.querySelector(".override-visibility").checked = Boolean(
rule && rule.startHidden !== undefined
);
syncSiteRuleField(ruleEl, rule, "startHidden", true);
applySiteRuleOverrideState(ruleEl, "override-visibility", "site-visibility-container");
if (
var hasAutohideOverride = Boolean(
rule &&
(rule.hideWithControls !== undefined ||
rule.hideWithControlsTimer !== undefined)
) {
ruleEl.querySelector(".override-autohide").checked = true;
ruleEl.querySelector(".site-autohide-container").style.display = "block";
}
);
ruleEl.querySelector(".override-autohide").checked = hasAutohideOverride;
syncSiteRuleField(ruleEl, rule, "hideWithControls", true);
syncSiteRuleField(ruleEl, rule, "hideWithControlsTimer", false);
applySiteRuleOverrideState(ruleEl, "override-autohide", "site-autohide-container");
if (
var hasPlaybackOverride = Boolean(
rule &&
(rule.rememberSpeed !== undefined ||
rule.forceLastSavedSpeed !== undefined ||
rule.audioBoolean !== undefined)
) {
ruleEl.querySelector(".override-playback").checked = true;
ruleEl.querySelector(".site-playback-container").style.display = "block";
}
);
ruleEl.querySelector(".override-playback").checked = hasPlaybackOverride;
syncSiteRuleField(ruleEl, rule, "rememberSpeed", true);
syncSiteRuleField(ruleEl, rule, "forceLastSavedSpeed", true);
syncSiteRuleField(ruleEl, rule, "audioBoolean", true);
applySiteRuleOverrideState(ruleEl, "override-playback", "site-playback-container");
if (rule && rule.controllerOpacity !== undefined) {
ruleEl.querySelector(".override-opacity").checked = true;
ruleEl.querySelector(".site-opacity-container").style.display = "block";
}
ruleEl.querySelector(".override-opacity").checked = Boolean(
rule && rule.controllerOpacity !== undefined
);
syncSiteRuleField(ruleEl, rule, "controllerOpacity", false);
applySiteRuleOverrideState(ruleEl, "override-opacity", "site-opacity-container");
if (
var hasSubtitleNudgeOverride = Boolean(
rule &&
(rule.enableSubtitleNudge !== undefined ||
rule.subtitleNudgeInterval !== undefined)
) {
ruleEl.querySelector(".override-subtitleNudge").checked = true;
ruleEl.querySelector(".site-subtitleNudge-container").style.display =
"block";
}
);
ruleEl.querySelector(".override-subtitleNudge").checked = hasSubtitleNudgeOverride;
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
applySiteRuleOverrideState(
ruleEl,
"override-subtitleNudge",
"site-subtitleNudge-container"
);
if (rule && Array.isArray(rule.controllerButtons)) {
ruleEl.querySelector(".override-controlbar").checked = true;
var cbContainer = ruleEl.querySelector(".site-controlbar-container");
cbContainer.style.display = "block";
populateControlBarZones(
ruleEl.querySelector(".site-cb-active"),
ruleEl.querySelector(".site-cb-available"),
rule.controllerButtons
);
}
var hasControlbarOverride = Boolean(rule && Array.isArray(rule.controllerButtons));
ruleEl.querySelector(".override-controlbar").checked = hasControlbarOverride;
populateControlBarZones(
ruleEl.querySelector(".site-cb-active"),
ruleEl.querySelector(".site-cb-available"),
hasControlbarOverride ? rule.controllerButtons : getControlBarOrder()
);
applySiteRuleOverrideState(ruleEl, "override-controlbar", "site-controlbar-container");
if (
var hasPopupControlbarOverride = Boolean(
rule &&
(rule.showPopupControlBar !== undefined ||
Array.isArray(rule.popupControllerButtons))
) {
ruleEl.querySelector(".override-popup-controlbar").checked = true;
var popupCbContainer = ruleEl.querySelector(".site-popup-controlbar-container");
popupCbContainer.style.display = "block";
var sitePopupActive = ruleEl.querySelector(".site-popup-cb-active");
var sitePopupAvailable = ruleEl.querySelector(".site-popup-cb-available");
if (Array.isArray(rule.popupControllerButtons)) {
populateControlBarZones(
sitePopupActive,
sitePopupAvailable,
sanitizePopupButtonOrder(rule.popupControllerButtons),
function (id) {
return !popupExcludedButtonIds.has(id);
}
);
} else if (
sitePopupActive &&
sitePopupAvailable &&
sitePopupActive.children.length === 0
) {
populateControlBarZones(
sitePopupActive,
sitePopupAvailable,
getPopupControlBarOrder(),
function (id) {
return !popupExcludedButtonIds.has(id);
}
);
);
ruleEl.querySelector(".override-popup-controlbar").checked =
hasPopupControlbarOverride;
populateControlBarZones(
ruleEl.querySelector(".site-popup-cb-active"),
ruleEl.querySelector(".site-popup-cb-available"),
hasPopupControlbarOverride && Array.isArray(rule.popupControllerButtons)
? sanitizePopupButtonOrder(rule.popupControllerButtons)
: getPopupControlBarOrder(),
function (id) {
return !popupExcludedButtonIds.has(id);
}
}
);
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
applySiteRuleOverrideState(
ruleEl,
"override-popup-controlbar",
"site-popup-controlbar-container"
);
if (rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0) {
ruleEl.querySelector(".override-shortcuts").checked = true;
var container = ruleEl.querySelector(".site-shortcuts-container");
container.style.display = "block";
var hasShortcutOverride = Boolean(
rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0
);
ruleEl.querySelector(".override-shortcuts").checked = hasShortcutOverride;
var container = ruleEl.querySelector(".site-shortcuts-container");
if (hasShortcutOverride) {
rule.shortcuts.forEach((shortcut) => {
addSiteRuleShortcut(
container,
@@ -1161,13 +1132,41 @@ function createSiteRule(rule) {
shortcut.force
);
});
} else {
populateDefaultSiteShortcuts(container);
}
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
document.getElementById("siteRulesContainer").appendChild(ruleEl);
}
function populateDefaultSiteShortcuts(container) {
tcDefaults.keyBindings.forEach((binding) => {
var bindings = [];
document.querySelectorAll("#customs .shortcut-row").forEach((row) => {
var action = row.dataset.action;
if (!action) return;
var keyInput = row.querySelector(".customKey");
var binding = normalizeStoredBinding(keyInput && keyInput.vscBinding);
if (!binding) return;
var valueInput = row.querySelector(".customValue");
bindings.push({
action: action,
code: binding.code,
disabled: binding.disabled === true,
value: customActionsNoValues.includes(action)
? 0
: Number(valueInput && valueInput.value),
force: false
});
});
if (bindings.length === 0) {
bindings = tcDefaults.keyBindings.slice();
}
bindings.forEach((binding) => {
addSiteRuleShortcut(container, binding.action, binding, binding.value, false);
});
}
@@ -1200,8 +1199,8 @@ function createControlBarBlock(buttonId) {
}
function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) {
activeZone.innerHTML = "";
availableZone.innerHTML = "";
vscClearElement(activeZone);
vscClearElement(availableZone);
var allowed = function (id) {
if (!controllerButtonDefs[id]) return false;
@@ -1394,7 +1393,7 @@ function initLucideButtonIconsUI() {
if (!actionSel.dataset.lucideInit) {
actionSel.dataset.lucideInit = "1";
actionSel.innerHTML = "";
vscClearElement(actionSel);
Object.keys(controllerButtonDefs).forEach(function (aid) {
if (aid === "nudge") {
Object.keys(lucideSubtitleNudgeActionLabels).forEach(function (subId) {
@@ -1415,7 +1414,7 @@ function initLucideButtonIconsUI() {
}
function renderResults(slugs) {
resultsEl.innerHTML = "";
vscClearElement(resultsEl);
slugs.forEach(function (slug) {
var b = document.createElement("button");
b.type = "button";
@@ -1450,11 +1449,13 @@ function initLucideButtonIconsUI() {
.then(function (txt) {
var safe = sanitizeLucideSvg(txt);
if (!safe) throw new Error("Bad SVG");
previewEl.innerHTML = safe;
if (!vscSetSvgContent(previewEl, safe)) {
throw new Error("Preview render failed");
}
setLucideStatus("Preview: " + slug);
})
.catch(function (e) {
previewEl.innerHTML = "";
vscClearElement(previewEl);
setLucideStatus(
"Could not load: " + slug + " — " + e.message
);
@@ -1473,7 +1474,7 @@ function initLucideButtonIconsUI() {
.then(function (map) {
var q = searchInput.value;
if (!q.trim()) {
resultsEl.innerHTML = "";
vscClearElement(resultsEl);
return;
}
renderResults(searchLucideSlugs(map, q, 48));
@@ -1638,7 +1639,7 @@ function restore_options() {
? storage.siteRules
: tcDefaults.siteRules || [];
document.getElementById("siteRulesContainer").innerHTML = "";
vscClearElement(document.getElementById("siteRulesContainer"));
if (siteRules.length > 0) {
siteRules.forEach((rule) => {
if (rule && rule.pattern) {
@@ -1737,29 +1738,26 @@ document.addEventListener("DOMContentLoaded", function () {
eventCaller(event, "customKey", recordKeyPress)
);
document.addEventListener("click", (event) => {
if (event.target.classList.contains("removeParent")) {
event.target.parentNode.remove();
var target = event.target;
var targetEl = target && target.closest ? target : target.parentElement;
if (!targetEl) return;
var removeParentButton = targetEl.closest(".removeParent");
if (removeParentButton) {
removeParentButton.parentNode.remove();
refreshAddShortcutSelector();
return;
}
if (event.target.classList.contains("remove-site-rule")) {
event.target.closest(".site-rule").remove();
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
if (removeSiteRuleButton) {
removeSiteRuleButton.closest(".site-rule").remove();
return;
}
if (event.target.classList.contains("toggle-site-rule")) {
var ruleEl = event.target.closest(".site-rule");
var ruleBody = ruleEl.querySelector(".site-rule-body");
var toggleButton = targetEl.closest(".toggle-site-rule");
if (toggleButton) {
var ruleEl = toggleButton.closest(".site-rule");
var isCollapsed = ruleEl.classList.contains("collapsed");
if (isCollapsed) {
ruleBody.style.display = "block";
ruleEl.classList.remove("collapsed");
event.target.textContent = "\u2212";
} else {
ruleBody.style.display = "none";
ruleEl.classList.add("collapsed");
event.target.textContent = "\u002b";
}
setSiteRuleExpandedState(ruleEl, isCollapsed);
return;
}
});
@@ -1781,7 +1779,10 @@ document.addEventListener("DOMContentLoaded", function () {
"override-autohide": "site-autohide-container",
"override-playback": "site-playback-container",
"override-opacity": "site-opacity-container",
"override-subtitleNudge": "site-subtitleNudge-container"
"override-subtitleNudge": "site-subtitleNudge-container",
"override-controlbar": "site-controlbar-container",
"override-popup-controlbar": "site-popup-controlbar-container",
"override-shortcuts": "site-shortcuts-container"
};
for (var ocb in siteOverrideContainers) {
if (event.target.classList.contains(ocb)) {
@@ -1790,78 +1791,10 @@ document.addEventListener("DOMContentLoaded", function () {
"." + siteOverrideContainers[ocb]
);
if (targetBox) {
targetBox.style.display = event.target.checked ? "block" : "none";
setSiteOverrideContainerState(targetBox, event.target.checked);
}
return;
}
}
// Handle site rule override checkboxes
if (event.target.classList.contains("override-shortcuts")) {
var container = event.target
.closest(".site-rule-shortcuts")
.querySelector(".site-shortcuts-container");
if (event.target.checked) {
container.style.display = "block";
if (container.children.length === 0) {
populateDefaultSiteShortcuts(container);
}
} else {
container.style.display = "none";
}
}
if (event.target.classList.contains("override-controlbar")) {
var cbContainer = event.target
.closest(".site-rule-controlbar")
.querySelector(".site-controlbar-container");
if (event.target.checked) {
cbContainer.style.display = "block";
var activeZone = cbContainer.querySelector(".site-cb-active");
var availableZone = cbContainer.querySelector(".site-cb-available");
if (
activeZone &&
availableZone &&
activeZone.children.length === 0 &&
availableZone.children.length === 0
) {
populateControlBarZones(
activeZone,
availableZone,
getControlBarOrder()
);
}
} else {
cbContainer.style.display = "none";
}
}
if (event.target.classList.contains("override-popup-controlbar")) {
var popupCbContainer = event.target
.closest(".site-rule-controlbar")
.querySelector(".site-popup-controlbar-container");
if (event.target.checked) {
popupCbContainer.style.display = "block";
var popupActiveZone = popupCbContainer.querySelector(".site-popup-cb-active");
var popupAvailableZone = popupCbContainer.querySelector(".site-popup-cb-available");
if (
popupActiveZone &&
popupAvailableZone &&
popupActiveZone.children.length === 0 &&
popupAvailableZone.children.length === 0
) {
populateControlBarZones(
popupActiveZone,
popupAvailableZone,
getPopupControlBarOrder(),
function (id) {
return !popupExcludedButtonIds.has(id);
}
);
}
} else {
popupCbContainer.style.display = "none";
}
}
});
});
+2128
View File
@@ -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"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "speeder",
"private": true,
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"jsdom": "^26.1.0",
"vitest": "^3.2.4"
}
}
+2
View File
@@ -4,6 +4,8 @@
<meta charset="utf-8" />
<title>Speeder</title>
<link rel="stylesheet" href="popup.css" />
<script src="shared/site-rules.js"></script>
<script src="shared/popup-controls.js"></script>
<script src="ui-icons.js"></script>
<script src="popup.js"></script>
</head>
+24 -86
View File
@@ -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() {
@@ -230,17 +164,21 @@ document.addEventListener("DOMContentLoaded", function () {
btn.dataset.action = btnId;
var customEntry = customMap[btnId];
if (customEntry && customEntry.svg) {
var customSpan = document.createElement("span");
customSpan.className = "vsc-btn-icon";
customSpan.innerHTML = customEntry.svg;
btn.appendChild(customSpan);
var customSpan = vscCreateSvgWrap(document, customEntry.svg, "vsc-btn-icon");
if (customSpan) {
btn.appendChild(customSpan);
} else {
btn.textContent = def.label || "?";
}
} else if (typeof vscIconSvgString === "function") {
var svgStr = vscIconSvgString(btnId, 16);
if (svgStr) {
var iconSpan = document.createElement("span");
iconSpan.className = "vsc-btn-icon";
iconSpan.innerHTML = svgStr;
btn.appendChild(iconSpan);
var iconSpan = vscCreateSvgWrap(document, svgStr, "vsc-btn-icon");
if (iconSpan) {
btn.appendChild(iconSpan);
} else {
btn.textContent = def.label || "?";
}
} else {
btn.textContent = def.label || "?";
}
+644
View File
@@ -0,0 +1,644 @@
(function (global) {
"use strict";
var SITE_RULES_DIFF_FORMAT = "defaults-diff-v1";
var DEFAULT_BUTTONS = ["rewind", "slower", "faster", "advance", "display"];
var SITE_RULE_OVERRIDE_KEYS = [
"controllerLocation",
"controllerMarginTop",
"controllerMarginBottom",
"startHidden",
"hideWithControls",
"hideWithControlsTimer",
"rememberSpeed",
"forceLastSavedSpeed",
"audioBoolean",
"controllerOpacity",
"enableSubtitleNudge",
"subtitleNudgeInterval",
"controllerButtons",
"showPopupControlBar",
"popupControllerButtons",
"shortcuts",
"preferredSpeed"
];
var DIFFABLE_OPTION_KEYS = [
"rememberSpeed",
"forceLastSavedSpeed",
"audioBoolean",
"enabled",
"startHidden",
"hideWithControls",
"hideWithControlsTimer",
"controllerLocation",
"controllerOpacity",
"controllerMarginTop",
"controllerMarginBottom",
"keyBindings",
"siteRules",
"siteRulesMeta",
"siteRulesFormat",
"controllerButtons",
"showPopupControlBar",
"popupMatchHoverControls",
"popupControllerButtons",
"enableSubtitleNudge",
"subtitleNudgeInterval",
"subtitleNudgeAmount"
];
var MANAGED_SYNC_KEYS = DIFFABLE_OPTION_KEYS.concat([
"hideWithYouTubeControls"
]);
var DEFAULT_SETTINGS = {
speed: 1.0,
lastSpeed: 1.0,
displayKeyCode: 86,
rememberSpeed: false,
audioBoolean: false,
startHidden: false,
hideWithYouTubeControls: false,
hideWithControls: false,
hideWithControlsTimer: 2.0,
controllerLocation: "top-left",
forceLastSavedSpeed: false,
enabled: true,
controllerOpacity: 0.3,
controllerMarginTop: 0,
controllerMarginRight: 0,
controllerMarginBottom: 65,
controllerMarginLeft: 0,
keyBindings: [
{
action: "display",
key: "V",
keyCode: 86,
code: null,
disabled: false,
value: 0,
force: false,
predefined: true
},
{
action: "move",
key: "P",
keyCode: 80,
code: null,
disabled: false,
value: 0,
force: false,
predefined: true
},
{
action: "slower",
key: "S",
keyCode: 83,
code: null,
disabled: false,
value: 0.1,
force: false,
predefined: true
},
{
action: "faster",
key: "D",
keyCode: 68,
code: null,
disabled: false,
value: 0.1,
force: false,
predefined: true
},
{
action: "rewind",
key: "Z",
keyCode: 90,
code: null,
disabled: false,
value: 10,
force: false,
predefined: true
},
{
action: "advance",
key: "X",
keyCode: 88,
code: null,
disabled: false,
value: 10,
force: false,
predefined: true
},
{
action: "reset",
key: "R",
keyCode: 82,
code: null,
disabled: false,
value: 0,
force: false,
predefined: true
},
{
action: "fast",
key: "G",
keyCode: 71,
code: null,
disabled: false,
value: 1.8,
force: false,
predefined: true
},
{
action: "toggleSubtitleNudge",
key: "N",
keyCode: 78,
code: null,
disabled: false,
value: 0,
force: false,
predefined: true
}
],
siteRules: [
{
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/(?!shorts\\/).*/",
enabled: true,
enableSubtitleNudge: true,
subtitleNudgeInterval: 50
},
{
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
enabled: true,
rememberSpeed: true,
controllerMarginTop: 60,
controllerMarginBottom: 85
}
],
controllerButtons: DEFAULT_BUTTONS.slice(),
showPopupControlBar: true,
popupMatchHoverControls: true,
popupControllerButtons: DEFAULT_BUTTONS.slice(),
enableSubtitleNudge: false,
subtitleNudgeInterval: 50,
subtitleNudgeAmount: 0.001
};
function clonePlainData(value) {
if (value === undefined) {
return undefined;
}
return JSON.parse(JSON.stringify(value));
}
function hasOwn(obj, key) {
return Boolean(obj) && Object.prototype.hasOwnProperty.call(obj, key);
}
function isPlainObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function sortComparableValue(value) {
if (Array.isArray(value)) {
return value.map(sortComparableValue);
}
if (isPlainObject(value)) {
var sorted = {};
Object.keys(value)
.sort()
.forEach(function (key) {
if (value[key] === undefined) {
return;
}
sorted[key] = sortComparableValue(value[key]);
});
return sorted;
}
return value;
}
function areComparableValuesEqual(a, b) {
return (
JSON.stringify(sortComparableValue(a)) ===
JSON.stringify(sortComparableValue(b))
);
}
function deepMergeDefaults(defaults, overrides) {
if (Array.isArray(defaults)) {
return Array.isArray(overrides)
? clonePlainData(overrides)
: clonePlainData(defaults);
}
if (isPlainObject(defaults)) {
var result = clonePlainData(defaults) || {};
if (!isPlainObject(overrides)) {
return result;
}
Object.keys(overrides).forEach(function (key) {
if (overrides[key] === undefined) {
return;
}
if (hasOwn(defaults, key)) {
result[key] = deepMergeDefaults(defaults[key], overrides[key]);
} else {
result[key] = clonePlainData(overrides[key]);
}
});
return result;
}
return overrides === undefined
? clonePlainData(defaults)
: clonePlainData(overrides);
}
function deepDiff(current, defaults) {
if (current === undefined) {
return undefined;
}
if (Array.isArray(current)) {
return areComparableValuesEqual(current, defaults)
? undefined
: clonePlainData(current);
}
if (isPlainObject(current)) {
var result = {};
Object.keys(current).forEach(function (key) {
var diff = deepDiff(current[key], defaults && defaults[key]);
if (diff !== undefined) {
result[key] = diff;
}
});
return Object.keys(result).length > 0 ? result : undefined;
}
return areComparableValuesEqual(current, defaults)
? undefined
: clonePlainData(current);
}
function getDefaultSiteRules() {
return clonePlainData(DEFAULT_SETTINGS.siteRules) || [];
}
function getDefaultSiteRulesByPattern() {
var map = Object.create(null);
getDefaultSiteRules().forEach(function (rule) {
if (!rule || typeof rule.pattern !== "string" || !rule.pattern) {
return;
}
map[rule.pattern] = rule;
});
return map;
}
function normalizeSiteRuleForDiff(rule, baseSettings) {
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
return null;
}
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
if (!pattern) {
return null;
}
var normalized = { pattern: pattern };
var baseEnabled = hasOwn(baseSettings, "enabled")
? Boolean(baseSettings.enabled)
: true;
var ruleEnabled = hasOwn(rule, "enabled")
? Boolean(rule.enabled)
: hasOwn(rule, "disableExtension")
? !Boolean(rule.disableExtension)
: baseEnabled;
if (!areComparableValuesEqual(ruleEnabled, baseEnabled)) {
normalized.enabled = ruleEnabled;
}
SITE_RULE_OVERRIDE_KEYS.forEach(function (key) {
var baseValue = clonePlainData(baseSettings[key]);
var effectiveValue = hasOwn(rule, key)
? clonePlainData(rule[key])
: baseValue;
if (!areComparableValuesEqual(effectiveValue, baseValue)) {
normalized[key] = effectiveValue;
}
});
Object.keys(rule).forEach(function (key) {
if (
key === "pattern" ||
key === "enabled" ||
key === "disableExtension" ||
SITE_RULE_OVERRIDE_KEYS.indexOf(key) !== -1 ||
rule[key] === undefined
) {
return;
}
normalized[key] = clonePlainData(rule[key]);
});
return normalized;
}
function compressSiteRules(siteRules, baseSettings) {
if (!Array.isArray(siteRules)) {
return {};
}
var defaultRules = getDefaultSiteRules();
var defaultRulesByPattern = getDefaultSiteRulesByPattern();
var currentPatterns = new Set();
var exportRules = [];
siteRules.forEach(function (rule) {
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
return;
}
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
if (pattern) {
currentPatterns.add(pattern);
}
var normalizedRule = normalizeSiteRuleForDiff(rule, baseSettings);
if (!normalizedRule || Object.keys(normalizedRule).length === 1) {
return;
}
var defaultRule = pattern ? defaultRulesByPattern[pattern] : null;
var normalizedDefaultRule = defaultRule
? normalizeSiteRuleForDiff(defaultRule, baseSettings)
: null;
if (normalizedDefaultRule) {
if (areComparableValuesEqual(normalizedRule, normalizedDefaultRule)) {
return;
}
var defaultRuleDiff = deepDiff(normalizedRule, normalizedDefaultRule);
if (defaultRuleDiff && Object.keys(defaultRuleDiff).length > 0) {
defaultRuleDiff.pattern = pattern;
exportRules.push(defaultRuleDiff);
}
return;
}
exportRules.push(normalizedRule);
});
var removedDefaultPatterns = defaultRules
.map(function (rule) {
return rule && typeof rule.pattern === "string" ? rule.pattern : "";
})
.filter(function (pattern) {
return pattern && !currentPatterns.has(pattern);
});
var result = {};
if (exportRules.length > 0) {
result.siteRules = exportRules;
result.siteRulesFormat = SITE_RULES_DIFF_FORMAT;
}
if (removedDefaultPatterns.length > 0) {
result.siteRulesMeta = {
removedDefaultPatterns: removedDefaultPatterns
};
result.siteRulesFormat = SITE_RULES_DIFF_FORMAT;
}
return result;
}
function expandSiteRules(siteRules, siteRulesMeta) {
var defaultRules = getDefaultSiteRules();
var defaultRulesByPattern = getDefaultSiteRulesByPattern();
if (defaultRules.length === 0) {
return Array.isArray(siteRules) ? clonePlainData(siteRules) : [];
}
var removedDefaultPatterns = new Set(
siteRulesMeta && Array.isArray(siteRulesMeta.removedDefaultPatterns)
? siteRulesMeta.removedDefaultPatterns
: []
);
var modifiedDefaultRules = Object.create(null);
var customRules = [];
if (Array.isArray(siteRules)) {
siteRules.forEach(function (rule) {
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
return;
}
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
if (
pattern &&
Object.prototype.hasOwnProperty.call(defaultRulesByPattern, pattern)
) {
modifiedDefaultRules[pattern] = clonePlainData(rule);
return;
}
customRules.push(clonePlainData(rule));
});
}
var mergedRules = [];
defaultRules.forEach(function (rule) {
var pattern = rule && typeof rule.pattern === "string" ? rule.pattern : "";
if (!pattern || removedDefaultPatterns.has(pattern)) {
return;
}
if (modifiedDefaultRules[pattern]) {
mergedRules.push(
Object.assign(
{},
clonePlainData(rule),
clonePlainData(modifiedDefaultRules[pattern])
)
);
return;
}
mergedRules.push(clonePlainData(rule));
});
customRules.forEach(function (rule) {
mergedRules.push(rule);
});
return mergedRules;
}
function buildStoredSettingsDiff(currentSettings) {
var defaults = clonePlainData(DEFAULT_SETTINGS);
var normalized = deepMergeDefaults(defaults, currentSettings || {});
var siteRuleData = compressSiteRules(normalized.siteRules, normalized);
var diffDefaults = {};
var diff = {};
delete normalized.siteRules;
delete normalized.siteRulesMeta;
delete normalized.siteRulesFormat;
delete normalized.hideWithYouTubeControls;
if (siteRuleData.siteRules) {
normalized.siteRules = siteRuleData.siteRules;
}
if (siteRuleData.siteRulesMeta) {
normalized.siteRulesMeta = siteRuleData.siteRulesMeta;
}
if (siteRuleData.siteRulesFormat) {
normalized.siteRulesFormat = siteRuleData.siteRulesFormat;
}
DIFFABLE_OPTION_KEYS.forEach(function (key) {
if (hasOwn(DEFAULT_SETTINGS, key)) {
diffDefaults[key] = clonePlainData(DEFAULT_SETTINGS[key]);
}
if (!hasOwn(normalized, key)) {
return;
}
var valueDiff = deepDiff(normalized[key], diffDefaults[key]);
if (valueDiff !== undefined) {
diff[key] = valueDiff;
}
});
return diff;
}
function expandStoredSettings(storage) {
var raw = clonePlainData(storage) || {};
var expanded = deepMergeDefaults(DEFAULT_SETTINGS, raw);
if (
!hasOwn(raw, "hideWithControls") &&
hasOwn(raw, "hideWithYouTubeControls")
) {
expanded.hideWithControls = Boolean(raw.hideWithYouTubeControls);
}
expanded.hideWithYouTubeControls = expanded.hideWithControls;
if (raw.siteRulesFormat === SITE_RULES_DIFF_FORMAT) {
expanded.siteRules = expandSiteRules(raw.siteRules, raw.siteRulesMeta);
} else if (Array.isArray(raw.siteRules)) {
expanded.siteRules = clonePlainData(raw.siteRules);
} else {
expanded.siteRules = getDefaultSiteRules();
}
return expanded;
}
function escapeStringRegExp(str) {
var matcher = /[|\\{}()[\]^$+*?.]/g;
return String(str).replace(matcher, "\\$&");
}
function siteRuleMatchesUrl(rule, currentUrl) {
if (!rule || !rule.pattern || !currentUrl) {
return false;
}
var pattern = String(rule.pattern).trim();
if (!pattern) {
return false;
}
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 (_error) {
return false;
}
} else {
regex = new RegExp(escapeStringRegExp(pattern));
}
return Boolean(regex && regex.test(currentUrl));
}
function mergeMatchingSiteRules(currentUrl, siteRules) {
if (!currentUrl || !Array.isArray(siteRules)) {
return null;
}
var matchedRules = [];
for (var i = 0; i < siteRules.length; i++) {
if (siteRuleMatchesUrl(siteRules[i], currentUrl)) {
matchedRules.push(siteRules[i]);
}
}
if (!matchedRules.length) {
return null;
}
var mergedRule = {};
matchedRules.forEach(function (rule) {
Object.keys(rule).forEach(function (key) {
var value = rule[key];
if (Array.isArray(value)) {
mergedRule[key] = clonePlainData(value);
return;
}
if (isPlainObject(value)) {
mergedRule[key] = clonePlainData(value);
return;
}
mergedRule[key] = value;
});
});
return mergedRule;
}
function isSiteRuleDisabled(rule) {
return Boolean(
rule &&
(
rule.enabled === false ||
(typeof rule.enabled === "undefined" && rule.disableExtension === true)
)
);
}
global.vscClonePlainData = clonePlainData;
global.vscAreComparableValuesEqual = areComparableValuesEqual;
global.vscDeepMergeDefaults = deepMergeDefaults;
global.vscBuildStoredSettingsDiff = buildStoredSettingsDiff;
global.vscExpandStoredSettings = expandStoredSettings;
global.vscGetSettingsDefaults = function () {
return clonePlainData(DEFAULT_SETTINGS);
};
global.vscGetManagedSyncKeys = function () {
return MANAGED_SYNC_KEYS.slice();
};
global.vscGetSiteRulesDiffFormat = function () {
return SITE_RULES_DIFF_FORMAT;
};
global.vscMatchSiteRule = mergeMatchingSiteRules;
global.vscSiteRuleMatchesUrl = siteRuleMatchesUrl;
global.vscIsSiteRuleDisabled = isSiteRuleDisabled;
})(typeof globalThis !== "undefined" ? globalThis : this);
+55
View File
@@ -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
};
});
+124
View File
@@ -0,0 +1,124 @@
(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() {
var rawSettingsKeys = new Set([
"audioBoolean",
"controllerButtons",
"controllerLocation",
"controllerMarginBottom",
"controllerMarginLeft",
"controllerMarginRight",
"controllerMarginTop",
"controllerOpacity",
"enableSubtitleNudge",
"enabled",
"forceLastSavedSpeed",
"hideWithControls",
"hideWithControlsTimer",
"hideWithYouTubeControls",
"keyBindings",
"lastSpeed",
"popupControllerButtons",
"popupMatchHoverControls",
"rememberSpeed",
"showPopupControlBar",
"siteRules",
"speed",
"startHidden",
"subtitleNudgeAmount",
"subtitleNudgeInterval"
]);
function isRecognizedRawSettingsObject(backup) {
if (!backup || typeof backup !== "object" || Array.isArray(backup)) {
return false;
}
return Object.keys(backup).some(function(key) {
return rawSettingsKeys.has(key);
});
}
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" &&
isRecognizedRawSettingsObject(backup)
) {
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,
isRecognizedRawSettingsObject: isRecognizedRawSettingsObject,
parseImportText: parseImportText
};
});
+122
View File
@@ -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
};
});
+85
View File
@@ -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
};
});
+69
View File
@@ -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
};
});
+164
View File
@@ -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;
}
+240
View File
@@ -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
};
+276
View File
@@ -0,0 +1,276 @@
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("imports raw settings objects without touching local storage", async () => {
vi.useFakeTimers();
const chrome = await setupImportExport({
local: { customButtonIcons: { faster: { slug: "rocket" } } }
});
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: false,
siteRules: [{ pattern: "example.com", enabled: false }]
})
}
]
}
});
expect(chrome.storage.local.clear).not.toHaveBeenCalled();
expect(chrome.storage.local.set).not.toHaveBeenCalled();
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
{
enabled: false,
siteRules: [{ pattern: "example.com", enabled: false }]
},
expect.any(Function)
);
});
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({ wat: true })
}
]
}
});
expect(document.getElementById("status").textContent).toBe(
"Error: Invalid backup file format"
);
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
});
});
+189
View File
@@ -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(`<!doctype html><html><body>
<button id="exportSettings">Export</button>
<button id="importSettings">Import</button>
<div id="status"></div>
</body></html>`);
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: "<svg></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: "<svg></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: "<svg></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"
);
});
});
+141
View File
@@ -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("<!doctype html><html><body></body></html>");
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);
});
});
+90
View File
@@ -0,0 +1,90 @@
import { describe, expect, it, vi } from "vitest";
import { createChromeMock, flushAsyncWork, loadScript } from "./helpers/browser.js";
function loadBlankDocument() {
document.open();
document.write("<!doctype html><html><body></body></html>");
document.close();
}
async function bootInject({ sync = {}, local = {} } = {}) {
loadBlankDocument();
globalThis.chrome = createChromeMock({ sync, local });
window.chrome = globalThis.chrome;
globalThis.chrome.runtime.onMessage = {
addListener: vi.fn()
};
const originalSyncGet = globalThis.chrome.storage.sync.get;
const originalLocalGet = globalThis.chrome.storage.local.get;
globalThis.chrome.storage.sync.get = vi.fn((keys, callback) => {
Promise.resolve().then(() => originalSyncGet(keys, callback));
});
globalThis.chrome.storage.local.get = vi.fn((keys, callback) => {
Promise.resolve().then(() => originalLocalGet(keys, callback));
});
globalThis.requestIdleCallback = (callback, options) =>
setTimeout(
() =>
callback({
didTimeout: false,
timeRemaining() {
return 1;
}
}),
(options && options.timeout) || 0
);
globalThis.cancelIdleCallback = (id) => clearTimeout(id);
loadScript("shared/controller-utils.js");
loadScript("shared/key-bindings.js");
loadScript("shared/site-rules.js");
loadScript("ui-icons.js");
loadScript("inject.js");
for (let i = 0; i < 3; i += 1) {
await flushAsyncWork();
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
describe("inject runtime", () => {
it("keeps subtitle nudge disabled when the effective setting is off", async () => {
await bootInject({
sync: {
enableSubtitleNudge: false
}
});
const stopSubtitleNudge = vi.fn();
const startSubtitleNudge = vi.fn();
const flashEl = document.createElement("span");
const video = {
paused: false,
playbackRate: 1.5,
vsc: {
stopSubtitleNudge,
startSubtitleNudge,
subtitleNudgeEnabledOverride: null,
subtitleNudgeIndicator: null,
nudgeFlashIndicator: flashEl
}
};
expect(window.tc.settings.enableSubtitleNudge).toBe(false);
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(false);
expect(video.vsc.subtitleNudgeEnabledOverride).toBeNull();
expect(stopSubtitleNudge).toHaveBeenCalledTimes(1);
expect(startSubtitleNudge).not.toHaveBeenCalled();
expect(flashEl.classList.contains("visible")).toBe(false);
window.tc.settings.enableSubtitleNudge = true;
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(true);
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(true);
window.tc.settings.enableSubtitleNudge = false;
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
await new Promise((resolve) => setTimeout(resolve, 0));
});
});
+61
View File
@@ -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("<!doctype html><html><body></body></html>");
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("<!doctype html><html><body></body></html>");
evaluateScript("ui-icons.js");
evaluateScript("lucide-client.js");
const sanitized = window.sanitizeLucideSvg(`
<svg width="10" height="10" onclick="evil()">
<script>alert(1)</script>
<foreignObject>bad</foreignObject>
<path d="M0 0h10v10"></path>
</svg>
`);
expect(sanitized).toContain("<svg");
expect(sanitized).not.toContain("onclick");
expect(sanitized).not.toContain("<script");
expect(sanitized).not.toContain("foreignObject");
expect(sanitized).toContain('width="100%"');
});
it("searches and ranks icon slugs by query", () => {
loadHtmlString("<!doctype html><html><body></body></html>");
evaluateScript("ui-icons.js");
evaluateScript("lucide-client.js");
const results = window.searchLucideSlugs(
{
alarm: ["clock", "time"],
"badge-alert": ["alert", "warning"],
calendar: ["date", "time"]
},
"al",
10
);
expect(results).toEqual(["alarm", "badge-alert", "calendar"]);
});
});
+194
View File
@@ -0,0 +1,194 @@
import {
createChromeMock,
flushAsyncWork,
loadHtml,
loadScript,
triggerDomContentLoaded
} from "./helpers/browser.js";
async function setupOptions(overrides = {}) {
loadHtml("options.html");
globalThis.chrome = createChromeMock(overrides);
window.chrome = globalThis.chrome;
globalThis.fetch = vi.fn();
loadScript("shared/controller-utils.js");
loadScript("shared/key-bindings.js");
loadScript("shared/popup-controls.js");
loadScript("ui-icons.js");
loadScript("lucide-client.js");
loadScript("options.js");
triggerDomContentLoaded();
await flushAsyncWork();
return globalThis.chrome;
}
describe("options page", () => {
it("restores stored settings, custom shortcuts, and site rules", async () => {
await setupOptions({
manifestVersion: "5.1.7.0",
sync: {
rememberSpeed: true,
enabled: false,
popupMatchHoverControls: false,
popupControllerButtons: ["rewind", "settings", "advance", "advance"],
keyBindings: [
{ action: "display", code: "KeyV", value: 0, predefined: true },
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
],
siteRules: [
{
pattern: "youtube.com",
enabled: true,
showPopupControlBar: false,
popupControllerButtons: ["advance", "settings", "advance"]
}
]
}
});
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
expect(document.getElementById("rememberSpeed").checked).toBe(true);
expect(document.getElementById("enabled").checked).toBe(false);
expect(document.querySelector('.shortcut-row[data-action="pause"]')).not.toBe(
null
);
expect(document.getElementById("siteRulesContainer").children.length).toBe(
1
);
expect(globalThis.getPopupControlBarOrder()).toEqual(["rewind", "advance"]);
});
it("validates site rule regexes before saving", async () => {
const chrome = await setupOptions();
chrome.storage.sync.set.mockClear();
globalThis.createSiteRule(null);
const rule = document.querySelector(".site-rule");
rule.querySelector(".site-pattern").value = "/(/";
globalThis.save_options();
expect(document.getElementById("status").textContent).toContain(
"Invalid site rule regex"
);
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
});
it("shows a more-menu trigger for collapsed site rules and a collapse trigger when open", async () => {
await setupOptions({ sync: { siteRules: [] } });
globalThis.createSiteRule({ pattern: "youtube.com" });
const rule = document.getElementById("siteRulesContainer").lastElementChild;
const toggle = rule.querySelector(".toggle-site-rule");
const body = rule.querySelector(".site-rule-body");
expect(rule.classList.contains("collapsed")).toBe(true);
expect(body.style.display).toBe("none");
expect(toggle.getAttribute("aria-expanded")).toBe("false");
expect(toggle.getAttribute("aria-label")).toBe("Expand site rule");
expect(toggle.querySelector("svg")).not.toBeNull();
globalThis.setSiteRuleExpandedState(rule, true);
expect(rule.classList.contains("collapsed")).toBe(false);
expect(body.style.display).toBe("block");
expect(toggle.getAttribute("aria-expanded")).toBe("true");
expect(toggle.getAttribute("aria-label")).toBe("Collapse site rule");
});
it("keeps site override settings visible but disabled until enabled", async () => {
await setupOptions({ sync: { siteRules: [] } });
globalThis.createSiteRule({ pattern: "youtube.com" });
const rule = document.getElementById("siteRulesContainer").lastElementChild;
const playbackOverride = rule.querySelector(".override-playback");
const playbackContainer = rule.querySelector(".site-playback-container");
const rememberSpeed = rule.querySelector(".site-rememberSpeed");
expect(playbackContainer.classList.contains("site-override-disabled")).toBe(
true
);
expect(rememberSpeed.disabled).toBe(true);
playbackOverride.checked = true;
playbackOverride.dispatchEvent(
new Event("change", {
bubbles: true
})
);
expect(playbackContainer.classList.contains("site-override-disabled")).toBe(
false
);
expect(rememberSpeed.disabled).toBe(false);
});
it("saves normalized settings and sanitized popup/site-rule controls", async () => {
const chrome = await setupOptions();
document.getElementById("rememberSpeed").checked = true;
document.getElementById("hideWithControlsTimer").value = "20";
document.getElementById("controllerOpacity").value = "0";
document.getElementById("controllerMarginTop").value = "250";
document.getElementById("controllerMarginBottom").value = "-4";
document.getElementById("enableSubtitleNudge").checked = true;
document.getElementById("subtitleNudgeInterval").value = "5";
document.getElementById("popupMatchHoverControls").checked = false;
document.getElementById("showPopupControlBar").checked = false;
globalThis.populatePopupControlBarEditor([
"rewind",
"settings",
"faster",
"faster"
]);
globalThis.createSiteRule(null);
const rule = document.querySelector(".site-rule");
rule.querySelector(".site-pattern").value = "youtube.com";
rule.querySelector(".override-playback").checked = true;
rule.querySelector(".site-rememberSpeed").checked = true;
rule.querySelector(".override-opacity").checked = true;
rule.querySelector(".site-controllerOpacity").value = "0";
rule.querySelector(".override-popup-controlbar").checked = true;
rule.querySelector(".site-showPopupControlBar").checked = false;
globalThis.populateControlBarZones(
rule.querySelector(".site-popup-cb-active"),
rule.querySelector(".site-popup-cb-available"),
["advance", "settings", "advance"],
function (id) {
return id !== "settings";
}
);
globalThis.save_options();
expect(chrome.storage.sync.remove).toHaveBeenCalled();
const savedSettings =
chrome.storage.sync.set.mock.calls[
chrome.storage.sync.set.mock.calls.length - 1
][0];
expect(savedSettings.rememberSpeed).toBe(true);
expect(savedSettings.hideWithControlsTimer).toBe(15);
expect(savedSettings.controllerOpacity).toBe(0);
expect(savedSettings.controllerMarginTop).toBe(200);
expect(savedSettings.controllerMarginBottom).toBe(0);
expect(savedSettings.subtitleNudgeInterval).toBe(10);
expect(savedSettings.showPopupControlBar).toBe(false);
expect(savedSettings.popupMatchHoverControls).toBe(false);
expect(savedSettings.popupControllerButtons).toEqual(["rewind", "faster"]);
expect(savedSettings.siteRules).toEqual(
expect.arrayContaining([
expect.objectContaining({
pattern: "youtube.com",
rememberSpeed: true,
controllerOpacity: 0,
showPopupControlBar: false,
popupControllerButtons: ["advance"]
})
])
);
});
});
+230
View File
@@ -0,0 +1,230 @@
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
const {
createChromeMock,
evaluateScript,
fireDOMContentLoaded,
flushAsyncWork,
installCommonWindowMocks,
loadHtmlFile
} = require("./helpers/extension-test-utils");
function bootOptions(options) {
const config = options || {};
loadHtmlFile("options.html");
installCommonWindowMocks();
const chrome = createChromeMock({
manifest: { version: "5.1.7.0" },
syncData: config.syncData,
localData: config.localData
});
global.chrome = chrome;
window.chrome = chrome;
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
text: () => Promise.resolve("<svg></svg>")
})
);
window.fetch = global.fetch;
evaluateScript("ui-icons.js");
evaluateScript("lucide-client.js");
evaluateScript("options.js");
fireDOMContentLoaded();
return chrome;
}
describe("options.js", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
delete global.chrome;
delete global.fetch;
});
it("restores saved settings, bindings, site rules, and popup bar order", async () => {
bootOptions({
syncData: {
rememberSpeed: true,
forceLastSavedSpeed: true,
controllerLocation: "middle-right",
controllerOpacity: 0.75,
controllerMarginTop: 22,
controllerMarginBottom: 14,
popupMatchHoverControls: false,
controllerButtons: ["rewind", "fast", "display"],
popupControllerButtons: ["advance", "settings", "rewind", "advance"],
keyBindings: [
{ action: "display", code: "KeyV", value: 0, predefined: true },
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
],
siteRules: [
{
pattern: "youtube.com",
enabled: false,
controllerMarginTop: 12,
popupControllerButtons: ["advance", "settings", "rewind"]
}
]
}
});
await flushAsyncWork(3);
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
expect(document.getElementById("rememberSpeed").checked).toBe(true);
expect(document.getElementById("forceLastSavedSpeed").checked).toBe(true);
expect(document.getElementById("controllerLocation").value).toBe(
"middle-right"
);
expect(document.getElementById("controllerOpacity").value).toBe("0.75");
expect(document.getElementById("controllerMarginTop").value).toBe("22");
expect(document.getElementById("popupMatchHoverControls").checked).toBe(false);
expect(
document.getElementById("popupCbEditorWrap").classList.contains(
"cb-editor-disabled"
)
).toBe(false);
const popupButtons = Array.from(
document.querySelectorAll("#popupControlBarActive .cb-block")
).map((block) => block.dataset.buttonId);
expect(popupButtons).toEqual(["advance", "rewind"]);
expect(
document.querySelector('.shortcut-row.customs[data-action="pause"]')
).not.toBeNull();
expect(document.querySelectorAll(".site-rule")).toHaveLength(1);
expect(document.querySelector(".site-rule .site-enabled").checked).toBe(false);
});
it("saves normalized settings and site rule overrides", async () => {
const chrome = bootOptions();
await flushAsyncWork(3);
document.getElementById("rememberSpeed").checked = true;
document.getElementById("hideWithControlsTimer").value = "99";
document.getElementById("controllerLocation").value = "bottom-left";
document.getElementById("controllerOpacity").value = "0.65";
document.getElementById("controllerMarginTop").value = "250";
document.getElementById("controllerMarginBottom").value = "-5";
document.getElementById("popupMatchHoverControls").checked = false;
document.getElementById("showPopupControlBar").checked = true;
window.populatePopupControlBarEditor(["advance", "settings", "rewind"]);
window.createSiteRule({ pattern: "youtube.com" });
const ruleEl = document.querySelector(".site-rule");
ruleEl.querySelector(".override-placement").checked = true;
ruleEl.querySelector(".site-controllerLocation").value = "top-right";
ruleEl.querySelector(".site-controllerMarginTop").value = "300";
ruleEl.querySelector(".site-controllerMarginBottom").value = "-10";
ruleEl.querySelector(".override-autohide").checked = true;
ruleEl.querySelector(".site-hideWithControls").checked = true;
ruleEl.querySelector(".site-hideWithControlsTimer").value = "0";
ruleEl.querySelector(".override-popup-controlbar").checked = true;
ruleEl.querySelector(".site-showPopupControlBar").checked = false;
window.populateControlBarZones(
ruleEl.querySelector(".site-popup-cb-active"),
ruleEl.querySelector(".site-popup-cb-available"),
["advance", "settings", "rewind"],
function (id) {
return id !== "settings";
}
);
window.save_options();
expect(chrome.storage.sync.remove).toHaveBeenCalledWith(
[
"resetSpeed",
"speedStep",
"fastSpeed",
"rewindTime",
"advanceTime",
"resetKeyCode",
"slowerKeyCode",
"fasterKeyCode",
"rewindKeyCode",
"advanceKeyCode",
"fastKeyCode",
"blacklist"
],
expect.any(Function)
);
const savedSettings = chrome.storage.sync.set.mock.calls.at(-1)[0];
expect(savedSettings.rememberSpeed).toBe(true);
expect(savedSettings.hideWithControlsTimer).toBe(15);
expect(savedSettings.controllerLocation).toBe("bottom-left");
expect(savedSettings.controllerMarginTop).toBe(200);
expect(savedSettings.controllerMarginBottom).toBe(0);
expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]);
expect(savedSettings.siteRules).toEqual([
{
pattern: "youtube.com",
enabled: true,
controllerLocation: "top-right",
controllerMarginTop: 200,
controllerMarginBottom: 0,
hideWithControls: true,
hideWithControlsTimer: 0.1,
showPopupControlBar: false,
popupControllerButtons: ["advance", "rewind"]
}
]);
});
it("blocks save when a site rule regex is invalid", async () => {
const chrome = bootOptions();
await flushAsyncWork(3);
window.createSiteRule({ pattern: "/[abc/" });
window.save_options();
expect(document.getElementById("status").textContent).toContain(
"Invalid site rule regex"
);
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
});
it("adds shortcuts from the selector and records key input states", async () => {
bootOptions();
await flushAsyncWork(3);
const selector = document.getElementById("addShortcutSelector");
selector.value = "pause";
selector.dispatchEvent(new window.Event("change", { bubbles: true }));
const row = document.querySelector('.shortcut-row.customs[data-action="pause"]');
expect(row).not.toBeNull();
const keyInput = row.querySelector(".customKey");
keyInput.dispatchEvent(
new window.KeyboardEvent("keydown", {
key: "q",
code: "KeyQ",
bubbles: true
})
);
expect(keyInput.vscBinding.code).toBe("KeyQ");
expect(keyInput.value).toBe("Q");
keyInput.dispatchEvent(
new window.KeyboardEvent("keydown", {
key: "Escape",
code: "Escape",
bubbles: true
})
);
expect(keyInput.vscBinding.disabled).toBe(true);
expect(selector.disabled).toBe(false);
});
});
+121
View File
@@ -0,0 +1,121 @@
import {
createChromeMock,
flushAsyncWork,
loadHtml,
loadScript,
triggerDomContentLoaded
} from "./helpers/browser.js";
async function setupPopup(overrides = {}) {
loadHtml("popup.html");
globalThis.chrome = createChromeMock(overrides);
window.chrome = globalThis.chrome;
loadScript("shared/site-rules.js");
loadScript("shared/popup-controls.js");
loadScript("ui-icons.js");
loadScript("popup.js");
triggerDomContentLoaded();
await flushAsyncWork();
return globalThis.chrome;
}
describe("popup UI", () => {
it("renders version, builds controls, and prefers the active frame speed", async () => {
await setupPopup({
manifestVersion: "5.1.7.0",
executeScriptResponse: [
{ speed: 1.1, preferred: false },
{ speed: 1.75, preferred: true }
]
});
expect(document.getElementById("app-version").innerText).toBe("5.1.7.0");
expect(document.getElementById("popupSpeed").textContent).toBe("1.75");
expect(
document.querySelectorAll("#popupControlBar button").length
).toBeGreaterThan(0);
});
it("shows disabled state for a matching site rule", async () => {
await setupPopup({
sync: {
enabled: true,
siteRules: [{ pattern: "example.com", enabled: false }]
}
});
expect(document.getElementById("status").innerText).toBe(
"Speeder is disabled for this site."
);
expect(document.getElementById("popupSpeed").textContent).toBe("1.00");
expect(document.getElementById("popupControlBar").style.display).toBe(
"none"
);
});
it("toggles enabled state and updates the browser action icons", async () => {
const chrome = await setupPopup();
chrome.storage.sync.set.mockClear();
chrome.browserAction.setIcon.mockClear();
document.getElementById("disable").click();
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
{ enabled: false },
expect.any(Function)
);
expect(document.getElementById("enable").classList.contains("hide")).toBe(
false
);
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
path: {
19: "icons/icon19_disabled.png",
38: "icons/icon38_disabled.png",
48: "icons/icon48_disabled.png"
}
});
});
it("handles refresh responses for unsupported and successful pages", async () => {
vi.useFakeTimers();
const chrome = await setupPopup();
let response = null;
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
if (message.action === "rescan_page") {
callback(response);
return;
}
callback({ speed: 1.25 });
});
document.getElementById("refresh").click();
expect(document.getElementById("status").innerText).toBe(
"Cannot run on this page."
);
response = { status: "complete" };
document.getElementById("refresh").click();
expect(document.getElementById("status").innerText).toBe(
"Scan complete. Closing..."
);
vi.advanceTimersByTime(500);
expect(window.close).toHaveBeenCalled();
});
it("dispatches popup control bar actions back to the active tab", async () => {
const chrome = await setupPopup();
chrome.tabs.sendMessage.mockClear();
chrome.tabs.executeScript.mockClear();
document.querySelector("#popupControlBar button").click();
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
1,
expect.objectContaining({
action: "run_action"
}),
expect.any(Function)
);
expect(chrome.tabs.executeScript).toHaveBeenCalled();
});
});
+173
View File
@@ -0,0 +1,173 @@
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
const {
createChromeMock,
evaluateScript,
fireDOMContentLoaded,
flushAsyncWork,
installCommonWindowMocks,
loadHtmlFile
} = require("./helpers/extension-test-utils");
function bootPopup(options) {
const config = options || {};
loadHtmlFile("popup.html");
installCommonWindowMocks();
const chrome = createChromeMock({
manifest: { version: "9.9.9-test" },
syncData: config.syncData,
localData: config.localData,
tabsQueryResult: [
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
]
});
chrome.tabs.executeScript.mockImplementation(
config.executeScriptImpl ||
((tabId, details, callback) => {
if (callback) callback([{ speed: 1.0, preferred: true }]);
})
);
chrome.tabs.sendMessage.mockImplementation(
config.sendMessageImpl ||
((tabId, message, callback) => {
if (message.action === "get_speed") {
callback({ speed: 1.0 });
return;
}
if (message.action === "rescan_page") {
callback({ status: "complete" });
return;
}
callback({ speed: 1.0 });
})
);
global.chrome = chrome;
window.chrome = chrome;
evaluateScript("ui-icons.js");
evaluateScript("popup.js");
fireDOMContentLoaded();
return chrome;
}
describe("popup.js", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
delete global.chrome;
});
it("renders the popup disabled state when a site rule disables Speeder", async () => {
bootPopup({
syncData: {
enabled: true,
siteRules: [
{
pattern: "youtube.com",
enabled: false
}
]
},
activeTab: {
id: 10,
active: true,
url: "https://www.youtube.com/watch?v=abc123"
}
});
await flushAsyncWork();
expect(document.querySelector("#app-version").textContent).toBe("9.9.9-test");
expect(document.querySelector("#status").textContent).toContain(
"disabled for this site"
);
expect(document.querySelector("#popupSpeed").textContent).toBe("1.00");
expect(document.querySelector("#popupControlBar").style.display).toBe("none");
});
it("builds sanitized popup buttons and refreshes speed after an action", async () => {
const chrome = bootPopup({
syncData: {
enabled: true,
controllerButtons: ["faster", "settings", "rewind", "faster"],
popupMatchHoverControls: true
}
});
chrome.tabs.executeScript
.mockImplementationOnce((tabId, details, callback) => {
callback([
{ speed: 1.25, preferred: false },
{ speed: 1.5, preferred: true }
]);
})
.mockImplementationOnce((tabId, details, callback) => {
callback([{ speed: 1.75, preferred: true }]);
});
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
if (message.action === "run_action") {
callback({ speed: 1.75 });
return;
}
callback({ speed: 1.0 });
});
document.dispatchEvent(new window.Event("DOMContentLoaded"));
await flushAsyncWork();
const buttons = Array.from(
document.querySelectorAll("#popupControlBar button[data-action]")
).map((button) => button.dataset.action);
expect(buttons).toEqual(["faster", "rewind"]);
expect(document.querySelector("#popupSpeed").textContent).toBe("1.50");
document.querySelector('#popupControlBar button[data-action="faster"]').click();
await flushAsyncWork();
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
99,
{ action: "run_action", actionName: "faster" },
expect.any(Function)
);
expect(document.querySelector("#popupSpeed").textContent).toBe("1.75");
});
it("toggles enablement and closes after a successful refresh", async () => {
vi.useFakeTimers();
const chrome = bootPopup({
syncData: {
enabled: false
}
});
await flushAsyncWork();
expect(document.querySelector("#enable").classList.contains("hide")).toBe(false);
expect(document.querySelector("#disable").classList.contains("hide")).toBe(true);
document.querySelector("#enable").click();
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
{ enabled: true },
expect.any(Function)
);
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
path: {
19: "icons/icon19.png",
38: "icons/icon38.png",
48: "icons/icon48.png"
}
});
document.querySelector("#refresh").click();
expect(document.querySelector("#status").textContent).toContain("Closing");
vi.advanceTimersByTime(500);
expect(window.close).toHaveBeenCalled();
});
});
+25
View File
@@ -0,0 +1,25 @@
import { afterEach, beforeEach, vi } from "vitest";
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
if (typeof window !== "undefined") {
window.open = vi.fn();
window.close = vi.fn();
}
globalThis.requestAnimationFrame = (callback) => setTimeout(callback, 0);
globalThis.cancelAnimationFrame = (id) => clearTimeout(id);
});
afterEach(() => {
vi.useRealTimers();
delete globalThis.SpeederShared;
delete globalThis.restore_options;
if (typeof document !== "undefined") {
document.head.innerHTML = "";
document.body.innerHTML = "";
}
delete globalThis.chrome;
});
+153
View File
@@ -0,0 +1,153 @@
import controllerUtils from "../shared/controller-utils.js";
import importExportUtils from "../shared/import-export.js";
import keyBindingUtils from "../shared/key-bindings.js";
import popupControls from "../shared/popup-controls.js";
import siteRules from "../shared/site-rules.js";
describe("shared helpers", () => {
it("matches site rules and skips invalid regex patterns", () => {
const literalRule = { pattern: "example.com/watch" };
const regexRule = { pattern: "/youtube\\.com\\/watch/i" };
expect(
siteRules.matchSiteRule("https://example.com/watch?v=1", [literalRule])
).toBe(literalRule);
expect(
siteRules.matchSiteRule("https://www.youtube.com/watch?v=2", [regexRule])
).toBe(regexRule);
expect(
siteRules.matchSiteRule("https://www.youtube.com/shorts/3", [
{ pattern: "/(/" },
regexRule
])
).toBeNull();
expect(siteRules.isSiteRuleDisabled({ enabled: false })).toBe(true);
});
it("sanitizes and resolves popup button orders", () => {
const controllerButtonDefs = {
rewind: {},
faster: {},
advance: {},
display: {},
settings: {}
};
expect(
popupControls.sanitizeButtonOrder(
["rewind", "settings", "rewind", "faster", "missing"],
controllerButtonDefs,
new Set(["settings"])
)
).toEqual(["rewind", "faster"]);
expect(
popupControls.resolvePopupButtons(
{
popupMatchHoverControls: true,
controllerButtons: ["advance", "display"],
popupControllerButtons: ["rewind"]
},
{ controllerButtons: ["faster", "advance"] },
{
controllerButtonDefs,
defaultButtons: ["rewind", "display"],
excludedIds: ["settings"]
}
)
).toEqual(["faster", "advance"]);
expect(
popupControls.resolvePopupButtons(
{
popupMatchHoverControls: false,
popupControllerButtons: ["rewind", "display"]
},
{ popupControllerButtons: ["advance", "settings", "advance"] },
{
controllerButtonDefs,
defaultButtons: ["rewind", "display"],
excludedIds: ["settings"]
}
)
).toEqual(["advance"]);
});
it("normalizes controller locations and margins", () => {
expect(controllerUtils.normalizeControllerLocation("top-right")).toBe(
"top-right"
);
expect(controllerUtils.normalizeControllerLocation("bogus")).toBe(
controllerUtils.defaultControllerLocation
);
expect(controllerUtils.clampControllerMarginPx(300, 65)).toBe(200);
expect(controllerUtils.clampControllerMarginPx(-5, 65)).toBe(0);
expect(controllerUtils.getNextControllerLocation("top-left")).toBe(
"top-center"
);
});
it("infers key binding codes from legacy formats", () => {
expect(keyBindingUtils.normalizeBindingKey("a")).toBe("A");
expect(keyBindingUtils.normalizeBindingKey("Esc")).toBe("Escape");
expect(keyBindingUtils.legacyBindingKeyToCode(" ")).toBe("Space");
expect(keyBindingUtils.legacyKeyCodeToCode(90)).toBe("KeyZ");
expect(keyBindingUtils.inferBindingCode({ key: "x" }, null)).toBe("KeyX");
expect(keyBindingUtils.inferBindingCode({ keyCode: 107 }, null)).toBe(
"NumpadAdd"
);
expect(keyBindingUtils.getLegacyKeyCode({ key: 65 })).toBe(65);
});
it("builds and parses import/export payloads", () => {
expect(
importExportUtils.generateBackupFilename(new Date(2026, 3, 4, 8, 9, 10))
).toBe("speeder-backup_2026-04-04_08.09.10.json");
expect(
importExportUtils.buildBackupPayload(
{ rememberSpeed: true },
{ customButtonIcons: {} },
"2026-04-04T08:09:10Z"
)
).toEqual({
version: "1.1",
exportDate: "2026-04-04T08:09:10.000Z",
settings: { rememberSpeed: true },
localSettings: { customButtonIcons: {} }
});
expect(
importExportUtils.extractImportSettings({
settings: { rememberSpeed: true },
localSettings: { customButtonIcons: {} }
})
).toEqual({
isWrappedBackup: true,
settings: { rememberSpeed: true },
localSettings: { customButtonIcons: {} }
});
expect(
importExportUtils.parseImportText(
JSON.stringify({ rememberSpeed: true, keyBindings: [] })
)
).toEqual({
isWrappedBackup: false,
settings: { rememberSpeed: true, keyBindings: [] },
localSettings: null
});
expect(
importExportUtils.extractImportSettings({ enabled: true })
).toEqual({
isWrappedBackup: false,
settings: { enabled: true },
localSettings: null
});
expect(importExportUtils.isRecognizedRawSettingsObject({ wat: true })).toBe(
false
);
});
});
+78 -4
View File
@@ -3,6 +3,7 @@
* Use stroke="currentColor" so buttons inherit foreground for monochrome UI.
*/
var VSC_ICON_SIZE_DEFAULT = 18;
var VSC_SVG_NS = "http://www.w3.org/2000/svg";
/** Inner SVG markup only (paths / shapes inside <svg>). */
var vscUiIconPaths = {
@@ -15,6 +16,9 @@ var vscUiIconPaths = {
slower: '<line x1="5" y1="12" x2="19" y2="12"/>',
faster:
'<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
moreHorizontal:
'<circle cx="6" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="18" cy="12" r="1.5"/>',
chevronUp: '<path d="m18 15-6-6-6 6"/>',
display:
'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
fast: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
@@ -54,6 +58,79 @@ function vscIconSvgString(action, size) {
);
}
function vscClearElement(el) {
if (!el) return;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
function vscSanitizeSvgTree(svg) {
if (!svg || String(svg.tagName).toLowerCase() !== "svg") return null;
svg.querySelectorAll("script, style, foreignObject").forEach(function (n) {
n.remove();
});
svg.querySelectorAll("*").forEach(function (el) {
for (var i = el.attributes.length - 1; i >= 0; i--) {
var attr = el.attributes[i];
var name = attr.name.toLowerCase();
var val = attr.value;
if (name.indexOf("on") === 0) {
el.removeAttribute(attr.name);
continue;
}
if (
(name === "href" || name === "xlink:href") &&
/^\s*javascript:/i.test(val)
) {
el.removeAttribute(attr.name);
}
}
});
svg.setAttribute("xmlns", VSC_SVG_NS);
svg.setAttribute("aria-hidden", "true");
return svg;
}
function vscCreateSvgNode(doc, svgText) {
if (!doc || !svgText || typeof svgText !== "string") return null;
var clean = String(svgText).replace(/\0/g, "").trim();
if (!clean || !/<svg[\s>]/i.test(clean)) return null;
var parsed = new DOMParser().parseFromString(clean, "image/svg+xml");
if (parsed.querySelector("parsererror")) return null;
var svg = vscSanitizeSvgTree(parsed.querySelector("svg"));
if (!svg) return null;
return doc.importNode(svg, true);
}
function vscSetSvgContent(el, svgText) {
if (!el) return false;
vscClearElement(el);
var doc = el.ownerDocument || document;
var svg = vscCreateSvgNode(doc, svgText);
if (!svg) return false;
el.appendChild(svg);
return true;
}
function vscCreateSvgWrap(doc, svgText, className) {
if (!doc) return null;
var span = doc.createElement("span");
span.className = className || "vsc-btn-icon";
if (!vscSetSvgContent(span, svgText)) {
return null;
}
return span;
}
/**
* @param {Document} doc
* @param {string} action
@@ -62,8 +139,5 @@ function vscIconSvgString(action, size) {
function vscIconWrap(doc, action, size) {
var html = vscIconSvgString(action, size);
if (!html) return null;
var span = doc.createElement("span");
span.className = "vsc-btn-icon";
span.innerHTML = html;
return span;
return vscCreateSvgWrap(doc, html, "vsc-btn-icon");
}
+12
View File
@@ -0,0 +1,12 @@
const { defineConfig } = require("vitest/config");
module.exports = defineConfig({
test: {
environment: "jsdom",
clearMocks: true,
globals: true,
restoreMocks: true,
include: ["tests/**/*.test.js"],
setupFiles: ["./tests/setup.js"]
}
});