diff --git a/.gitignore b/.gitignore index 3e35ece..bd3e97a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ .vscode *.zip update_notes.txt -.codex \ No newline at end of file +.codex +node_modules/ +.wxt/ +.output/ +dist/ +coverage/ \ No newline at end of file diff --git a/README.md b/README.md index 0710b5f..6ee0f6b 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,200 @@ +
+ +Cleanplaats + # Cleanplaats -Cleanplaats is een browserextensie voor [Marktplaats](https://www.marktplaats.nl/), [2dehands](https://www.2dehands.be/) en [2ememain](https://www.2ememain.be/) die rommel uit zoekresultaten en overzichtspagina's haalt. De extensie helpt gebruikers om sneller door relevante advertenties te bladeren door promotionele, storende en ongewenste listings te verbergen. +**Marktplaats, 2dehands en 2ememain — zonder spam, dagtoppers en bedrijfsadvertenties.** -Gebouwd voor Chromium-browsers en Firefox, met een modulaire codebase die eenvoudiger te onderhouden en uit te breiden is. +[![Version](https://img.shields.io/badge/version-2.0.7-2563eb?style=flat-square)](./package.json) +[![WXT](https://img.shields.io/badge/built%20with-WXT%200.20-ff7e1d?style=flat-square&logo=googlechrome&logoColor=white)](https://wxt.dev) +[![React](https://img.shields.io/badge/React-19-61dafb?style=flat-square&logo=react&logoColor=000)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org) +[![Manifest V3](https://img.shields.io/badge/Manifest-V3-4285f4?style=flat-square&logo=googlechrome&logoColor=white)](https://developer.chrome.com/docs/extensions/mv3/intro/) +[![Vitest](https://img.shields.io/badge/tested%20with-Vitest-6e9f18?style=flat-square&logo=vitest&logoColor=white)](https://vitest.dev) +[![Chrome](https://img.shields.io/badge/Chrome-supported-34a853?style=flat-square&logo=googlechrome&logoColor=white)](https://www.google.com/chrome/) +[![Firefox](https://img.shields.io/badge/Firefox-supported-ff7139?style=flat-square&logo=firefoxbrowser&logoColor=white)](https://www.mozilla.org/firefox/) -## Wat doet Cleanplaats? +
-Cleanplaats voegt een compact bedieningspaneel toe aan ondersteunde pagina's en laat gebruikers zelf bepalen wat verborgen wordt. +--- -Belangrijkste functies: +## Overzicht -- Verbergt topadvertenties, dagtoppers, bedrijfsadvertenties en promotionele stickers -- Ondersteunt verborgen verkopers en blacklist-termen op basis van advertentietitels -- Bevat een dark mode voor zowel het paneel als de marktplaatsinterface -- Ondersteunt meertalige varianten, inclusief Nederlandse en Franse termen op 2dehands en 2ememain -- Onthoudt voorkeuren zoals filters, sortering, resultaten per pagina en thema-instelling -- Toont onboarding en update-notities bij nieuwe versies +Cleanplaats is een browserextensie die overzichtspagina's en advertenties op +[Marktplaats](https://www.marktplaats.nl/), [2dehands](https://www.2dehands.be/) +en [2ememain](https://www.2ememain.be/) opschoont. Topadvertenties, dagtoppers, +bedrijfsadvertenties en promotieblokken worden uit de weg gehaald, en +gebruikers kunnen zelf verkopers en zoektermen verbergen. -## Ondersteunde websites +Deze codebase is volledig herschreven met **WXT + React 19 + TypeScript**, met +behoud van bestaande functionaliteit, voorkeuren en domeinondersteuning. -- `marktplaats.nl` -- `2dehands.be` -- `2ememain.be` +## Functies -## Screenshot -in progress +- **Slimme opschoning** — verbergt topadvertenties, dagtoppers, bedrijfsadvertenties en promotionele stickers. +- **Verkopersbeheer** — verberg specifieke verkopers in één klik, direct vanaf advertentie- of overzichtspagina's. +- **Blacklist op titel** — onderdruk listings op basis van zelfgekozen woorden of frases. +- **Dark mode** — donker thema voor zowel het Cleanplaats-paneel als de Marktplaats-interface, zonder visuele flicker bij het laden. +- **Verkoper-ouderdomswaarschuwing** — krijg een melding bij accounts jonger dan een door jou gekozen periode (dagen, weken, maanden of jaren). +- **Meertalige ondersteuning** — werkt met de Nederlandse en Franse termen op 2dehands en 2ememain. +- **Persistente voorkeuren** — filters, sortering, resultaten per pagina en thema-instellingen worden onthouden. +- **Onboarding & update-notities** — heldere uitleg bij installatie en bij iedere nieuwe versie. -## Installatie voor lokaal gebruik +## Ondersteunde sites -### Chrome of andere Chromium-browsers +| Domein | Land | Taal | +| --- | --- | --- | +| `marktplaats.nl` | NL | Nederlands | +| `2dehands.be` | BE | Nederlands | +| `2ememain.be` | BE | Frans | -1. Clone deze repository. -2. Open `chrome://extensions`. -3. Zet `Developer mode` aan. -4. Kies `Load unpacked`. -5. Selecteer de rootmap van deze repository. +## Browserondersteuning -### Firefox +| Browser | Manifest | Status | +| --- | --- | --- | +| Chromium (Chrome, Edge, Brave, Arc, Opera) | MV3 | Ondersteund | +| Firefox (desktop + Android) | MV3 | Ondersteund (`strict_min_version: 121.0`) | -1. Clone deze repository. -2. Open `about:debugging#/runtime/this-firefox`. -3. Kies `Load Temporary Add-on`. -4. Selecteer het bestand [manifest.json](/home/aron/projects/Cleanplaats/manifest.json). +Gecko-instellingen (extensie-id en minimum versie) blijven in de +manifestconfiguratie behouden. -## Ontwikkeling +## Aan de slag -Deze repository gebruikt geen buildstap. De extensie draait direct op de bestanden in de repo. +### Vereisten -Tijdens development werk je meestal zo: +- Node.js 20 of hoger +- npm 10 of hoger -1. Pas bestanden aan in de repo. -2. Herlaad de extensie in de browser. -3. Ververs een ondersteunde pagina op Marktplaats, 2dehands of 2ememain. +### Installeren -Handige controle: +```bash +npm install +``` + +`postinstall` voert automatisch `wxt prepare` uit en genereert de typings in +`.wxt/`. + +### Development ```bash -node --check content/shared.js -node --check content/notifications.js +# Chromium +npm run dev + +# Firefox +npm run dev:firefox ``` -## Projectstructuur +WXT start een dev-runtime en schrijft de extensie naar `.output/`. Laad die +map als unpacked extension in je browser. + +### Productiebuilds -De codebase is opgesplitst per verantwoordelijkheid: +```bash +# Chromium (MV3) +npm run build + +# Firefox (MV3) +npm run build:firefox +``` + +### Distributiezips + +```bash +npm run zip +npm run zip:firefox +``` + +### Quality gates + +```bash +npm run compile # tsc --noEmit +npm run test # vitest run +``` + +## Projectstructuur ```text -background/ Service worker modules -content/ Content-script modules -icons/ Extensie-assets -background.js Bootstrap voor background modules -content.js Bootstrap voor content modules -content.css Paneel- en UI-styling -dark-mode.css Dark mode overrides voor ondersteunde sites -theme-init.js Vroege theme-initialisatie om flash te voorkomen -manifest.json Browser extension manifest +src/ + entrypoints/ WXT entrypoints (background, main content, theme-init) + content/ Content runtime, services, React-paneel + panel/ CleanplaatsPanel.tsx + hooks/state + services/ cleanup, blacklist-inject, theme, notifications, … + runtime/ store en bootstrap + locale/ paneelteksten + utils/ site- en sorthelpers + background/ Background services, listeners en messaging + services/ settings, navigation, hash-url, keepalive, rules + shared/ Types, constants, storage- en messaging-utilities + styles/ Content/panel + dark mode CSS + types/ Asset-typedeclaraties +public/ + icons/ Extensie-assets (gekopieerd naar de build) +tests/ Vitest-suites (background/content/shared) +wxt.config.ts WXT-config en manifestdeclaratie +vitest.config.ts Vitest-config ``` ## Belangrijke modules -- `content/cleanup.js`: detecteert en verbergt listings -- `content/blacklist.js`: beheer van blacklist-termen en verborgen verkopers -- `content/theme.js`: thema-logica en dark mode synchronisatie -- `content/notifications.js`: onboarding, update-popup en toastmeldingen -- `content/ui.js`: opbouw van het Cleanplaats-paneel -- `background/`: achtergrondlogica voor lifecycle, messaging en URL-regels +| Module | Verantwoordelijkheid | +| --- | --- | +| `src/content/services/cleanup.ts` | Detecteert en verbergt listings/ads in DOM-mutaties. | +| `src/content/services/blacklist-inject.ts` | Beheert verborgen verkopers en injecteert "verberg verkoper"-knoppen. | +| `src/content/services/blacklist-terms.ts` | Filtert advertenties op zelfgekozen titeltermen. | +| `src/content/services/theme.ts` | Thema-logica en dark-mode-synchronisatie. | +| `src/content/services/notifications.ts` | Onboarding, update-popup en toastmeldingen. | +| `src/content/panel/CleanplaatsPanel.tsx` | React-paneel met hooks, store en tabbladen. | +| `src/background/services/rules.ts` | URL-regels via `declarativeNetRequest`. | +| `src/background/services/keepalive.ts` | Service-worker keepalive via `alarms`. | -## Rechten +## Permissies -Cleanplaats gebruikt browserrechten die nodig zijn voor: +Cleanplaats vraagt de volgende browserrechten aan in `wxt.config.ts`: -- opslag van gebruikersinstellingen -- injecteren van scripts en styles op ondersteunde domeinen -- tab- en navigatie-events voor extensielogica +| Permissie | Reden | +| --- | --- | +| `storage` | Opslag van gebruikersinstellingen, blacklist en thema. | +| `scripting` | Injecteren van content scripts en styles. | +| `tabs` | Reageren op tab-events voor extensielogica. | +| `webNavigation` | Detecteren van navigaties binnen Marktplaats SPA-routes. | +| `declarativeNetRequest` | URL-regels voor netwerkfilters. | +| `alarms` | Service-worker keepalive. | -Zie [manifest.json](/home/aron/projects/Cleanplaats/manifest.json) voor de actuele lijst van permissies en host-permissies. +Host-permissies zijn beperkt tot: -## Roadmap +```text +*://*.marktplaats.nl/* +*://*.2dehands.be/* +*://*.2ememain.be/* +``` -Mogelijke vervolgstappen: +## Tech stack -- extra regressietests voor selector-wijzigingen op de ondersteunde sites -- visuele regression checks voor dark mode -- verdere opschoning van content-script styling en componentstructuur +- **[WXT](https://wxt.dev) 0.20** — extension framework met first-class MV3-ondersteuning +- **[React](https://react.dev) 19** — UI voor het in-page paneel +- **[TypeScript](https://www.typescriptlang.org) 6** — strikte types in alle entrypoints +- **[Vitest](https://vitest.dev) 4** — unit-tests voor shared utilities en content/background services +- **[@wxt-dev/module-react](https://www.npmjs.com/package/@wxt-dev/module-react)** — React-integratie binnen WXT ## Bijdragen -Issues en verbeterideeën zijn welkom. Gebruik bij voorkeur GitHub Issues voor: +Bug reports, regressies en feature requests zijn welkom via GitHub Issues. +Geef bij een melding bij voorkeur aan: -- bugs +- welk domein (marktplaats.nl, 2dehands.be, 2ememain.be) +- welke browser en versie +- een URL of screenshot van het probleem +- of het probleem ook optreedt na het uitzetten van de extensie + +Zinvolle categorieën: + +- bugs op overzichts- of advertentiepagina's - regressies na markup-wijzigingen op de marktplaatssites -- feature requests -- compatibiliteitsproblemen tussen Chrome en Firefox +- compatibiliteitsproblemen tussen Chromium en Firefox +- voorstellen voor nieuwe filters of paneelopties ## Versie -Huidige versie in deze repository: `2.0.4` - +Huidige versie: **`2.0.7`** — zie `src/shared/constants/update-notes.ts` voor +de volledige changelog die in de extensie wordt getoond. diff --git a/background.js b/background.js deleted file mode 100644 index a5186d4..0000000 --- a/background.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Cleanplaats background bootstrap. - */ - -importScripts( - 'background/shared.js', - 'background/url-rules.js', - 'background/theme.js', - 'background/messages.js', - 'background/keepalive.js', - 'background/lifecycle.js' -); - -initialize(); -setupKeepAlive(); - -console.log('Cleanplaats background.js: Script execution finished initial top-level setup.', new Date().toISOString()); diff --git a/background/keepalive.js b/background/keepalive.js deleted file mode 100644 index 4adbd4a..0000000 --- a/background/keepalive.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Background Firefox keep-alive management. - */ - -function setupKeepAlive() { - if (typeof browser !== 'undefined') { - console.log('Cleanplaats: Setting up smart Firefox keep-alive mechanism'); - - browserAPI.alarms.create('cleanplaats-keepalive', { - delayInMinutes: 2, - periodInMinutes: 2 - }); - - if (!browserAPI.alarms.onAlarm.hasListener(handleKeepAliveAlarm)) { - browserAPI.alarms.onAlarm.addListener(handleKeepAliveAlarm); - } - - if (browserAPI.webNavigation && browserAPI.webNavigation.onBeforeNavigate) { - browserAPI.webNavigation.onBeforeNavigate.addListener((details) => { - if (details.frameId === 0 && - (details.url.includes('marktplaats.nl') || details.url.includes('2dehands.be') || details.url.includes('2ememain.be'))) { - lastMarktplaatsActivity = Date.now(); - console.log('Cleanplaats: Marktplaats activity detected, updating timestamp'); - } - }); - } - } -} - -function handleKeepAliveAlarm(alarm) { - if (alarm.name === 'cleanplaats-keepalive') { - const timeSinceActivity = Date.now() - lastMarktplaatsActivity; - const minutesSinceActivity = timeSinceActivity / (1000 * 60); - - console.log(`Cleanplaats: Keep-alive check - ${minutesSinceActivity.toFixed(1)} minutes since last Marktplaats activity`); - - if (minutesSinceActivity > 30) { - console.log('Cleanplaats: User inactive for 30+ minutes, switching to low-frequency mode'); - browserAPI.alarms.clear('cleanplaats-keepalive'); - browserAPI.alarms.create('cleanplaats-keepalive', { - delayInMinutes: 10, - periodInMinutes: 10 - }); - } else if (minutesSinceActivity > 10) { - console.log('Cleanplaats: User inactive for 10+ minutes, switching to medium-frequency mode'); - browserAPI.alarms.clear('cleanplaats-keepalive'); - browserAPI.alarms.create('cleanplaats-keepalive', { - delayInMinutes: 5, - periodInMinutes: 5 - }); - } else { - console.log('Cleanplaats: User recently active, maintaining normal frequency'); - } - } -} - -function resetKeepAliveToActiveMode() { - if (typeof browser !== 'undefined') { - lastMarktplaatsActivity = Date.now(); - - browserAPI.alarms.clear('cleanplaats-keepalive'); - browserAPI.alarms.create('cleanplaats-keepalive', { - delayInMinutes: 2, - periodInMinutes: 2 - }); - - console.log('Cleanplaats: Reset keep-alive to active mode'); - } -} diff --git a/background/lifecycle.js b/background/lifecycle.js deleted file mode 100644 index abe1bbc..0000000 --- a/background/lifecycle.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Background lifecycle and listener registration. - */ - -async function handleStorageChanges(changes, areaName) { - if (areaName !== 'local' || !changes.cleanplaatsSettings) return; - - console.log('Cleanplaats: handleStorageChanges triggered.', changes.cleanplaatsSettings); - try { - const newSettingsData = JSON.parse(changes.cleanplaatsSettings.newValue || '{}'); - const newResultsPerPage = newSettingsData.resultsPerPage?.toString() || '30'; - const newDefaultSortMode = newSettingsData.defaultSortMode || 'standard'; - const newSortPreferenceSource = newSettingsData.sortPreferenceSource || 'cleanplaats'; - const darkModeEnabled = Boolean(newSettingsData.darkMode); - - let settingsActuallyChanged = false; - if (newResultsPerPage !== resultsPerPage) { - console.log(`Cleanplaats: Results per page changed from ${resultsPerPage} to ${newResultsPerPage}`); - resultsPerPage = newResultsPerPage; - settingsActuallyChanged = true; - } - if (newDefaultSortMode !== defaultSortMode) { - console.log(`Cleanplaats: Default sort mode changed from ${defaultSortMode} to ${newDefaultSortMode}`); - defaultSortMode = newDefaultSortMode; - settingsActuallyChanged = true; - } - if (newSortPreferenceSource !== sortPreferenceSource) { - console.log(`Cleanplaats: Sort preference source changed from ${sortPreferenceSource} to ${newSortPreferenceSource}`); - sortPreferenceSource = newSortPreferenceSource; - settingsActuallyChanged = true; - } - - await updateDarkModeStartupScript(darkModeEnabled); - - if (settingsActuallyChanged) { - await updateApiRequestRules(resultsPerPage, defaultSortMode); - } - } catch (error) { - console.error('Cleanplaats: Error parsing settings in handleStorageChanges:', error); - } -} - -async function initialize() { - console.log('Cleanplaats background.js: initialize() called.', new Date().toISOString()); - - browserAPI.storage.local.get(['cleanplaatsSettings'], async (result) => { - if (browserAPI.runtime.lastError) { - console.error('Cleanplaats: Error loading settings during initialize:', browserAPI.runtime.lastError); - } else { - console.log('Cleanplaats: Settings loaded from storage:', result.cleanplaatsSettings); - try { - if (result.cleanplaatsSettings) { - const settings = JSON.parse(result.cleanplaatsSettings); - resultsPerPage = settings.resultsPerPage?.toString() || '30'; - defaultSortMode = settings.defaultSortMode || 'standard'; - sortPreferenceSource = settings.sortPreferenceSource || 'cleanplaats'; - await updateDarkModeStartupScript(Boolean(settings.darkMode)); - } else { - await updateDarkModeStartupScript(false); - } - } catch (error) { - console.error('Cleanplaats: Error parsing stored settings:', error, '. Using default settings.'); - await updateDarkModeStartupScript(false); - } - } - - console.log(`Cleanplaats: Initialized with settings - RPP: ${resultsPerPage}, Sort: ${defaultSortMode}, SortSource: ${sortPreferenceSource}`); - - await updateApiRequestRules(resultsPerPage, defaultSortMode); - - if (browserAPI.webRequest) { - try { - if (typeof browserAPI.webRequest.onBeforeRequest.hasListener === 'function') { - if (browserAPI.webRequest.onBeforeRequest.hasListener(rewriteHashRequests_MV2_compat)) { - browserAPI.webRequest.onBeforeRequest.removeListener(rewriteHashRequests_MV2_compat); - } - if (browserAPI.webRequest.onBeforeRequest.hasListener(rewriteApiRequests_MV2_compat)) { - browserAPI.webRequest.onBeforeRequest.removeListener(rewriteApiRequests_MV2_compat); - } - } - } catch (e) { - console.warn('Cleanplaats: Could not remove old webRequest listeners.', e); - } - } - - try { - if (browserAPI.storage.onChanged.hasListener(handleStorageChanges)) { - browserAPI.storage.onChanged.removeListener(handleStorageChanges); - } - browserAPI.storage.onChanged.addListener(handleStorageChanges); - console.log('Cleanplaats: Added storage.onChanged listener.'); - } catch (error) { - console.error('Cleanplaats: Error setting up storage listener:', error); - } - - try { - if (browserAPI.webNavigation.onBeforeNavigate.hasListener(handleHashNavigation)) { - browserAPI.webNavigation.onBeforeNavigate.removeListener(handleHashNavigation); - } - browserAPI.webNavigation.onBeforeNavigate.addListener(handleHashNavigation, { - url: WAKEUP_NAVIGATION_FILTERS - }); - console.log('Cleanplaats: Added webNavigation.onBeforeNavigate listener with wakeup filters.'); - } catch (error) { - console.error('Cleanplaats: Error setting up onBeforeNavigate listener:', error); - } - - try { - if (browserAPI.webNavigation.onHistoryStateUpdated.hasListener(handleHistoryStateUpdated)) { - browserAPI.webNavigation.onHistoryStateUpdated.removeListener(handleHistoryStateUpdated); - } - browserAPI.webNavigation.onHistoryStateUpdated.addListener(handleHistoryStateUpdated, { - url: WAKEUP_NAVIGATION_FILTERS - }); - console.log('Cleanplaats: Added webNavigation.onHistoryStateUpdated listener with wakeup filters.'); - } catch (error) { - console.error('Cleanplaats: Error setting up onHistoryStateUpdated listener:', error); - } - - console.log('Cleanplaats: All listeners registered. Ready.'); - }); -} - -browserAPI.runtime.onInstalled.addListener(async (details) => { - console.log('Cleanplaats: runtime.onInstalled event triggered, reason: ', details.reason); - if (details.reason === 'install' || details.reason === 'update') { - console.log('Cleanplaats: Extension installed or updated. Clearing old declarativeNetRequest rules.'); - try { - const existingRules = await browserAPI.declarativeNetRequest.getDynamicRules(); - const ruleIdsToRemove = existingRules.map(rule => rule.id); - if (ruleIdsToRemove.length > 0) { - await browserAPI.declarativeNetRequest.updateDynamicRules({ removeRuleIds: ruleIdsToRemove }); - console.log('Cleanplaats: Successfully cleared old dynamic rules.'); - } - } catch (error) { - console.error('Cleanplaats: Error clearing dynamic rules on install/update:', error); - } - } -}); - -function rewriteHashRequests_MV2_compat() {} -function rewriteApiRequests_MV2_compat() {} diff --git a/background/messages.js b/background/messages.js deleted file mode 100644 index 008d972..0000000 --- a/background/messages.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Background message handling and settings refresh. - */ - -function messageListener(message, sender, sendResponse) { - console.log('Cleanplaats: messageListener received message: ', message); - - if (message.action === 'keepAlive') { - console.log('Cleanplaats: Background script woken up by content script'); - resetKeepAliveToActiveMode(); - sendResponse({ status: 'acknowledged', timestamp: Date.now() }); - refreshSettingsAndRules(); - return true; - } - - if (message.action === 'forceRefresh') { - console.log('Cleanplaats: Force refresh requested'); - resetKeepAliveToActiveMode(); - refreshSettingsAndRules(); - sendResponse({ status: 'refreshed' }); - return true; - } - - return true; -} - -async function refreshSettingsAndRules() { - try { - const result = await new Promise((resolve) => { - browserAPI.storage.local.get(['cleanplaatsSettings'], resolve); - }); - - if (result.cleanplaatsSettings) { - const settings = JSON.parse(result.cleanplaatsSettings); - const newResultsPerPage = settings.resultsPerPage?.toString() || '30'; - const newDefaultSortMode = settings.defaultSortMode || 'standard'; - const newSortPreferenceSource = settings.sortPreferenceSource || 'cleanplaats'; - const darkModeEnabled = Boolean(settings.darkMode); - - let settingsChanged = false; - if (newResultsPerPage !== resultsPerPage) { - console.log(`Cleanplaats: Refreshing RPP from ${resultsPerPage} to ${newResultsPerPage}`); - resultsPerPage = newResultsPerPage; - settingsChanged = true; - } - if (newDefaultSortMode !== defaultSortMode) { - console.log(`Cleanplaats: Refreshing sort mode from ${defaultSortMode} to ${newDefaultSortMode}`); - defaultSortMode = newDefaultSortMode; - settingsChanged = true; - } - if (newSortPreferenceSource !== sortPreferenceSource) { - console.log(`Cleanplaats: Refreshing sort source from ${sortPreferenceSource} to ${newSortPreferenceSource}`); - sortPreferenceSource = newSortPreferenceSource; - settingsChanged = true; - } - - await updateDarkModeStartupScript(darkModeEnabled); - - if (settingsChanged) { - await updateApiRequestRules(resultsPerPage, defaultSortMode); - console.log('Cleanplaats: Settings and rules refreshed after wake-up'); - } - } - } catch (error) { - console.error('Cleanplaats: Error refreshing settings:', error); - } -} - -if (browserAPI.runtime.onMessage) { - if (!browserAPI.runtime.onMessage.hasListener(messageListener)) { - browserAPI.runtime.onMessage.addListener(messageListener); - } -} diff --git a/background/shared.js b/background/shared.js deleted file mode 100644 index d988eef..0000000 --- a/background/shared.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Cleanplaats background shared state and constants. - */ - -console.log('Cleanplaats background.js: Script execution started/restarted.', new Date().toISOString()); - -var browserAPI = typeof browser !== 'undefined' ? browser : chrome; - -var resultsPerPage = '30'; -var defaultSortMode = 'standard'; -var sortPreferenceSource = 'cleanplaats'; -var lastMarktplaatsActivity = Date.now(); - -var SORT_MODES = { - standard: { sortBy: 'OPTIMIZED', sortOrder: 'DECREASING' }, - date_new_old: { sortBy: 'SORT_INDEX', sortOrder: 'DECREASING' }, - date_old_new: { sortBy: 'SORT_INDEX', sortOrder: 'INCREASING' }, - price_low_high: { sortBy: 'PRICE', sortOrder: 'INCREASING' }, - price_high_low: { sortBy: 'PRICE', sortOrder: 'DECREASING' }, - distance: { sortBy: 'LOCATION', sortOrder: 'INCREASING' } -}; - -var API_RULE_ID = 1; -var HASH_URL_PATTERNS = [ - 'https://www.marktplaats.nl/l/', - 'https://www.marktplaats.nl/q/', - 'https://www.2dehands.be/l/', - 'https://www.2dehands.be/q/', - 'https://www.2ememain.be/l/', - 'https://www.2ememain.be/q/' -]; -var API_URL_PATTERNS = [ - 'https://www.marktplaats.nl/lrp/api/search*', - 'https://www.2dehands.be/lrp/api/search*', - 'https://www.2ememain.be/lrp/api/search*' -]; -var THEME_INIT_SCRIPT_ID = 'cleanplaats-theme-init'; -var THEME_MATCH_PATTERNS = [ - '*://*.marktplaats.nl/*', - '*://*.2dehands.be/*', - '*://*.2ememain.be/*' -]; -var WAKEUP_NAVIGATION_FILTERS = [ - { hostSuffix: 'marktplaats.nl' }, - { hostSuffix: '2dehands.be' }, - { hostSuffix: '2ememain.be' } -]; diff --git a/background/theme.js b/background/theme.js deleted file mode 100644 index 9e63cbf..0000000 --- a/background/theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Background theme-init registration. - */ - -async function updateDarkModeStartupScript(enabled) { - // theme-init.js is now loaded statically via manifest.json at document_start. - // Keeping this async hook as a no-op avoids browser-specific timing issues - // with runtime content-script registration, especially in Firefox. - console.log(`Cleanplaats: Startup dark-mode script is manifest-driven (${enabled ? 'enabled' : 'disabled'}).`); -} diff --git a/background/url-rules.js b/background/url-rules.js deleted file mode 100644 index 34dc4e2..0000000 --- a/background/url-rules.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Background URL rewriting and DNR rule management. - */ - -function parseHashOptions(hashStr) { - const options = {}; - if (!hashStr || hashStr.length < 2) return options; - const hashKeysValues = hashStr.substring(1).split('|'); - for (let i = 0; i < hashKeysValues.length; ++i) { - const keyValue = hashKeysValues[i].split(':'); - if (keyValue.length !== 2) continue; - options[keyValue[0]] = keyValue[1]; - } - return options; -} - -function buildHashOptions(options) { - const entries = Object.entries(options).filter(([_, v]) => v && v !== ''); - if (entries.length === 0) return ''; - let hashStr = '#'; - for (const key in options) { - if (options[key] && options[key] !== '') { - hashStr += key + ':' + options[key] + '|'; - } - } - if (hashStr.endsWith('|')) { - hashStr = hashStr.substring(0, hashStr.length - 1); - } - return hashStr; -} - -function getModifiedUrlIfNeeded(urlString, currentResultsPerPage, currentDefaultSortMode, currentSortPreferenceSource) { - const url = new URL(urlString); - const options = parseHashOptions(url.hash); - let needsRewrite = false; - const hasExplicitSort = Boolean(options.sortBy && options.sortOrder); - const shouldApplyCleanplaatsSort = currentSortPreferenceSource !== 'marketplace'; - - if (!Object.prototype.hasOwnProperty.call(options, 'limit') || options.limit !== currentResultsPerPage) { - options.limit = currentResultsPerPage; - needsRewrite = true; - } - - if (shouldApplyCleanplaatsSort && currentDefaultSortMode !== 'standard') { - const sortConfig = SORT_MODES[currentDefaultSortMode]; - if (sortConfig && (!hasExplicitSort || options.sortBy !== sortConfig.sortBy || options.sortOrder !== sortConfig.sortOrder)) { - options.sortBy = sortConfig.sortBy; - options.sortOrder = sortConfig.sortOrder; - needsRewrite = true; - } - } else if (shouldApplyCleanplaatsSort && currentDefaultSortMode === 'standard' && hasExplicitSort) { - delete options.sortBy; - delete options.sortOrder; - needsRewrite = true; - } - - if (needsRewrite) { - url.hash = buildHashOptions(options); - return url.href; - } - return null; -} - -async function updateApiRequestRules(currentResultsPerPage, currentDefaultSortMode) { - console.log(`Cleanplaats: updateApiRequestRules called with RPP: ${currentResultsPerPage}, Sort: ${currentDefaultSortMode}`); - const rulesToRemove = [API_RULE_ID]; - const rulesToAdd = []; - const shouldModifyApi = currentResultsPerPage !== '30'; - - if (shouldModifyApi) { - const rule = { - id: API_RULE_ID, - priority: 1, - action: { type: 'redirect', redirect: { transform: { queryTransform: { removeParams: [], addOrReplaceParams: [] } } } }, - condition: { urlFilter: API_URL_PATTERNS.map(p => p.replace('*', '')).join('|'), resourceTypes: ['xmlhttprequest'] } - }; - if (currentResultsPerPage !== '30') { - rule.action.redirect.transform.queryTransform.addOrReplaceParams.push({ key: 'limit', value: currentResultsPerPage }); - } - rulesToAdd.push(rule); - console.log('Cleanplaats: Adding declarativeNetRequest rule:', JSON.parse(JSON.stringify(rule))); - } else { - console.log('Cleanplaats: Removing declarativeNetRequest rule as settings are default.'); - } - try { - await browserAPI.declarativeNetRequest.updateDynamicRules({ removeRuleIds: rulesToRemove, addRules: rulesToAdd }); - console.log('Cleanplaats: declarativeNetRequest rules updated successfully.'); - } catch (error) { - console.error('Cleanplaats: Error updating declarativeNetRequest rules:', error, JSON.stringify(rulesToAdd)); - } -} - -function handleHashNavigation(details) { - if (details.frameId !== 0 || details.parentFrameId !== -1) return; - - console.log('Cleanplaats: handleHashNavigation triggered.', `URL: ${details.url}`, `Transition: ${details.transitionType}`); - - const urlMatches = HASH_URL_PATTERNS.some(pattern => details.url.startsWith(pattern)); - if (!urlMatches) { - console.log('Cleanplaats: handleHashNavigation - URL does not match HASH_URL_PATTERNS, skipping.', details.url); - return; - } - - const newUrl = getModifiedUrlIfNeeded(details.url, resultsPerPage, defaultSortMode, sortPreferenceSource); - console.log(`Cleanplaats: handleHashNavigation - Original URL: ${details.url}, Processed newUrl: ${newUrl}`); - - if (newUrl && newUrl !== details.url) { - console.log(`Cleanplaats: Rewriting URL via onBeforeNavigate from ${details.url} to ${newUrl}`); - browserAPI.tabs.update(details.tabId, { url: newUrl }); - if (details.transitionType === undefined) { - console.log(`Cleanplaats: TransitionType was undefined. Attempting follow-up reload for ${newUrl}`); - setTimeout(() => { - browserAPI.tabs.get(details.tabId, (tab) => { - if (browserAPI.runtime.lastError) { - console.warn(`Cleanplaats: Error getting tab ${details.tabId} for reload: ${browserAPI.runtime.lastError.message}`); - return; - } - if (tab && tab.url === newUrl) { - console.log(`Cleanplaats: Tab ${details.tabId} URL matches, proceeding with reload.`); - browserAPI.tabs.reload(details.tabId); - } else { - console.log(`Cleanplaats: Tab ${details.tabId} URL changed or tab closed (current: ${tab ? tab.url : 'N/A'}), skipping reload.`); - } - }); - }, 150); - } - } -} - -function handleHistoryStateUpdated(details) { - if (details.frameId !== 0 || details.parentFrameId !== -1) return; - - console.log('Cleanplaats: handleHistoryStateUpdated triggered.', `URL: ${details.url}`, `Transition: ${details.transitionType}`); - - const urlMatches = HASH_URL_PATTERNS.some(pattern => details.url.startsWith(pattern)); - if (!urlMatches) { - console.log('Cleanplaats: handleHistoryStateUpdated - URL does not match HASH_URL_PATTERNS, skipping.', details.url); - return; - } - - const newUrl = getModifiedUrlIfNeeded(details.url, resultsPerPage, defaultSortMode, sortPreferenceSource); - console.log(`Cleanplaats: handleHistoryStateUpdated - Original URL: ${details.url}, Processed newUrl: ${newUrl}`); - - if (newUrl && newUrl !== details.url) { - console.log(`Cleanplaats: Correcting URL via onHistoryStateUpdated from ${details.url} to ${newUrl}`); - browserAPI.tabs.update(details.tabId, { url: newUrl }); - } -} diff --git a/content.js b/content.js deleted file mode 100644 index 0da49c5..0000000 --- a/content.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Cleanplaats content bootstrap. - */ - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initCleanplaats); -} else { - initCleanplaats(); -} diff --git a/content/blacklist.js b/content/blacklist.js deleted file mode 100644 index fd04af5..0000000 --- a/content/blacklist.js +++ /dev/null @@ -1,469 +0,0 @@ -/** - * Content-script seller and term blacklist management. - */ - -function showTermsModal() { - const modal = document.getElementById('cleanplaats-terms-modal'); - if (!modal) return; - const panelText = getPanelLocaleText(); - - const blacklistModal = document.getElementById('cleanplaats-blacklist-modal'); - if (blacklistModal) { - blacklistModal.style.display = 'none'; - } - - if (modal.style.display === 'block') { - modal.style.display = 'none'; - return; - } - - const terms = CLEANPLAATS.settings.blacklistedTerms; - - modal.innerHTML = DOMPurify.sanitize(` -
-

${panelText.termsModalTitle}

- -
- - -
-
${panelText.termInputHelp}
- -
- `); - modal.style.display = 'block'; - - document.getElementById('cleanplaats-terms-close').onclick = () => { - modal.style.display = 'none'; - }; - - const addTerm = () => { - const input = document.getElementById('cleanplaats-term-input'); - const term = input.value.trim(); - if (term && !CLEANPLAATS.settings.blacklistedTerms.includes(term)) { - CLEANPLAATS.settings.blacklistedTerms.push(term); - saveSettings().then(() => { - input.value = ''; - updateTermsModal(); - performCleanup(); - showBlacklistTermToast(term); - }); - } - }; - - document.getElementById('cleanplaats-add-term').onclick = addTerm; - - document.getElementById('cleanplaats-term-input').addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - addTerm(); - } - }); - - setupTermsModalButtons(); -} - -function updateTermsModal() { - const modal = document.getElementById('cleanplaats-terms-modal'); - if (!modal || modal.style.display === 'none') return; - const panelText = getPanelLocaleText(); - - const terms = CLEANPLAATS.settings.blacklistedTerms; - const list = document.getElementById('cleanplaats-terms-list'); - - if (list) { - list.innerHTML = DOMPurify.sanitize( - terms.length === 0 - ? `
  • ${panelText.termsEmpty}
  • ` - : terms.map(term => ` -
  • - ${term} - -
  • - `).join('') - ); - - setupTermsModalButtons(); - } -} - -function setupTermsModalButtons() { - const panelText = getPanelLocaleText(); - document.querySelectorAll('.cleanplaats-unblacklist-term-btn').forEach(btn => { - btn.onmouseover = () => { - btn.style.background = 'green'; - btn.textContent = panelText.unhideButton; - }; - btn.onmouseout = () => { - btn.style.background = '#ff4d4d'; - btn.textContent = panelText.hiddenButton; - }; - btn.style.background = '#ff4d4d'; - btn.style.color = 'white'; - btn.onclick = () => { - const term = btn.dataset.term; - CLEANPLAATS.settings.blacklistedTerms = CLEANPLAATS.settings.blacklistedTerms.filter(t => t !== term); - saveSettings().then(() => { - updateTermsModal(); - unhideListingsByTerm(term); - performCleanup(); - showUnblacklistTermToast(term); - }); - }; - }); -} - -function unhideListingsByTerm(term) { - document.querySelectorAll('.hz-Link').forEach(link => { - const title = getListingTitleText(link); - if (title.includes(term.toLowerCase())) { - const listingEl = link.closest('.hz-StructuredListing') || link; - listingEl.removeAttribute('data-cleanplaats-hidden'); - listingEl.style.display = ''; - } - }); - document.querySelectorAll('.hz-Listing').forEach(listing => { - const title = getListingTitleText(listing); - if (title.includes(term.toLowerCase())) { - listing.removeAttribute('data-cleanplaats-hidden'); - listing.style.display = ''; - } - }); -} - -function addSellerToBlacklist(sellerName) { - addSellersToBlacklist([sellerName]); -} - -function addSellersToBlacklist(sellerNames) { - const normalizedSellerNames = sellerNames - .map(name => name.trim()) - .filter(Boolean) - .filter((name, index, arr) => arr.indexOf(name) === index) - .filter(name => !CLEANPLAATS.settings.blacklistedSellers.includes(name)); - - if (normalizedSellerNames.length === 0) return; - - CLEANPLAATS.settings.blacklistedSellers.push(...normalizedSellerNames); - saveSettings().then(() => { - performCleanup(); - injectBlacklistButtons(); - updateBlacklistModal(); - - if (normalizedSellerNames.length === 1) { - showBlacklistToast(normalizedSellerNames[0]); - return; - } - - showBulkBlacklistToast(normalizedSellerNames.length); - }); -} - -function removeSellerFromBlacklist(sellerName) { - CLEANPLAATS.settings.blacklistedSellers = CLEANPLAATS.settings.blacklistedSellers.filter(s => s !== sellerName); - saveSettings().then(() => { - document.querySelectorAll('.hz-Listing').forEach(listing => { - const sellerNameEl = listing.querySelector('.hz-Listing-seller-name, .hz-Listing-seller-name-new, .hz-Listing-seller-link, .hz-Listing-sellerName, .hz-Listing-sellerName-new'); - if (!sellerNameEl) return; - if (sellerNameEl.textContent.trim() === sellerName) { - listing.removeAttribute('data-cleanplaats-hidden'); - listing.style.display = ''; - } - }); - performCleanup(); - injectBlacklistButtons(); - updateBlacklistModal(); - }); -} - -function injectProductDetailBlacklistButton() { - const panelText = getPanelLocaleText(); - const sellerRoot = document.querySelector('.SellerInfoSmall-root'); - const sellerNameElement = sellerRoot?.querySelector('.SellerInfoSmall-name a, .SellerInfoSmall-name'); - const existingRow = document.querySelector('.cleanplaats-detail-blacklist-row'); - - if (!isProductDetailPage() || !sellerRoot || !sellerNameElement) { - existingRow?.remove(); - return; - } - - const sellerName = sellerNameElement.textContent?.trim(); - if (!sellerName) { - existingRow?.remove(); - return; - } - - const isBlacklisted = CLEANPLAATS.settings.blacklistedSellers.includes(sellerName); - const detailRow = existingRow || document.createElement('div'); - detailRow.className = 'cleanplaats-detail-blacklist-row'; - - const button = document.createElement('button'); - button.className = 'cleanplaats-blacklist-btn cleanplaats-detail-blacklist-btn'; - button.type = 'button'; - button.tabIndex = 0; - button.textContent = isBlacklisted ? panelText.hiddenSellerButton : panelText.hideSellerButton; - button.disabled = isBlacklisted; - button.setAttribute('aria-disabled', isBlacklisted ? 'true' : 'false'); - - if (!isBlacklisted) { - button.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - addSellerToBlacklist(sellerName); - }); - } - - detailRow.replaceChildren(button); - - if (!existingRow) { - sellerRoot.insertAdjacentElement('afterend', detailRow); - } -} - -function injectBlacklistButtons() { - const panelText = getPanelLocaleText(); - document.querySelectorAll('.hz-Listing').forEach(listing => { - const oldBtn = listing.querySelector('.cleanplaats-blacklist-btn-row'); - const oldTopRight = listing.querySelector('.cleanplaats-seller-topright-mobile'); - const oldInlineBtn = listing.querySelector('.cleanplaats-inline-btn'); - - let sellerName = listing.dataset.cleanplaatsSellerName || null; - let sellerElement = null; - let isCarAdvert = false; - - const carSellerElement = listing.querySelector('.hz-Listing-sellerName, .hz-Listing-sellerName-new'); - if (carSellerElement) { - sellerName = carSellerElement.textContent.trim(); - sellerElement = carSellerElement; - isCarAdvert = true; - } else { - const sellerNameEl = listing.querySelector('.hz-Listing-seller-name, .hz-Listing-seller-name-new'); - if (sellerNameEl) { - sellerName = sellerNameEl.textContent.trim(); - const sellerLink = sellerNameEl.closest('a'); - sellerElement = sellerLink ? (sellerLink.parentElement || sellerLink) : sellerNameEl; - isCarAdvert = false; - } - } - - if (sellerName) { - listing.dataset.cleanplaatsSellerName = sellerName; - } - - if (!sellerName) return; - - if (CLEANPLAATS.settings.blacklistedSellers.includes(sellerName)) { - listing.setAttribute('data-cleanplaats-hidden', 'true'); - listing.style.display = 'none'; - return; - } - - if (window.innerWidth < 700) { - if (oldTopRight && oldTopRight.dataset.cleanplaatsSellerName === sellerName) { - return; - } - - if (oldBtn) oldBtn.remove(); - if (oldInlineBtn) oldInlineBtn.remove(); - if (oldTopRight) oldTopRight.remove(); - - const topRow = document.createElement('div'); - topRow.className = 'cleanplaats-seller-topright-mobile'; - topRow.dataset.cleanplaatsSellerName = sellerName; - topRow.innerHTML = DOMPurify.sanitize(` - ${sellerName} - - `); - const content = listing.querySelector('.hz-Listing-listview-content, .hz-Listing-listview-content-new'); - if (content && content.firstChild) { - content.insertBefore(topRow, content.firstChild); - } else if (content) { - content.appendChild(topRow); - } - topRow.querySelector('.cleanplaats-blacklist-btn-mobile').onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); - if (confirm(`Wil je alle advertenties van ${sellerName} verbergen?`)) { - addSellerToBlacklist(sellerName); - } - }; - return; - } - - if (!sellerElement) return; - - if (oldBtn) oldBtn.remove(); - if (oldTopRight) oldTopRight.remove(); - if (oldInlineBtn) oldInlineBtn.remove(); - - if (isCarAdvert) { - carSellerElement.style.display = 'inline-flex'; - carSellerElement.style.alignItems = 'center'; - carSellerElement.style.gap = '8px'; - - const btn = document.createElement('button'); - btn.className = 'cleanplaats-blacklist-btn cleanplaats-inline-btn'; - btn.textContent = panelText.hideSellerButton; - btn.type = 'button'; - btn.tabIndex = 0; - btn.style.marginLeft = '8px'; - - btn.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - addSellerToBlacklist(sellerName); - }); - - carSellerElement.appendChild(btn); - } else { - const btnRow = document.createElement('div'); - btnRow.className = 'cleanplaats-blacklist-btn-row'; - - const btn = document.createElement('button'); - btn.className = 'cleanplaats-blacklist-btn'; - btn.textContent = panelText.hideSellerButton; - btn.type = 'button'; - btn.tabIndex = 0; - - btn.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - addSellerToBlacklist(sellerName); - }); - - btnRow.appendChild(btn); - - if (sellerElement.parentNode) { - sellerElement.parentNode.insertBefore(btnRow, sellerElement.nextSibling); - } - } - }); - - injectProductDetailBlacklistButton(); -} - -function showBlacklistModal() { - const modal = document.getElementById('cleanplaats-blacklist-modal'); - if (!modal) return; - const panelText = getPanelLocaleText(); - - const termsModal = document.getElementById('cleanplaats-terms-modal'); - if (termsModal) { - termsModal.style.display = 'none'; - } - - if (modal.style.display === 'block') { - modal.style.display = 'none'; - return; - } - - const sellers = CLEANPLAATS.settings.blacklistedSellers; - - modal.innerHTML = DOMPurify.sanitize(` -
    -

    ${panelText.sellersModalTitle}

    - -
    - - -
    -
    ${panelText.sellerInputHelp}
    - -
    - `); - modal.style.display = 'block'; - - document.getElementById('cleanplaats-blacklist-close').onclick = () => { - modal.style.display = 'none'; - }; - - const addSeller = () => { - const input = document.getElementById('cleanplaats-seller-input'); - const sellerNames = input.value - .split(/[;,]+/) - .map(name => name.trim()) - .filter(Boolean); - - if (sellerNames.length === 0) return; - - addSellersToBlacklist(sellerNames); - input.value = ''; - }; - - document.getElementById('cleanplaats-add-seller').onclick = addSeller; - - document.getElementById('cleanplaats-seller-input').addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - addSeller(); - } - }); - - setupBlacklistModalButtons(); -} - -function updateBlacklistModal() { - const modal = document.getElementById('cleanplaats-blacklist-modal'); - if (!modal || modal.style.display === 'none') return; - const panelText = getPanelLocaleText(); - - const sellers = CLEANPLAATS.settings.blacklistedSellers; - const list = document.getElementById('cleanplaats-blacklist-list'); - - if (list) { - list.innerHTML = DOMPurify.sanitize( - sellers.length === 0 - ? `
  • ${panelText.sellersEmpty}
  • ` - : sellers.map(seller => ` -
  • - ${seller} - -
  • - `).join('') - ); - - setupBlacklistModalButtons(); - } -} - -function setupBlacklistModalButtons() { - const panelText = getPanelLocaleText(); - document.querySelectorAll('.cleanplaats-unblacklist-btn').forEach(btn => { - btn.onmouseover = () => { - btn.style.background = 'green'; - btn.textContent = panelText.unhideButton; - }; - btn.onmouseout = () => { - btn.style.background = '#ff4d4d'; - btn.textContent = panelText.hiddenButton; - }; - btn.style.background = '#ff4d4d'; - btn.style.color = 'white'; - btn.onclick = () => { - const sellerName = btn.dataset.seller; - showUnblacklistToast(sellerName); - removeSellerFromBlacklist(sellerName); - }; - }); -} diff --git a/content/cleanup.js b/content/cleanup.js deleted file mode 100644 index 021602e..0000000 --- a/content/cleanup.js +++ /dev/null @@ -1,524 +0,0 @@ -/** - * Content-script cleanup and filtering routines. - */ - -function getListingTitleElement(container) { - if (!(container instanceof Element)) return null; - - return container.querySelector([ - '.hz-StructuredListing-title', - '.hz-Listing-title', - '.hz-Listing-group--title-description', - '.hz-StructuredListing-body', - '[class*="ListingTitle_hz-Listing-title"]', - '[class*="ListingTitle_hz-StructuredListing-title"]' - ].join(', ')); -} - -function getListingTitleText(container) { - const titleElement = getListingTitleElement(container); - return titleElement?.textContent?.trim().toLowerCase() || ''; -} - -function updateStatsDisplay() { - if (!CLEANPLAATS.featureFlags.showStats) return; - - const stats = CLEANPLAATS.stats; - - updateElementText('cleanplaats-topads-count', stats.topAdsRemoved); - updateElementText('cleanplaats-dagtoppers-count', stats.dagtoppersRemoved); - updateElementText('cleanplaats-promoted-count', stats.promotedListingsRemoved); - updateElementText('cleanplaats-stickers-count', stats.opvalStickersRemoved); - updateElementText('cleanplaats-otherads-count', stats.otherAdsRemoved); - - const total = stats.topAdsRemoved + stats.dagtoppersRemoved + stats.promotedListingsRemoved + stats.opvalStickersRemoved + stats.otherAdsRemoved; - stats.totalRemoved = total; - - updateElementText('cleanplaats-total-count-stats', total); - // Header total-removed badge disabled for now. - // updateElementText('cleanplaats-total-count', total); -} - -function updateElementText(id, value) { - const element = document.getElementById(id); - if (element) { - element.textContent = value; - } -} - -function performInitialCleanup() { - try { - performCleanup(); - } catch (error) { - console.error('Cleanplaats: Initial cleanup failed', error); - } -} - -function performCleanup() { - removeAllAds(); - removePersistentGoogleAds(); - if (CLEANPLAATS.settings.removeFavoriteRelatedAds) removeSimilarAdsSections(); - removeNonFeatureBuyerBanner(); - - if (CLEANPLAATS.settings.removeTopAds) removeTopAdvertisements(); - if (CLEANPLAATS.settings.removeDagtoppers) removeDagtoppers(); - if (CLEANPLAATS.settings.removePromotedListings) removePromotedListings(); - if (CLEANPLAATS.settings.removeOpvalStickers) removeOpvalStickerListings(); - if (CLEANPLAATS.settings.removeReservedListings) removeReservedListings(); - - document.querySelectorAll('.hz-Listing').forEach(listing => { - const sellerNameEl = listing.querySelector('.hz-Listing-seller-name, .hz-Listing-seller-name-new, .hz-Listing-seller-link, .hz-Listing-sellerName, .hz-Listing-sellerName-new'); - if (!sellerNameEl) return; - const sellerName = sellerNameEl.textContent.trim(); - if (CLEANPLAATS.settings.blacklistedSellers.includes(sellerName)) { - listing.setAttribute('data-cleanplaats-hidden', 'true'); - listing.style.display = 'none'; - } - }); - - document.querySelectorAll('.hz-Link').forEach(link => { - const title = getListingTitleText(link); - if (!title) return; - CLEANPLAATS.settings.blacklistedTerms.forEach(term => { - if (title.includes(term.toLowerCase())) { - const listingEl = link.closest('.hz-StructuredListing') || link; - listingEl.setAttribute('data-cleanplaats-hidden', 'true'); - listingEl.style.display = 'none'; - } - }); - }); - - document.querySelectorAll('.hz-Listing').forEach(listing => { - const title = getListingTitleText(listing); - if (!title) return; - CLEANPLAATS.settings.blacklistedTerms.forEach(term => { - if (title.includes(term.toLowerCase())) { - listing.setAttribute('data-cleanplaats-hidden', 'true'); - listing.style.display = 'none'; - } - }); - }); - - updateStatsDisplay(); -} - -function resetPreviousChanges() { - resetStats(); - - document.querySelectorAll('[data-cleanplaats-hidden]').forEach(el => { - try { - el.style.cssText = el.getAttribute('data-original-style') || ''; - el.removeAttribute('data-cleanplaats-hidden'); - el.removeAttribute('data-original-style'); - } catch (error) { - console.error('Cleanplaats: Error restoring element', error); - } - }); -} - -function removeTopAdvertisements() { - const is2dehands = location.hostname.includes('2dehands.be'); - const is2ememain = location.hostname.includes('2ememain.be'); - const labels = is2ememain ? ['Pub au top'] : is2dehands ? ['Topzoekertje', 'Topadvertentie'] : ['Topadvertentie']; - const priorityBadgeSelector = [ - '.hz-Listing-priority span', - '.hz-Listing-priority-new', - '[class*="hz-Listing-priority-new"]' - ].join(', '); - const removedCount = labels.reduce((total, label) => { - return total + findAndHideListings(priorityBadgeSelector, label); - }, 0); - CLEANPLAATS.stats.topAdsRemoved += removedCount; -} - -function removeDagtoppers() { - const priorityBadgeSelector = [ - '.hz-Listing-priority span', - '.hz-Listing-priority-new', - '[class*="hz-Listing-priority-new"]' - ].join(', '); - const removedCount = findAndHideListings(priorityBadgeSelector, 'Dagtopper'); - CLEANPLAATS.stats.dagtoppersRemoved += removedCount; -} - -function removePromotedListings() { - let count = 0; - const visitWebsiteLabels = location.hostname.includes('2ememain.be') - ? ['Visiter le site internet'] - : ['Bezoek website']; - - const selectors = [ - '.hz-Listing-seller-link', - '.hz-Listing-seller-external-link' - ]; - - selectors.forEach(selector => { - document.querySelectorAll(selector).forEach(sellerLink => { - try { - const hasVisitWebsite = Array.from(sellerLink.querySelectorAll('span, a')) - .some(el => visitWebsiteLabels.includes(el.textContent?.trim())); - - if (hasVisitWebsite) { - const listing = sellerLink.closest('.hz-Listing'); - if (listing && !listing.hasAttribute('data-cleanplaats-hidden') && hideElement(listing)) { - count++; - } - } - } catch (error) { - console.error('Cleanplaats: Error processing promoted listing', error); - } - }); - }); - - document.querySelectorAll('.hz-StructuredListing').forEach(listing => { - try { - if (listing.hasAttribute('data-cleanplaats-hidden') || !isHomepagePartnerListing(listing)) { - return; - } - - if (hideElement(listing)) { - count++; - } - } catch (error) { - console.error('Cleanplaats: Error processing homepage partner listing', error); - } - }); - - CLEANPLAATS.stats.promotedListingsRemoved += count; -} - -function isHomepagePartnerListing(listing) { - const hrefs = Array.from(listing.querySelectorAll('a[href]')) - .map(link => link.href || link.getAttribute('href') || '') - .filter(Boolean); - - return hrefs.some(href => /\/a\d+(?:[-/?]|$)/i.test(href)); -} - -function removeOpvalStickerListings() { - let count = 0; - const stickerSelectors = [ - '.hz-Listing-Opvalsticker-wrapper, .hz-Listing-Opvalsticker-wrapper-new', - '[data-testid="listing-opval-sticker"]' - ]; - - stickerSelectors.forEach(selector => { - document.querySelectorAll(selector).forEach(sticker => { - try { - const listing = sticker.closest('.hz-Listing'); - if (listing && !listing.hasAttribute('data-cleanplaats-hidden') && hideElement(listing)) { - count++; - } - } catch (error) { - console.error('Cleanplaats: Error processing sticker listing', error); - } - }); - }); - - CLEANPLAATS.stats.opvalStickersRemoved += count; -} - -function removeReservedListings() { - const count = findAndHideListings('.hz-Listing-price, [class*="ListingPrice_hz-Listing-price"]', [ - 'gereserveerd', - 'réservé' - ]); - CLEANPLAATS.stats.otherAdsRemoved += count; -} - -function removeAllAds() { - let count = 0; - const marktplaatsMarketingBannerSelector = '.MpCard-mpCardBanner, img[alt="Marktplaats Marketing Banner"]'; - const marktplaatsMarketingBannerWrapperSelector = 'div[role="button"][tabindex]'; - const getMarktplaatsMarketingBannerContainer = element => { - if (!(element instanceof Element)) { - return null; - } - - const bannerCard = element.closest('.MpCard-mpCardBanner'); - if (bannerCard) { - const bannerWrapper = bannerCard.closest(marktplaatsMarketingBannerWrapperSelector); - if (bannerWrapper?.querySelector(marktplaatsMarketingBannerSelector)) { - return bannerWrapper; - } - - return bannerCard; - } - - const bannerWrapper = element.closest(marktplaatsMarketingBannerWrapperSelector); - if (bannerWrapper?.querySelector(marktplaatsMarketingBannerSelector)) { - return bannerWrapper; - } - - return element.closest('img[alt="Marktplaats Marketing Banner"]'); - }; - const isMarktplaatsSponsoredNotice = element => { - if (!element) return false; - - const text = (element.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase(); - return text.includes('de volgorde van de resultaten wordt mede bepaald door betaalde opvalmogelijkheden'); - }; - const isMarktplaatsMarketingBanner = element => { - if (!element) return false; - - if ( - element.matches?.('.MpCard-mpCardBanner') || - element.querySelector?.(marktplaatsMarketingBannerSelector) - ) { - return true; - } - - const bannerImage = element.querySelector?.('img[alt="Marktplaats Marketing Banner"]'); - return Boolean(bannerImage); - }; - - function safeHide(selector) { - try { - const elements = document.querySelectorAll(selector); - elements.forEach(el => { - if (!el.hasAttribute('data-cleanplaats-hidden') && hideElement(el)) { - count++; - } - - const parentLi = el.closest('li.bannerContainerLoading'); - if (parentLi && !parentLi.hasAttribute('data-cleanplaats-hidden')) { - hideElement(parentLi); - } - - const feedBanner = el.closest('.hz-FeedBannerBlock, .Banners-bannerFeedItem'); - if (feedBanner && !feedBanner.hasAttribute('data-cleanplaats-hidden')) { - hideElement(feedBanner); - } - - const topBanner = el.closest('.BannerTop-root, #top-banner-root'); - if (topBanner && !topBanner.hasAttribute('data-cleanplaats-hidden')) { - hideElement(topBanner); - } - }); - } catch (error) { - console.log('Cleanplaats: Error hiding ads', error); - } - } - - document.querySelectorAll('.hz-Listing-imageOverlayLabel').forEach(overlay => { - if (overlay.textContent.trim() === 'Homepagina-advertentie') { - const link = overlay.closest('.hz-Link.hz-Link--block'); - if (link && !link.hasAttribute('data-cleanplaats-hidden')) { - hideElement(link); - count++; - } - } - }); - - const adSelectors = [ - '#adsense-root', - '#adsense-container', - '#adsense-container-bottom-lazy', - '#similar-items-root', - '.AdmarktSimilarItemsContainer', - '.AdmarktSimilarItems-root', - '.AdmarktSimilarItems-headerTitle', - '#adBlock', - '.ndfc-wrapper[data-testid="ndfc-generic-text"]', - '[data-testid="ndfc-close"]', - '.MpCard-mpCardBanner', - 'div[role="button"][tabindex] > .MpCard-mpCardBanner', - 'img[alt="Marktplaats Marketing Banner"]', - '.hz-Banner', - '.hz-Banner--fluid', - '.BannerTop-root', - '#banner-rubrieks-dt', - '#banner-top-dt', - '#banner-top-dt-container', - '#top-banner-root', - '[data-google-query-id]', - '[id*="google_ads_iframe"]', - '[id*="google_ads_top_frame"]', - '[aria-label="Advertisement"]', - '[title="3rd party ad content"]', - '.i_.div', - '[data-ad-container]', - '[data-bg="true"]', - '[class*="adsbygoogle"]', - 'ins.adsbygoogle', - 'iframe[src*="googleads"]', - 'iframe[src*="doubleclick"]', - '[id*="div-gpt-ad"]', - '.hz-Listings__container--cas[data-testid="BottomBlockLazyListings"]', - '[class*="creative"]', - '#google_ads_top_frame', - '.creative', - 'li.bannerContainerLoading', - '.bannerContainerLoading', - '.bannerContainerLoading .hz-Banner', - '.bannerContainerLoading .hz-Banner--fluid' - ]; - - adSelectors.forEach(selector => { - safeHide(selector); - }); - - document.querySelectorAll('.ndfc-wrapper, [data-testid="ndfc-generic-text"]').forEach(notice => { - if (isMarktplaatsSponsoredNotice(notice) && hideElement(notice)) { - count++; - } - }); - - document.querySelectorAll('.MpCard-mpCardBanner, img[alt="Marktplaats Marketing Banner"]').forEach(banner => { - const bannerCard = getMarktplaatsMarketingBannerContainer(banner) || banner; - if (isMarktplaatsMarketingBanner(bannerCard) && hideElement(bannerCard)) { - count++; - } - - const bannerWrapper = bannerCard.parentElement; - if ( - bannerWrapper instanceof Element && - bannerWrapper !== bannerCard && - bannerWrapper.childElementCount === 1 && - !bannerWrapper.hasAttribute('data-cleanplaats-hidden') - ) { - hideElement(bannerWrapper); - } - }); - - CLEANPLAATS.stats.otherAdsRemoved += count; -} - -function removePersistentGoogleAds() { - let count = 0; - - document.querySelectorAll('#adsense-root, .creative, div[id^="google_ads_iframe"], div[data-google-query-id], div[aria-label="Advertisement"]').forEach(ad => { - try { - const gridItem = ad.closest('.hz-Link.hz-Link--block'); - if (gridItem && gridItem.parentNode) { - gridItem.parentNode.removeChild(gridItem); - count++; - return; - } - if (ad.parentNode) { - ad.parentNode.removeChild(ad); - count++; - } - } catch (error) { - console.error('Cleanplaats: Error removing persistent ad', error); - } - }); - - document.querySelectorAll('#banner-right-container').forEach(banner => { - if (banner.parentNode) { - banner.parentNode.removeChild(banner); - count++; - } - }); - - document.querySelectorAll('#banner-top-dt-container').forEach(container => { - if (container.parentNode) { - container.parentNode.removeChild(container); - count++; - } - }); - - document.querySelectorAll('.BannerTop-root').forEach(banner => { - const hasAdContent = banner.querySelector( - '.hz-Banner, .hz-Banner--fluid, iframe, [data-google-query-id], [id*="google_ads_iframe"], ins.adsbygoogle' - ); - if (!hasAdContent && banner.parentNode) { - banner.parentNode.removeChild(banner); - count++; - } - }); - - document.querySelectorAll('#top-banner-root').forEach(container => { - const hasVisibleContent = Array.from(container.children).some(child => child.offsetParent !== null); - if (!hasVisibleContent && container.parentNode) { - container.parentNode.removeChild(container); - count++; - } - }); - - document.querySelectorAll('.hz-FeedBannerBlock, .Banners-bannerFeedItem').forEach(banner => { - if ( - banner.childElementCount === 0 || - Array.from(banner.children).every(child => child.offsetParent === null) - ) { - if (banner.parentNode) { - banner.parentNode.removeChild(banner); - count++; - } - } - }); - - CLEANPLAATS.stats.otherAdsRemoved += count; -} - -function removeSimilarAdsSections() { - let count = 0; - - document.querySelectorAll('.SimilarAdsList-related-ads-section').forEach(section => { - if (hideElement(section)) { - count++; - } - }); - - CLEANPLAATS.stats.otherAdsRemoved += count; -} - -function removeNonFeatureBuyerBanner() { - let count = 0; - - document.querySelectorAll( - '#notifications-root, .NonFeatureBuyerBanner-root, .feature-banner[data-testid="50-percent-off-banner"]' - ).forEach(element => { - const banner = element.id === 'notifications-root' - ? element - : element.closest('#notifications-root') - || element.closest('.feature-banner[data-testid="50-percent-off-banner"]') - || element; - - if (hideElement(banner)) { - count++; - } - }); - - CLEANPLAATS.stats.otherAdsRemoved += count; -} - -function findAndHideListings(selector, textContent) { - let count = 0; - const expectedTexts = Array.isArray(textContent) - ? textContent.map(text => text.trim().toLowerCase()) - : [textContent.trim().toLowerCase()]; - - try { - document.querySelectorAll(selector).forEach(el => { - const elementText = el.textContent?.trim().toLowerCase(); - if (elementText && expectedTexts.includes(elementText)) { - const listing = el.closest('.hz-Listing'); - if (listing && !listing.hasAttribute('data-cleanplaats-hidden') && hideElement(listing)) { - count++; - } - } - }); - } catch (error) { - console.error(`Cleanplaats: Error finding "${textContent}" listings`, error); - } - - return count; -} - -function hideElement(element) { - if (!element || element.hasAttribute('data-cleanplaats-hidden')) { - return false; - } - - try { - element.setAttribute('data-original-style', element.style.cssText); - element.setAttribute('data-cleanplaats-hidden', 'true'); - element.style.display = 'none !important'; - - return true; - } catch (error) { - console.error('Cleanplaats: Error hiding element', error); - return false; - } -} diff --git a/content/init.js b/content/init.js deleted file mode 100644 index cc336ea..0000000 --- a/content/init.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Content-script initialization and background wake-up. - */ - -function wakeUpBackground() { - try { - browserAPI.runtime.sendMessage({ action: 'keepAlive' }, (response) => { - if (browserAPI.runtime.lastError) { - console.log('Cleanplaats: Background script not responding, this is normal if it was sleeping'); - setTimeout(() => { - try { - browserAPI.runtime.sendMessage({ action: 'forceRefresh' }, () => { - if (!browserAPI.runtime.lastError) { - console.log('Cleanplaats: Background script force-refreshed successfully'); - } - }); - } catch (e) { - console.log('Cleanplaats: Force refresh also failed:', e); - } - }, 100); - } else { - console.log('Cleanplaats: Background script is awake', response); - } - }); - } catch (error) { - console.log('Cleanplaats: Could not wake background script:', error); - } -} - -function setupPeriodicWakeUp() { - if (typeof browser !== 'undefined') { - console.log('Cleanplaats: Setting up periodic background wake-up for Firefox'); - - setInterval(() => { - if (isSearchResultsPage()) { - wakeUpBackground(); - } - }, 30000); - - ['click', 'scroll', 'keydown'].forEach(eventType => { - document.addEventListener(eventType, () => { - if (isSearchResultsPage()) { - clearTimeout(window.cleanplaatsWakeUpTimeout); - window.cleanplaatsWakeUpTimeout = setTimeout(wakeUpBackground, 1000); - } - }, { passive: true }); - }); - } -} - -function checkFirstRun() { - return new Promise(resolve => { - browserAPI.storage.local.get('firstRun', (items) => { - if (browserAPI.runtime.lastError) { - console.error('Cleanplaats: Error checking first run:', browserAPI.runtime.lastError); - resolve(true); - return; - } - - let isFirstRun; - if (items.firstRun === undefined) { - isFirstRun = true; - } else { - isFirstRun = items.firstRun; - } - - if (isFirstRun) { - browserAPI.storage.local.set({ firstRun: false }, () => { - if (browserAPI.runtime.lastError) { - console.error('Cleanplaats: Error setting first run flag:', browserAPI.runtime.lastError); - } - resolve(isFirstRun); - }); - } else { - resolve(isFirstRun); - } - }); - }); -} - -function getExtensionVersion() { - try { - if (browserAPI?.runtime?.getManifest) { - const manifest = browserAPI.runtime.getManifest(); - if (manifest && typeof manifest.version === 'string') { - return manifest.version; - } - } - } catch (error) { - console.error('Cleanplaats: Failed to read extension version', error); - } - - return ''; -} - -function initCleanplaats() { - console.log('Cleanplaats: Initializing...'); - - const currentVersion = getExtensionVersion(); - - wakeUpBackground(); - setupPeriodicWakeUp(); - - loadSettings() - .then(() => { - registerSettingsStorageSync(); - applyDarkModeToDocument(CLEANPLAATS.settings.darkMode); - - checkFirstRun() - .then(isFirstRun => { - CLEANPLAATS.featureFlags.firstRun = isFirstRun; - - createControlPanel(); - setupWebchatCollisionAvoidance(); - setupAllObservers(); - applySettings(); - scheduleSellerAgeWarningCheck({ resetState: true }); - showOnboarding(currentVersion); - - const tryCleanup = () => { - if (document.querySelector('.hz-Listing') || document.querySelector('#adsense-container')) { - performInitialCleanup(); - injectBlacklistButtons(); - setTimeout(checkForEmptyPage, 300); - setTimeout(updateStatsDisplay, 500); - - let attempts = 0; - const maxAttempts = 10; - const interval = setInterval(() => { - removePersistentGoogleAds(); - - document.querySelectorAll('#banner-top-dt').forEach(banner => { - if (banner.parentNode) { - banner.parentNode.removeChild(banner); - } - }); - - document.body.offsetHeight; - attempts++; - if ( - (!document.querySelector('#banner-right-container') && !document.querySelector('#banner-top-dt')) || - attempts >= maxAttempts - ) { - clearInterval(interval); - } - }, 80); - } else { - setTimeout(tryCleanup, 60); - } - }; - tryCleanup(); - }); - }) - .catch(error => { - console.error('Cleanplaats: Initialization failed', error); - }); -} diff --git a/content/notifications.js b/content/notifications.js deleted file mode 100644 index 51bd569..0000000 --- a/content/notifications.js +++ /dev/null @@ -1,513 +0,0 @@ -/** - * Content-script notifications, onboarding, and lightweight feedback UI. - */ - -function showFirstTimeOnboarding() { - const onboarding = document.createElement('div'); - onboarding.className = 'cleanplaats-onboarding'; - onboarding.id = 'cleanplaats-onboarding'; - - onboarding.innerHTML = DOMPurify.sanitize(` -
    -
    -

    🎉 Welkom bij Cleanplaats!

    - -
    -
    -
    - 1 -

    Cleanplaats verwijdert automatisch advertenties en promotionele content

    -
    -
    - 2 -

    Gebruik het configuratiescherm rechtsonder om de filtering aan te passen. Je opent en sluit het paneel via het pijltje bovenin.

    -
    -
    - 3 -

    Bekijk statistieken over verwijderde items in het configuratiescherm

    -
    -
    - -
    - `); - - document.body.appendChild(onboarding); - - ['cleanplaats-onboarding-close', 'cleanplaats-onboarding-got-it'].forEach(id => { - document.getElementById(id)?.addEventListener('click', () => { - onboarding.classList.add('cleanplaats-fade-out'); - setTimeout(() => onboarding.remove(), 300); - }); - }); - - setTimeout(() => { - if (onboarding.parentNode) { - onboarding.classList.add('cleanplaats-fade-out'); - setTimeout(() => onboarding.remove(), 300); - } - }, 15000); -} - -function shouldShowUpdatePopup(currentVersion) { - if (!currentVersion) { - return false; - } - - return CLEANPLAATS.panelState.lastSeenVersion !== currentVersion; -} - -function showUpdatePopup(version) { - const existingPopup = document.getElementById('cleanplaats-update-popup'); - if (existingPopup) { - existingPopup.remove(); - } - - const updateContent = CLEANPLAATS_UPDATE_NOTES[version] || { - intro: 'Cleanplaats heeft een nieuwe update gekregen met verbeteringen en onderhoud aan de extensie.', - highlights: [ - 'Diverse verbeteringen en fixes voor de huidige resultaatpagina’s.', - 'Kleine verfijningen aan het paneel en de filtering.', - 'Onderhoudswerk om Cleanplaats stabiel te houden op nieuwe sitewijzigingen.' - ], - note: 'Zie je een probleem of heb je een idee? Gebruik de GitHub-link in het paneel.' - }; - - const popup = document.createElement('div'); - popup.className = 'cleanplaats-info-overlay cleanplaats-info-overlay--visible'; - popup.id = 'cleanplaats-update-popup'; - popup.setAttribute('role', 'dialog'); - popup.setAttribute('aria-modal', 'true'); - popup.setAttribute('aria-hidden', 'false'); - - const stepsMarkup = updateContent.highlights - .map(step => `
  • ${step}
  • `) - .join(''); - - popup.innerHTML = DOMPurify.sanitize(` -
    -
    - - Nieuwe update -

    Wat is er nieuw? (${version})

    -

    ${updateContent.intro}

    -
    -
      ${stepsMarkup}
    -

    ${updateContent.note}

    - -
    - `); - - const closePopup = () => { - popup.classList.remove('cleanplaats-info-overlay--visible'); - popup.setAttribute('aria-hidden', 'true'); - setTimeout(() => popup.remove(), 200); - document.removeEventListener('keydown', handleKeydown); - }; - - const handleKeydown = (event) => { - if (event.key === 'Escape') { - closePopup(); - } - }; - - popup.addEventListener('click', (event) => { - if (event.target === popup) { - closePopup(); - } - }); - - document.addEventListener('keydown', handleKeydown); - document.body.appendChild(popup); - const popupLogo = document.getElementById('cleanplaats-update-popup-logo'); - if (popupLogo) { - popupLogo.src = browserAPI.runtime.getURL('icons/icon128.png'); - } - document.getElementById('cleanplaats-update-popup-close')?.addEventListener('click', () => { - closePopup(); - showBubbleNotification(`Veel plezier met ${version}`); - }); -} - -function showWelcomeToast() { - if (CLEANPLAATS.panelState.hasShownWelcomeToast || - location.pathname !== '/' || - location.hostname !== 'www.marktplaats.nl') { - return; - } - - const toast = document.createElement('div'); - toast.className = 'cleanplaats-toast'; - toast.id = 'cleanplaats-toast'; - - const totalRemoved = CLEANPLAATS.stats.totalRemoved; - const message = totalRemoved > 0 - ? `Cleanplaats is actief (${totalRemoved} items verwijderd)` - : 'Cleanplaats is actief'; - - toast.innerHTML = DOMPurify.sanitize(` -
    - - ${message} -
    - `); - - document.body.appendChild(toast); - setTimeout(() => toast.classList.add('visible'), 100); - setTimeout(() => { - toast.classList.remove('visible'); - setTimeout(() => toast.remove(), 300); - }, 3000); - - CLEANPLAATS.panelState.hasShownWelcomeToast = true; -} - -function showOnboarding(currentVersion = '') { - if (CLEANPLAATS.featureFlags.firstRun) { - if (currentVersion) { - CLEANPLAATS.panelState.lastSeenVersion = currentVersion; - saveSettings().catch(error => { - console.error('Cleanplaats: Failed to store initial version state', error); - }); - } - showFirstTimeOnboarding(); - } else if (shouldShowUpdatePopup(currentVersion)) { - CLEANPLAATS.panelState.lastSeenVersion = currentVersion; - saveSettings().catch(error => { - console.error('Cleanplaats: Failed to store seen update version', error); - }); - showUpdatePopup(currentVersion); - } else { - showWelcomeToast(); - } -} - -function checkForEmptyPage() { - clearTimeout(notificationTimeout); - - notificationTimeout = setTimeout(() => { - performCleanup(); - - const visibleListings = document.querySelectorAll('.hz-Listing:not([data-cleanplaats-hidden])'); - const totalListings = document.querySelectorAll('.hz-Listing'); - const hiddenCount = totalListings.length - visibleListings.length; - - if (hiddenCount === 0) return; - - clearAllNotifications(); - - if (visibleListings.length === 0) { - showBubbleNotification('De pagina is leeg omdat deze helemaal uit advertenties bestond! Probeer een volgende pagina of wijzig de filters.'); - } else if (visibleListings.length < 5) { - const listingWord = visibleListings.length === 1 ? 'resultaat' : 'resultaten'; - const removedWord = hiddenCount === 1 ? 'advertentie' : 'advertenties'; - showBubbleNotification(`Er ${visibleListings.length === 1 ? 'is' : 'zijn'} nog ${visibleListings.length} ${listingWord} over nadat Cleanplaats ${hiddenCount} ${removedWord} heeft verwijderd.`); - } - }, 1000); -} - -function showBubbleNotification(message) { - let toast = document.getElementById('cleanplaats-bubble-notification'); - - if (toast) { - const messageElement = toast.querySelector('.cleanplaats-toast-message span'); - if (messageElement) { - messageElement.textContent = message; - } - } else { - toast = document.createElement('div'); - toast.className = 'cleanplaats-blacklist-toast'; - toast.id = 'cleanplaats-bubble-notification'; - - toast.innerHTML = DOMPurify.sanitize(` -
    - -
    - ${message} -
    -
    - `); - - document.body.appendChild(toast); - setTimeout(() => requestAnimationFrame(() => toast.classList.add('visible')), 0); - } - - if (toast.timeoutId) { - clearTimeout(toast.timeoutId); - } - - toast.timeoutId = setTimeout(() => { - toast.classList.remove('visible'); - setTimeout(() => { - if (toast) { - toast.remove(); - } - }, 300); - }, 5000); -} - -function clearSellerAgeWarningToast() { - const toast = document.getElementById('cleanplaats-seller-age-warning-toast'); - if (toast) { - toast.classList.remove('visible'); - setTimeout(() => { - toast.remove(); - }, 300); - } -} - -function getSellerAgeWarningThresholdLabel() { - const panelText = getPanelLocaleText(); - const value = Math.max(1, parseInt(CLEANPLAATS.settings.sellerAgeWarningThresholdValue, 10) || 1); - const unit = CLEANPLAATS.settings.sellerAgeWarningThresholdUnit; - const unitLabel = panelText.sellerAgeWarningThresholdUnits[unit] || panelText.sellerAgeWarningThresholdUnits.months; - - return `${value} ${unitLabel}`; -} - -function getSellerAgeInfoFromPage() { - const sellerRows = Array.from(document.querySelectorAll('.SellerInfoSmall-root .SellerInfoSmall-row')); - const sellerAgeRow = sellerRows.find(row => parseSellerAgeToDays(row.textContent) !== null); - const sellerNameElement = document.querySelector('.SellerInfoSmall-root .SellerInfoSmall-name a, .SellerInfoSmall-root .SellerInfoSmall-name'); - const sellerAgeText = sellerAgeRow?.textContent?.trim() || ''; - const sellerName = sellerNameElement?.textContent?.trim() || 'Deze verkoper'; - const sellerAgeDays = parseSellerAgeToDays(sellerAgeText); - - if (!sellerAgeText || sellerAgeDays === null) { - return null; - } - - return { - sellerName, - sellerAgeText, - sellerAgeDays - }; -} - -function showSellerAgeWarningToast({ sellerName, sellerAgeText }) { - const panelText = getPanelLocaleText(); - const thresholdLabel = getSellerAgeWarningThresholdLabel(); - - clearSellerAgeWarningToast(); - - const toast = document.createElement('div'); - toast.className = 'cleanplaats-blacklist-toast cleanplaats-blacklist-toast-warning'; - toast.id = 'cleanplaats-seller-age-warning-toast'; - - toast.innerHTML = DOMPurify.sanitize(` -
    - ! -
    - ${panelText.sellerAgeWarningToastTitle} - ${panelText.sellerAgeWarningToastMessage(sellerName, sellerAgeText, thresholdLabel)} -
    -
    - `); - - document.body.appendChild(toast); - setTimeout(() => { - requestAnimationFrame(() => toast.classList.add('visible')); - }, 50); - - toast.timeoutId = setTimeout(() => { - toast.classList.remove('visible'); - setTimeout(() => toast.remove(), 300); - }, 5200); -} - -function maybeShowSellerAgeWarning(options = {}) { - const force = options.force === true; - - if (!isProductDetailPage()) { - clearSellerAgeWarningToast(); - return; - } - - if (!CLEANPLAATS.settings.sellerAgeWarningEnabled) { - clearSellerAgeWarningToast(); - return; - } - - const sellerAgeInfo = getSellerAgeInfoFromPage(); - if (!sellerAgeInfo) { - clearSellerAgeWarningToast(); - return; - } - - const thresholdDays = getSellerAgeWarningThresholdDays(); - if (sellerAgeInfo.sellerAgeDays >= thresholdDays) { - clearSellerAgeWarningToast(); - return; - } - - const warningKey = `${location.pathname}|${sellerAgeInfo.sellerAgeText}|${thresholdDays}`; - if (!force && CLEANPLAATS.runtime.lastSellerAgeWarningKey === warningKey) { - return; - } - - CLEANPLAATS.runtime.lastSellerAgeWarningKey = warningKey; - showSellerAgeWarningToast(sellerAgeInfo); -} - -function scheduleSellerAgeWarningCheck(options = {}) { - const force = options.force === true; - const resetState = options.resetState === true; - - if (resetState) { - CLEANPLAATS.runtime.lastSellerAgeWarningKey = ''; - } - - window.clearTimeout(CLEANPLAATS.runtime.sellerAgeCheckTimer); - CLEANPLAATS.runtime.sellerAgeCheckTimer = window.setTimeout(() => { - maybeShowSellerAgeWarning({ force }); - }, 180); -} - -function clearAllNotifications() { - const notifications = document.querySelectorAll('[id^="cleanplaats-"]'); - notifications.forEach(notification => { - if (notification.classList.contains('cleanplaats-empty-notification') || - notification.id === 'cleanplaats-loading' || - notification.id === 'cleanplaats-seller-age-warning-toast') { - notification.remove(); - } - }); - notificationVisible = false; -} - -function clearBubbleNotification() { - const toast = document.getElementById('cleanplaats-bubble-notification'); - if (toast) { - toast.classList.remove('visible'); - setTimeout(() => { - if (toast) { - toast.remove(); - } - }, 300); - } -} - -function showSettingFeedback() { - return; -} - -function showBlacklistToast(sellerName) { - const panelText = getPanelLocaleText(); - const toast = document.createElement('div'); - toast.className = 'cleanplaats-blacklist-toast'; - - toast.innerHTML = DOMPurify.sanitize(` -
    - 👁 -
    - ${sellerName} ${panelText.blacklistToastHiddenSuffix} - ${panelText.blacklistToastHint} -
    -
    - `); - - document.body.appendChild(toast); - setTimeout(() => { - requestAnimationFrame(() => toast.classList.add('visible')); - }, 50); - - setTimeout(() => { - toast.classList.remove('visible'); - setTimeout(() => toast.remove(), 300); - }, 3000); -} - -function showBulkBlacklistToast(count) { - const panelText = getPanelLocaleText(); - const toast = document.createElement('div'); - toast.className = 'cleanplaats-blacklist-toast'; - - toast.innerHTML = DOMPurify.sanitize(` -
    - 👁 -
    - ${count} ${panelText.blacklistToastHiddenPluralSuffix} - ${panelText.blacklistToastHint} -
    -
    - `); - - document.body.appendChild(toast); - setTimeout(() => { - requestAnimationFrame(() => toast.classList.add('visible')); - }, 50); - - setTimeout(() => { - toast.classList.remove('visible'); - setTimeout(() => toast.remove(), 300); - }, 3000); -} - -function showUnblacklistToast(sellerName) { - const panelText = getPanelLocaleText(); - const toast = document.createElement('div'); - toast.className = 'cleanplaats-blacklist-toast'; - - toast.innerHTML = DOMPurify.sanitize(` -
    - 👁 -
    - ${sellerName} ${panelText.blacklistToastShownSuffix} - ${panelText.blacklistToastShownHint} -
    -
    - `); - - document.body.appendChild(toast); - setTimeout(() => { - requestAnimationFrame(() => toast.classList.add('visible')); - }, 50); - - setTimeout(() => { - toast.classList.remove('visible'); - setTimeout(() => toast.remove(), 300); - }, 3000); -} - -function showBlacklistTermToast(term) { - const panelText = getPanelLocaleText(); - const toast = document.createElement('div'); - toast.className = 'cleanplaats-blacklist-toast'; - toast.innerHTML = DOMPurify.sanitize(` -
    - 🔎 -
    - '${term}' ${panelText.blacklistToastHiddenSuffix} - ${panelText.termToastHidden(term)} -
    -
    - `); - document.body.appendChild(toast); - setTimeout(() => { requestAnimationFrame(() => toast.classList.add('visible')); }, 50); - setTimeout(() => { - toast.classList.remove('visible'); - setTimeout(() => toast.remove(), 300); - }, 3000); -} - -function showUnblacklistTermToast(term) { - const panelText = getPanelLocaleText(); - const toast = document.createElement('div'); - toast.className = 'cleanplaats-blacklist-toast'; - toast.innerHTML = DOMPurify.sanitize(` -
    - 🔎 -
    - '${term}' ${panelText.blacklistToastShownSuffix} - ${panelText.termToastShown(term)} -
    -
    - `); - document.body.appendChild(toast); - setTimeout(() => { requestAnimationFrame(() => toast.classList.add('visible')); }, 50); - setTimeout(() => { - toast.classList.remove('visible'); - setTimeout(() => toast.remove(), 300); - }, 3000); -} diff --git a/content/observers.js b/content/observers.js deleted file mode 100644 index d1c6a9c..0000000 --- a/content/observers.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Content-script observers and navigation handling. - */ - -function performCleanupAndCheckForEmptyPage() { - const existingNotification = document.getElementById('cleanplaats-empty-notification'); - if (existingNotification) { - existingNotification.remove(); - notificationVisible = false; - } - - clearBubbleNotification(); - scheduleSellerAgeWarningCheck({ resetState: true }); - - const checkContentLoaded = setInterval(() => { - if (document.querySelector('.hz-Listing') || document.querySelector('#adsense-container')) { - clearInterval(checkContentLoaded); - console.log('Cleanplaats: Running cleanup after navigation'); - performCleanup(); - injectBlacklistButtons(); - - setTimeout(checkForEmptyPage, 500); - } - }, 100); -} - -function setupObservers() { - let lastUrl = location.href; - - if (CLEANPLAATS.observers.mutation) { - CLEANPLAATS.observers.mutation.disconnect(); - } - - const observer = new MutationObserver(mutations => { - if (lastUrl !== location.href) { - console.log('Cleanplaats: URL changed from', lastUrl, 'to', location.href); - lastUrl = location.href; - CLEANPLAATS.runtime.lastSellerAgeWarningKey = ''; - performCleanupAndCheckForEmptyPage(); - } - - let shouldCleanup = false; - let shouldSyncHeaderLogo = false; - - for (const mutation of mutations) { - if (mutation.type === 'childList' && mutation.addedNodes.length) { - const listingMutationTarget = mutation.target?.nodeType === Node.ELEMENT_NODE - ? mutation.target.closest?.('.hz-Listing') - : null; - - if (window.innerWidth < 700 && listingMutationTarget) { - shouldCleanup = true; - break; - } - - for (const node of mutation.addedNodes) { - if (node.nodeType === Node.ELEMENT_NODE) { - if ( - node.classList?.contains('hz-Header-logo-desktop') || - node.classList?.contains('mp-Header-logo') || - node.querySelector?.('.hz-Header-logo-desktop, .mp-Header-logo') - ) { - shouldSyncHeaderLogo = true; - } - - if ( - node.classList?.contains('SellerInfoSmall-root') || - node.querySelector?.('.SellerInfoSmall-root') - ) { - scheduleSellerAgeWarningCheck(); - } - - if ( - node.classList?.contains('hz-Listing') || - node.querySelector?.('.hz-Listing') || - node.classList?.contains('MpCard-mpCardBanner') || - node.querySelector?.('.MpCard-mpCardBanner, img[alt="Marktplaats Marketing Banner"]') || - node.classList?.contains('SimilarAdsList-related-ads-section') || - node.querySelector?.('.SimilarAdsList-related-ads-section') || - node.id === 'notifications-root' || - node.classList?.contains('NonFeatureBuyerBanner-root') || - node.classList?.contains('feature-banner') || - node.querySelector?.('#notifications-root, .NonFeatureBuyerBanner-root, .feature-banner[data-testid="50-percent-off-banner"]') || - node.id?.includes('ad') || - node.id === 'similar-items-root' || - node.querySelector?.('#similar-items-root, .AdmarktSimilarItemsContainer, .AdmarktSimilarItems-root') || - node.classList?.contains('hz-Banner') || - node.querySelector?.('[data-google-query-id]') || - node.classList?.contains('hz-FeedBannerBlock') || - node.classList?.contains('Banners-bannerFeedItem') || - node.id === 'banner-top-dt-container' || - node.querySelector?.('#banner-top-dt, #banner-top-dt-container') - ) { - shouldCleanup = true; - break; - } - } - } - } - - if (mutation.type === 'attributes') { - const target = mutation.target; - if ( - target?.classList?.contains('SellerInfoSmall-root') - ) { - scheduleSellerAgeWarningCheck(); - } - - if ( - target?.classList?.contains('hz-FeedBannerBlock') || - target?.classList?.contains('Banners-bannerFeedItem') || - target?.classList?.contains('MpCard-mpCardBanner') || - target?.classList?.contains('SimilarAdsList-related-ads-section') || - target?.classList?.contains('NonFeatureBuyerBanner-root') || - target?.classList?.contains('feature-banner') || - target?.classList?.contains('AdmarktSimilarItemsContainer') || - target?.classList?.contains('AdmarktSimilarItems-root') || - target?.id === 'notifications-root' || - target?.id === 'similar-items-root' || - target?.id === 'banner-right-container' || - target?.id === 'banner-top-dt-container' - ) { - shouldCleanup = true; - } - } - - if (shouldCleanup) break; - } - - if (CLEANPLAATS.settings.darkMode && shouldSyncHeaderLogo) { - syncHeaderLogoForDarkMode(true); - } - - if (shouldCleanup) { - performCleanup(); - injectBlacklistButtons(); - } - }); - - observer.observe(document, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['class', 'style', 'hidden', 'aria-hidden'] - }); - - CLEANPLAATS.observers.mutation = observer; -} - -function handleNavigation() { - wakeUpBackground(); - window.dispatchEvent(new Event('navigation')); -} - -function setupNavigationDetection() { - window.addEventListener('popstate', handleNavigation); - - const originalPushState = history.pushState; - history.pushState = function () { - originalPushState.apply(this, arguments); - }; - - const originalReplaceState = history.replaceState; - history.replaceState = function () { - originalReplaceState.apply(this, arguments); - }; - - document.addEventListener('click', (e) => { - const link = e.target.closest('a[href]'); - if (link && link.hostname === window.location.hostname) { - setTimeout(() => handleNavigation(), 100); - } - }); -} - -function setupAllObservers() { - setupObservers(); - setupNavigationDetection(); -} - -function isSearchResultsPage() { - const url = window.location.href; - return url.includes('marktplaats.nl/l/') || - url.includes('marktplaats.nl/q/') || - url.includes('2dehands.be/l/') || - url.includes('2dehands.be/q/') || - url.includes('2ememain.be/l/') || - url.includes('2ememain.be/q/'); -} diff --git a/content/shared.js b/content/shared.js deleted file mode 100644 index 774e7dd..0000000 --- a/content/shared.js +++ /dev/null @@ -1,389 +0,0 @@ -/** - * Cleanplaats shared content-script state and locale helpers. - */ - -var browserAPI = typeof browser !== 'undefined' ? browser : chrome; -var CLEANPLAATS_DARK_MODE_CLASS = 'cleanplaats-dark-mode'; -var CLEANPLAATS_TWH_SITE_CLASS = 'cleanplaats-site-twh'; -var CLEANPLAATS_THEME_STORAGE_KEY = 'cleanplaats:darkMode'; -var CLEANPLAATS_FLOATING_OFFSET_VAR = '--cleanplaats-floating-offset'; -var MARKTPLAATS_DESKTOP_LOGO_MATCH = /\/tenant--nlnl(?:\.[a-z0-9]+)?\.svg$/i; -var CLEANPLAATS_DARK_LOGO_PATH = 'icons/marktplaats-logo-darkmode.svg'; -var cleanplaatsStorageSyncRegistered = false; -var notificationTimeout; -var notificationVisible = false; - -function getReviewCTAConfig() { - const runtimeUrl = browserAPI?.runtime?.getURL ? browserAPI.runtime.getURL('') : ''; - const isFirefox = runtimeUrl.startsWith('moz-extension://') || navigator.userAgent.includes('Firefox'); - - if (isFirefox) { - return { - linkLabel: 'Firefox Add-ons', - url: 'https://addons.mozilla.org/nl/firefox/addon/cleanplaats-marktplaats-filter/reviews/' - }; - } - - return { - linkLabel: 'Chrome Web Store', - url: 'https://chromewebstore.google.com/detail/cleanplaats-marktplaats-z/peebdbeclpkljmfocjifjpjlngfpfhjp/reviews' - }; -} - -function is2ememainLocale() { - return location.hostname.includes('2ememain.be'); -} - -function is2dehandsFamilySite() { - return location.hostname.includes('2dehands.be') || location.hostname.includes('2ememain.be'); -} - -function isMarktplaatsSite() { - return location.hostname.includes('marktplaats.nl'); -} - -function isProductDetailPage() { - return /\/v\//.test(window.location.pathname); -} - -function normalizeSellerAgeText(text) { - return (text || '') - .trim() - .toLowerCase() - .replace(/\s+/g, ' '); -} - -function parseSellerAgeToDays(text) { - const normalizedText = normalizeSellerAgeText(text); - const match = normalizedText.match(/(\d+)\s+(dag|dagen|day|days|jour|jours|week|weken|maand|maanden|jaar|jaren|month|months|year|years|mois|an|ans|semaine|semaines)\b/); - - if (!match) { - return null; - } - - const amount = parseInt(match[1], 10); - const unit = match[2]; - - if (!Number.isFinite(amount) || amount < 0) { - return null; - } - - if (unit === 'dag' || unit === 'dagen' || unit === 'day' || unit === 'days' || unit === 'jour' || unit === 'jours') { - return amount; - } - - if (unit === 'week' || unit === 'weken' || unit === 'semaine' || unit === 'semaines') { - return amount * 7; - } - - if (unit === 'maand' || unit === 'maanden' || unit === 'month' || unit === 'months' || unit === 'mois') { - return amount * 30; - } - - if (unit === 'jaar' || unit === 'jaren' || unit === 'year' || unit === 'years' || unit === 'an' || unit === 'ans') { - return amount * 365; - } - - return null; -} - -function getSellerAgeWarningThresholdDays() { - const value = Math.max(1, parseInt(CLEANPLAATS.settings.sellerAgeWarningThresholdValue, 10) || 1); - const unit = CLEANPLAATS.settings.sellerAgeWarningThresholdUnit; - - if (unit === 'days') { - return value; - } - - if (unit === 'weeks') { - return value * 7; - } - - if (unit === 'years') { - return value * 365; - } - - return value * 30; -} - -function getPanelLocaleText() { - if (is2ememainLocale()) { - return { - feedbackLabel: 'Retour', - feedbackText: 'Issues GitHub', - feedbackAriaLabel: 'Ouvrir GitHub issues pour les demandes de fonctionnalité, modifications et bugs', - reviewAriaLabel: linkLabel => `Laisser un avis sur Cleanplaats sur ${linkLabel}`, - supportTitle: 'Soutenir Cleanplaats', - supportButton: 'Soutenir Cleanplaats', - optionsTitle: 'Options de filtrage', - topAdLabel: 'Pub au top', - topAdTooltip: "Masque les annonces marquées 'Pub au top'", - dagtoppersLabel: 'Tops du jour', - dagtoppersTooltip: "Supprime les annonces marquées 'Top du jour'", - promotedListingsLabel: 'Annonces professionnelles', - promotedListingsTooltip: "Masque les annonces de boutiques et d'entreprises, y compris sur la page d'accueil dans 'Pour vous' et 'Près de chez vous'", - stickersLabel: 'Autocollants promotionnels', - stickersTooltip: 'Supprime les annonces avec des autocollants promotionnels', - reservedLabel: 'Réservées', - reservedTooltip: "Masque les annonces marquées 'Réservé'", - favoriteRelatedAdsLabel: 'Annonces similaires dans les favoris', - favoriteRelatedAdsTooltip: 'Masque la liste des annonces similaires affichée dans les favoris', - sellerAgeWarningLabel: 'Alerte compte vendeur récent', - sellerAgeWarningTooltip: "Affiche un avertissement sur une page d'annonce si le compte vendeur est plus récent que votre seuil.", - sellerAgeWarningThresholdLabel: 'Avertir en dessous de', - sellerAgeWarningThresholdValueAriaLabel: 'Valeur seuil pour le compte vendeur récent', - sellerAgeWarningThresholdUnitAriaLabel: 'Unité seuil pour le compte vendeur récent', - sellerAgeWarningThresholdUnits: { - days: 'jours', - weeks: 'semaines', - months: 'mois', - years: 'ans' - }, - sellerAgeWarningToastTitle: 'Compte vendeur récent', - sellerAgeWarningToastMessage: (sellerName, sellerAgeText, thresholdLabel) => `${sellerName} est sur la plateforme depuis ${sellerAgeText}. Votre seuil est ${thresholdLabel}.`, - preferencesLabel: 'Préférences', - backLabel: '← Retour', - preferencesIntro: '', - darkModeLabel: 'Mode sombre', - darkModeTooltip: 'Active un thème sombre pour 2ememain et le panneau Cleanplaats. Expérimental: si la visibilité pose problème, désactivez-le.', - resultsPerPageLabel: 'Résultats par page :', - defaultSortLabel: 'Tri par défaut :', - sortOptions: { - standard: 'Standard', - date_new_old: 'Plus récentes', - date_old_new: 'Plus anciennes', - price_low_high: 'Prix ↑', - price_high_low: 'Prix ↓', - distance: 'Distance' - }, - statsTitle: 'Éléments supprimés', - statsTop: 'Top :', - statsDagtoppers: 'Tops du jour :', - statsBusiness: 'Professionnel :', - statsStickers: 'Autocollants :', - statsOther: 'Autres :', - statsTotal: 'Total :', - manageTerms: 'Gérer les termes masqués dans le titre', - manageSellers: 'Gérer les vendeurs masqués', - termsModalTitle: 'Termes masqués', - termsEmpty: 'Aucun terme ajouté', - hiddenButton: 'Masqué', - unhideButton: 'Afficher', - termInputPlaceholder: 'Saisissez un terme', - termInputHelp: 'Les annonces sont masquées si ce terme apparaît dans le titre.', - addButton: 'Ajouter', - closeButton: 'Fermer', - sellersModalTitle: 'Vendeurs masqués', - sellersEmpty: 'Aucun vendeur ajouté', - sellerInputPlaceholder: 'ex. Catawiki', - sellerInputHelp: 'Vous voulez ajouter plusieurs noms à la fois ? Séparez-les avec des virgules ou des points-virgules.', - hideSellerButton: 'Masquer le vendeur', - hiddenSellerButton: 'Vendeur masqué', - hideSellerButtonAriaLabel: 'Masquer ce vendeur', - blacklistToastHint: 'Gérez les vendeurs masqués via le panneau', - blacklistToastHiddenSuffix: 'masqué', - blacklistToastHiddenPluralSuffix: 'vendeurs masqués', - blacklistToastShownSuffix: "n'est plus masqué", - blacklistToastShownHint: 'Ce vendeur est à nouveau visible dans les résultats', - termToastHidden: term => `Toutes les annonces contenant le terme '${term}' sont désormais masquées.`, - termToastShown: term => `Les annonces contenant le terme '${term}' sont à nouveau affichées.` - }; - } - - return { - feedbackLabel: 'Feedback', - feedbackText: 'GitHub issues', - feedbackAriaLabel: 'Open GitHub issues voor functieverzoeken, wijzigingen en bugs', - reviewAriaLabel: linkLabel => `Laat een review achter voor Cleanplaats op ${linkLabel}`, - supportTitle: 'Steun Cleanplaats met een kleine bijdrage', - supportButton: 'Steun Cleanplaats', - optionsTitle: 'Filteropties', - topAdLabel: 'Topadvertenties', - topAdTooltip: location.hostname.includes('2dehands.be') - ? "Verbergt 'Topadvertentie' en 'Topzoekertje' listings" - : "Verwijdert betaalde 'Topadvertentie' advertenties", - dagtoppersLabel: 'Dagtoppers', - dagtoppersTooltip: "Verwijdert 'Dagtopper' advertenties", - promotedListingsLabel: 'Bedrijfsadvertenties', - promotedListingsTooltip: "Verbergt advertenties van bedrijven en winkels, zoals Catawiki, ook op de homepage bij 'Voor jou' en 'In je buurt'", - stickersLabel: 'Opvalstickers', - stickersTooltip: 'Verwijdert advertenties met opvalstickers', - reservedLabel: 'Gereserveerde', - reservedTooltip: "Verbergt advertenties die 'Gereserveerd' zijn", - favoriteRelatedAdsLabel: 'Gerelateerde advertenties bij favorieten', - favoriteRelatedAdsTooltip: 'Verbergt het blok met gerelateerde advertenties op de favorietenpagina', - sellerAgeWarningLabel: 'Waarschuwing voor nieuwe verkoperaccounts', - sellerAgeWarningTooltip: 'Toont op een advertentiepagina een waarschuwing als het verkopersaccount jonger is dan jouw ingestelde grens.', - sellerAgeWarningThresholdLabel: 'Waarschuwen onder', - sellerAgeWarningThresholdValueAriaLabel: 'Drempelwaarde voor waarschuwing nieuwe verkoperaccounts', - sellerAgeWarningThresholdUnitAriaLabel: 'Drempeleenheid voor waarschuwing nieuwe verkoperaccounts', - sellerAgeWarningThresholdUnits: { - days: 'dagen', - weeks: 'weken', - months: 'maanden', - years: 'jaar' - }, - sellerAgeWarningToastTitle: 'Nieuw verkoperaccount', - sellerAgeWarningToastMessage: (sellerName, sellerAgeText, thresholdLabel) => `${sellerName} zit pas ${sellerAgeText}. Jouw grens staat op ${thresholdLabel}. Verberg verkoper via de knop onder de naam.`, - preferencesLabel: 'Voorkeuren', - backLabel: '← Terug', - preferencesIntro: '', - darkModeLabel: 'Donkere modus', - darkModeTooltip: 'Schakelt een donker thema in voor Marktplaats en het Cleanplaats-paneel. Experimenteel: werkt meestal goed, maar zet het uit als iets slecht leesbaar is.', - resultsPerPageLabel: 'Resultaten per pagina:', - defaultSortLabel: 'Standaard sortering:', - sortOptions: { - standard: 'Standaard', - date_new_old: 'Nieuw eerst', - date_old_new: 'Oud eerst', - price_low_high: 'Prijs ↑', - price_high_low: 'Prijs ↓', - distance: 'Afstand' - }, - statsTitle: 'Verwijderde items', - statsTop: 'Top:', - statsDagtoppers: 'Dagtoppers:', - statsBusiness: 'Bedrijf:', - statsStickers: 'Stickers:', - statsOther: 'Overig:', - statsTotal: 'Totaal:', - manageTerms: 'Beheer blacklist-termen in titels', - manageSellers: 'Beheer verborgen verkopers', - termsModalTitle: 'Blacklist termen', - termsEmpty: 'Geen termen toegevoegd', - hiddenButton: 'Verborgen', - unhideButton: 'Opheffen', - termInputPlaceholder: 'Voer een term in', - termInputHelp: 'Advertenties worden verborgen als deze term in de titel voorkomt.', - addButton: 'Toevoegen', - closeButton: 'Sluiten', - sellersModalTitle: 'Verborgen verkopers', - sellersEmpty: 'Geen verkopers toegevoegd', - sellerInputPlaceholder: 'bijv. Catawiki', - sellerInputHelp: "Wil je meerdere namen tegelijk toevoegen? Scheid ze dan met komma's of puntkomma's.", - hideSellerButton: 'Verkoper verbergen', - hiddenSellerButton: 'Verkoper verborgen', - hideSellerButtonAriaLabel: 'Verberg deze verkoper', - blacklistToastHint: 'Beheer verborgen verkopers via het paneel', - blacklistToastHiddenSuffix: 'verborgen', - blacklistToastHiddenPluralSuffix: 'verkopers verborgen', - blacklistToastShownSuffix: 'niet meer verborgen', - blacklistToastShownHint: 'Deze verkoper is weer zichtbaar in de resultaten', - termToastHidden: term => `Alle advertenties met de term '${term}' zijn nu verborgen.`, - termToastShown: term => `Advertenties met de term '${term}' worden weer getoond.` - }; -} - -var CLEANPLAATS = { - settings: { - removeTopAds: true, - removeDagtoppers: true, - removePromotedListings: true, - removeOpvalStickers: true, - removeReservedListings: false, - removeFavoriteRelatedAds: false, - sellerAgeWarningEnabled: false, - sellerAgeWarningThresholdValue: 3, - sellerAgeWarningThresholdUnit: 'days', - darkMode: false, - blacklistedSellers: [], - blacklistedTerms: [], - resultsPerPage: 30, - defaultSortMode: 'standard', - sortPreferenceSource: 'cleanplaats' - }, - - stats: { - topAdsRemoved: 0, - dagtoppersRemoved: 0, - promotedListingsRemoved: 0, - opvalStickersRemoved: 0, - otherAdsRemoved: 0, - totalRemoved: 0 - }, - - observers: { - mutation: null, - ads: null, - webchat: null, - sellerAge: null - }, - - runtime: { - lastSellerAgeWarningKey: '', - sellerAgeCheckTimer: 0 - }, - - featureFlags: { - showStats: true, - autoCollapse: false, - firstRun: true - }, - - panelState: { - isCollapsed: false, - hasShownWelcomeToast: false, - lastSeenVersion: '', - activeView: 'filters' - } -}; - -var CLEANPLAATS_UPDATE_NOTES = { - '2.0.7': { - intro: 'Cleanplaats 2.0.7 voegt een extra veiligheidswaarschuwing toe op advertentiepagina’s en maakt het verbergen van verkopers duidelijker en handiger.', - highlights: [ - 'Je kunt nu een waarschuwing krijgen bij nieuwe verkoperaccounts. Deze instelling vind je onder het tabje "Voorkeuren" in het paneel, waar je zelf kiest vanaf hoeveel dagen, weken, maanden of jaren je zo’n melding wilt zien.', - 'Op advertentiepagina’s staat nu ook een knop onder de verkopernaam om in één keer alle advertenties van die verkoper te verbergen.', - 'De knop om een verkoper te verbergen is nu ook netjes vertaald op 2ememain.' - ], - note: 'Zie je een verkoper die je niet vertrouwt? Dan kun je die nu direct vanaf de advertentiepagina verbergen.' - }, - '2.0.6': { - intro: 'Cleanplaats 2.0.6 herstelt een paar dingen op Favorieten en lost een vervelende fout op die sommige filters uit beeld haalde.', - highlights: [ - 'De filters voor categorie en afstand zijn weer terug waar ze horen.', - 'Gerelateerde advertenties in Favorieten worden niet meer standaard verborgen. Via de nieuwe knop "Voorkeuren" kun je dit nu zelf aan of uit zetten.', - 'Niet-beschikbare advertenties in Favorieten zien er in dark mode nu weer duidelijk anders uit dan actieve advertenties.' - ], - note: 'Excuses voor de bug waardoor categorie en afstand ineens konden verdwijnen. Bedankt aan iedereen die dit zo snel heeft gemeld via Reddit en GitHub issues. Jullie hulp en betrokkenheid maken Cleanplaats tot het succes dat het is.' - }, - '2.0.5': { - intro: 'Cleanplaats 2.0.5 werkt Marktplaats verder bij met vooral meer dark mode-ondersteuning en een rustigere interface op meerdere pagina’s.', - highlights: [ - 'Dark mode is verder uitgebreid op onder meer "Mijn advertenties", account- en plaats advertentie-pagina’s, tabelweergaven en onderdelen rond eigen advertenties.', - 'Ook losse interface-elementen zoals "Deal gesloten?", voorstel- en leveringsmenu’s nemen nu beter het donkere thema over.', - 'Storende banners en promotieblokken zijn op meerdere plekken verborgen, waaronder "gerelateerde advertenties" in Favorieten.', - 'Een visuele flicker bij het laden in dark mode is aangepakt, waardoor pagina’s rustiger en consistenter openen.', - "Marktplaats banner voor 'koop je auto bij autobedrijven' weggehaald" - ], - note: "Zie je nog een onderdeel of licht onderdeel dat door de dark mode heen glipt in veel gebruikte pagina's? Meld het via GitHub issues in het paneel." - } -}; - -var MARKTPLAATS_SORT_LABEL_TO_MODE = { - 'standaard': 'standard', - 'datum (nieuw-oud)': 'date_new_old', - 'datum (oud-nieuw)': 'date_old_new', - 'prijs (laag-hoog)': 'price_low_high', - 'prijs (hoog-laag)': 'price_high_low', - 'afstand': 'distance' -}; - -function normalizeSortLabel(label) { - return (label || '').trim().toLowerCase(); -} - -function getSortModeFromLabel(label) { - return MARKTPLAATS_SORT_LABEL_TO_MODE[normalizeSortLabel(label)] || null; -} - -function isMarketplaceSortDropdown(element) { - if (!(element instanceof HTMLSelectElement)) return false; - - const ariaLabel = normalizeSortLabel(element.getAttribute('aria-label')); - if (ariaLabel === 'sorteer op') return true; - - return Array.from(element.options || []).some(option => { - return normalizeSortLabel(option.textContent) === 'datum (nieuw-oud)'; - }); -} diff --git a/content/storage.js b/content/storage.js deleted file mode 100644 index 9d3d180..0000000 --- a/content/storage.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Content-script storage and state persistence. - */ - -function registerSettingsStorageSync() { - if (cleanplaatsStorageSyncRegistered || !browserAPI?.storage?.onChanged?.addListener) { - return; - } - - browserAPI.storage.onChanged.addListener((changes, areaName) => { - if (areaName !== 'local' || !changes.cleanplaatsSettings?.newValue) { - return; - } - - try { - const nextSettings = JSON.parse(changes.cleanplaatsSettings.newValue); - const darkModeEnabled = Boolean(nextSettings?.darkMode); - - if (CLEANPLAATS.settings.darkMode !== darkModeEnabled) { - CLEANPLAATS.settings.darkMode = darkModeEnabled; - applyDarkModeToDocument(darkModeEnabled); - syncDarkModeToggle(darkModeEnabled); - } else { - persistDarkModePreference(darkModeEnabled); - } - } catch (error) { - console.error('Cleanplaats: Failed to sync dark mode from storage', error); - } - }); - - cleanplaatsStorageSyncRegistered = true; -} - -function loadSettings() { - return new Promise((resolve, reject) => { - browserAPI.storage.local.get(['cleanplaatsSettings', 'panelState'], (items) => { - if (browserAPI.runtime.lastError) { - console.error('Cleanplaats: Failed to load settings from storage', browserAPI.runtime.lastError); - reject(browserAPI.runtime.lastError); - return; - } - - try { - const storedSettings = items.cleanplaatsSettings; - const storedPanelState = items.panelState; - - if (storedSettings) { - const settings = JSON.parse(storedSettings); - Object.assign(CLEANPLAATS.settings, settings); - } - - try { - const storedDarkMode = window.localStorage.getItem(CLEANPLAATS_THEME_STORAGE_KEY); - if (storedDarkMode === 'true' || storedDarkMode === 'false') { - CLEANPLAATS.settings.darkMode = storedDarkMode === 'true'; - } - } catch (error) { - console.warn('Cleanplaats: Failed to read dark mode from localStorage', error); - } - - if (storedPanelState) { - CLEANPLAATS.panelState = JSON.parse(storedPanelState); - } - - resolve(); - } catch (error) { - console.error('Cleanplaats: Failed to parse settings from storage', error); - reject(error); - } - }); - }); -} - -function saveSettings() { - return new Promise((resolve, reject) => { - try { - persistDarkModePreference(Boolean(CLEANPLAATS.settings.darkMode)); - browserAPI.storage.local.set({ - cleanplaatsSettings: JSON.stringify(CLEANPLAATS.settings), - panelState: JSON.stringify(CLEANPLAATS.panelState) - }, () => { - if (browserAPI.runtime.lastError) { - console.error('Cleanplaats: Failed to save settings to storage', browserAPI.runtime.lastError); - reject(browserAPI.runtime.lastError); - return; - } - resolve(); - }); - } catch (error) { - console.error('Cleanplaats: Failed to save settings to storage', error); - reject(error); - } - }); -} - -function resetStats() { - Object.keys(CLEANPLAATS.stats).forEach(key => { - CLEANPLAATS.stats[key] = 0; - }); - - updateStatsDisplay(); -} diff --git a/content/theme.js b/content/theme.js deleted file mode 100644 index ca63c34..0000000 --- a/content/theme.js +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Content-script dark mode and sort synchronization helpers. - */ - -function persistDarkModePreference(enabled) { - try { - window.localStorage.setItem(CLEANPLAATS_THEME_STORAGE_KEY, enabled ? 'true' : 'false'); - } catch (error) { - console.warn('Cleanplaats: Failed to persist dark mode in localStorage', error); - } -} - -function syncSiteThemeClass() { - document.documentElement.classList.toggle(CLEANPLAATS_TWH_SITE_CLASS, is2dehandsFamilySite()); -} - -function syncCleanplaatsSortMode(sortMode) { - if (!sortMode) return; - - const modeChanged = CLEANPLAATS.settings.defaultSortMode !== sortMode; - const sourceChanged = CLEANPLAATS.settings.sortPreferenceSource !== 'marketplace'; - if (!modeChanged && !sourceChanged) return; - - CLEANPLAATS.settings.defaultSortMode = sortMode; - CLEANPLAATS.settings.sortPreferenceSource = 'marketplace'; - - const cleanplaatsDropdown = document.getElementById('cleanplaats-sort-dropdown'); - if (cleanplaatsDropdown && cleanplaatsDropdown.value !== sortMode) { - cleanplaatsDropdown.value = sortMode; - } - - wakeUpBackground(); - saveSettings().catch(error => { - console.error('Cleanplaats: Failed to sync sort mode from page selection', error); - }); -} - -function setupMarketplaceSortSync() { - if (document.body?.dataset.cleanplaatsSortSyncBound === 'true') return; - if (document.body) { - document.body.dataset.cleanplaatsSortSyncBound = 'true'; - } - - document.addEventListener('change', (event) => { - const target = event.target; - if (!isMarketplaceSortDropdown(target)) return; - - const selectedOption = target.options[target.selectedIndex]; - const sortMode = getSortModeFromLabel(selectedOption?.textContent || target.value); - syncCleanplaatsSortMode(sortMode); - }, true); -} - -function applyDarkModeToDocument(enabled) { - const isEnabled = Boolean(enabled); - syncSiteThemeClass(); - document.documentElement.classList.toggle(CLEANPLAATS_DARK_MODE_CLASS, isEnabled); - persistDarkModePreference(isEnabled); - syncHeaderLogoForDarkMode(isEnabled); - - const panel = document.getElementById('cleanplaats-panel'); - if (panel) { - panel.classList.toggle(CLEANPLAATS_DARK_MODE_CLASS, isEnabled); - updateCollapsedPanelIcon(panel); - } -} - -function getCollapsedPanelIconUrl() { - const iconPath = CLEANPLAATS.settings.darkMode ? 'icons/darkmode_icon_128.png' : 'icons/icon128.png'; - return browserAPI.runtime.getURL(iconPath); -} - -function syncHeaderLogoForDarkMode(enabled) { - document.querySelectorAll('.hz-Header-logo-desktop').forEach(img => { - if (!(img instanceof HTMLImageElement)) return; - - const currentSource = img.getAttribute('src') || ''; - const originalSource = img.dataset.cleanplaatsOriginalSrc || currentSource; - - if (!img.dataset.cleanplaatsOriginalSrc) { - img.dataset.cleanplaatsOriginalSrc = currentSource; - } - - if (!MARKTPLAATS_DESKTOP_LOGO_MATCH.test(originalSource)) { - return; - } - - const nextSource = enabled - ? browserAPI.runtime.getURL(CLEANPLAATS_DARK_LOGO_PATH) - : originalSource; - - if (currentSource !== nextSource) { - img.setAttribute('src', nextSource); - } - }); - - document.querySelectorAll('.mp-Header-logo').forEach(link => { - if (!(link instanceof HTMLElement)) return; - - if (enabled && isMarktplaatsSite()) { - link.style.backgroundImage = `url("${browserAPI.runtime.getURL(CLEANPLAATS_DARK_LOGO_PATH)}")`; - link.style.backgroundRepeat = 'no-repeat'; - link.style.backgroundPosition = 'center'; - link.style.backgroundSize = 'contain'; - return; - } - - link.style.removeProperty('background-image'); - link.style.removeProperty('background-repeat'); - link.style.removeProperty('background-position'); - link.style.removeProperty('background-size'); - }); -} - -function updateCollapsedPanelIcon(panel = document.getElementById('cleanplaats-panel')) { - if (!panel) return; - - if (panel.classList.contains('collapsed-ready')) { - panel.style.backgroundImage = `url('${getCollapsedPanelIconUrl()}')`; - return; - } - - panel.style.backgroundImage = ''; -} - -function syncDarkModeToggle(enabled) { - const toggle = document.getElementById('cleanplaats-theme-toggle'); - if (!toggle) return; - - const isEnabled = Boolean(enabled); - toggle.setAttribute('aria-pressed', isEnabled ? 'true' : 'false'); - toggle.setAttribute('aria-checked', isEnabled ? 'true' : 'false'); - toggle.dataset.theme = isEnabled ? 'dark' : 'light'; -} - -function isElementVisuallyVisible(element) { - if (!(element instanceof Element)) return false; - - const style = window.getComputedStyle(element); - if ( - style.display === 'none' || - style.visibility === 'hidden' || - style.opacity === '0' - ) { - return false; - } - - const rect = element.getBoundingClientRect(); - return rect.width > 0 && - rect.height > 0 && - rect.bottom > 0 && - rect.right > 0 && - rect.top < window.innerHeight && - rect.left < window.innerWidth; -} - -function updateFloatingUiOffsetForWebchat() { - const webchatToggle = document.querySelector( - '[data-cognigy-webchat-toggle="true"], #webchatWindowToggleButton' - ); - - let offset = 0; - - if (isElementVisuallyVisible(webchatToggle)) { - const rect = webchatToggle.getBoundingClientRect(); - const gap = 16; - offset = Math.max(0, Math.ceil(rect.height + gap)); - } - - document.documentElement.style.setProperty(CLEANPLAATS_FLOATING_OFFSET_VAR, `${offset}px`); -} - -function setupWebchatCollisionAvoidance() { - updateFloatingUiOffsetForWebchat(); - - if (CLEANPLAATS.observers.webchat) { - CLEANPLAATS.observers.webchat.disconnect(); - } - - let rafId = 0; - const scheduleOffsetUpdate = () => { - if (rafId) return; - rafId = window.requestAnimationFrame(() => { - rafId = 0; - updateFloatingUiOffsetForWebchat(); - }); - }; - - const observer = new MutationObserver(scheduleOffsetUpdate); - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['style', 'class', 'hidden', 'aria-hidden'] - }); - - window.addEventListener('resize', scheduleOffsetUpdate, { passive: true }); - CLEANPLAATS.observers.webchat = observer; -} diff --git a/content/ui.js b/content/ui.js deleted file mode 100644 index e9b8325..0000000 --- a/content/ui.js +++ /dev/null @@ -1,830 +0,0 @@ -/** - * Content-script control panel rendering and UI event handling. - */ - -function createControlPanel() { - if (document.getElementById('cleanplaats-panel')) return; - - const panel = document.createElement('div'); - panel.id = 'cleanplaats-panel'; - panel.className = 'cleanplaats-panel'; - panel.classList.toggle(CLEANPLAATS_DARK_MODE_CLASS, CLEANPLAATS.settings.darkMode); - - if (CLEANPLAATS.featureFlags.autoCollapse || CLEANPLAATS.panelState.isCollapsed) { - panel.classList.add('collapsed'); - panel.classList.add('collapsed-ready'); - updateCollapsedPanelIcon(panel); - } - - const panelText = getPanelLocaleText(); - const reviewCTA = getReviewCTAConfig(); - - panel.innerHTML = DOMPurify.sanitize(` -
    -
    -

    - - Cleanplaats - -

    -
    - - -
    -
    -
    - - - - ${panelText.feedbackLabel} - ${panelText.feedbackText} - - - - - - Review - ${reviewCTA.linkLabel} - - -
    -
    -
    -
    -
    - - - ${panelText.supportButton} - -
    -
    ${panelText.optionsTitle}
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    - - -
    -
    - - -
    -
    - - ${CLEANPLAATS.featureFlags.showStats ? ` -
    -
    ${panelText.statsTitle}
    -
    - ${panelText.statsTop} - 0 -
    -
    - ${panelText.statsDagtoppers} - 0 -
    -
    - ${panelText.statsBusiness} - 0 -
    -
    - ${panelText.statsStickers} - 0 -
    -
    - ${panelText.statsOther} - 0 -
    -
    - ${panelText.statsTotal} - 0 -
    -
    - ` : ''} - - - -
    -
    -
    -
    - -
    ${panelText.preferencesLabel}
    -
    - ${panelText.preferencesIntro ? ` -
    - ${panelText.preferencesIntro} -
    - ` : ''} -
    -
    -
    - - -
    -
    -
    - - -
    -
    - - - -
    -
    -
    -
    -
    - - -
    - `); - - document.body.appendChild(panel); - const logoImg = panel.querySelector('#cleanplaats-header-logo'); - if (logoImg) { - logoImg.src = browserAPI.runtime.getURL('icons/icon128.png'); - } - panel.querySelectorAll('.cleanplaats-external-link').forEach(link => { - link.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - window.open(link.href, '_blank', 'noopener,noreferrer'); - }); - }); - setupEventListeners(); - syncDarkModeToggle(CLEANPLAATS.settings.darkMode); - - document.getElementById('cleanplaats-manage-blacklist').addEventListener('click', (e) => { - e.preventDefault(); - showBlacklistModal(); - }); - - document.getElementById('cleanplaats-manage-terms').addEventListener('click', (e) => { - e.preventDefault(); - showTermsModal(); - }); - - if (!document.getElementById('cleanplaats-global-tooltip')) { - const tooltip = document.createElement('div'); - tooltip.id = 'cleanplaats-global-tooltip'; - tooltip.className = 'cleanplaats-global-tooltip'; - tooltip.style.display = 'none'; - document.body.appendChild(tooltip); - } - - setupGlobalTooltip(); - setActivePanelView(getStoredPanelView(), { persist: false, animated: false }); -} - -function getStoredPanelView() { - return CLEANPLAATS.panelState.activeView === 'preferences' ? 'preferences' : 'filters'; -} - -function getPanelViewDirection(fromView, toView) { - if (fromView === toView) { - return 'none'; - } - return toView === 'preferences' ? 'down' : 'up'; -} - -function clearPanelViewAnimationState(viewElement) { - if (!viewElement) { - return; - } - viewElement.classList.remove( - 'active', - 'is-entering', - 'is-leaving', - 'is-entering-down', - 'is-entering-up', - 'is-leaving-down', - 'is-leaving-up' - ); -} - -function syncPanelViewContainerHeight(activeView) { - const viewsContainer = document.getElementById('cleanplaats-panel-views'); - if (!viewsContainer || !activeView) { - return; - } - - viewsContainer.style.height = `${activeView.scrollHeight}px`; -} - -function measurePanelViewHeight(viewElement) { - const viewsContainer = document.getElementById('cleanplaats-panel-views'); - if (!viewElement || !viewsContainer) { - return 0; - } - - const clone = viewElement.cloneNode(true); - const measurementWrapper = document.createElement('div'); - clone.removeAttribute('id'); - clone.querySelectorAll('[id]').forEach((element) => { - element.removeAttribute('id'); - }); - - clearPanelViewAnimationState(clone); - clone.classList.add('active'); - clone.setAttribute('aria-hidden', 'true'); - clone.style.position = 'relative'; - clone.style.visibility = 'hidden'; - clone.style.pointerEvents = 'none'; - clone.style.opacity = '0'; - clone.style.transform = 'translateY(0)'; - - measurementWrapper.setAttribute('aria-hidden', 'true'); - measurementWrapper.style.position = 'absolute'; - measurementWrapper.style.top = '0'; - measurementWrapper.style.right = '0'; - measurementWrapper.style.left = '0'; - measurementWrapper.style.visibility = 'hidden'; - measurementWrapper.style.pointerEvents = 'none'; - measurementWrapper.style.opacity = '0'; - measurementWrapper.style.overflow = 'visible'; - - measurementWrapper.appendChild(clone); - viewsContainer.appendChild(measurementWrapper); - const height = clone.getBoundingClientRect().height; - measurementWrapper.remove(); - - return height; -} - -function setActivePanelView(view, options = {}) { - const persist = options.persist !== false; - const nextView = view === 'preferences' ? 'preferences' : 'filters'; - const animated = options.animated !== false; - const viewsContainer = document.getElementById('cleanplaats-panel-views'); - const filtersView = document.getElementById('cleanplaats-view-filters'); - const preferencesView = document.getElementById('cleanplaats-view-preferences'); - - if (!filtersView || !preferencesView || !viewsContainer) { - return; - } - - const currentView = CLEANPLAATS.panelState.activeView === 'preferences' ? 'preferences' : 'filters'; - const currentElement = currentView === 'preferences' ? preferencesView : filtersView; - const nextElement = nextView === 'preferences' ? preferencesView : filtersView; - - if (currentView === nextView) { - clearPanelViewAnimationState(filtersView); - clearPanelViewAnimationState(preferencesView); - nextElement.classList.add('active'); - syncPanelViewContainerHeight(nextElement); - CLEANPLAATS.panelState.activeView = nextView; - - if (persist) { - saveSettings().catch(error => { - console.error('Cleanplaats: Failed to store active panel view', error); - }); - } - return; - } - - if (!animated) { - clearPanelViewAnimationState(filtersView); - clearPanelViewAnimationState(preferencesView); - nextElement.classList.add('active'); - syncPanelViewContainerHeight(nextElement); - CLEANPLAATS.panelState.activeView = nextView; - - if (persist) { - saveSettings().catch(error => { - console.error('Cleanplaats: Failed to store active panel view', error); - }); - } - return; - } - - const direction = getPanelViewDirection(currentView, nextView); - const fromHeight = currentElement.scrollHeight; - const nextHeight = measurePanelViewHeight(nextElement); - - clearPanelViewAnimationState(filtersView); - clearPanelViewAnimationState(preferencesView); - - currentElement.classList.add('active', 'is-leaving', direction === 'down' ? 'is-leaving-up' : 'is-leaving-down'); - nextElement.classList.add('active', 'is-entering', direction === 'down' ? 'is-entering-down' : 'is-entering-up'); - - viewsContainer.style.height = `${fromHeight}px`; - void viewsContainer.offsetHeight; - - requestAnimationFrame(() => { - viewsContainer.style.height = `${nextHeight}px`; - currentElement.classList.remove(direction === 'down' ? 'is-leaving-up' : 'is-leaving-down'); - nextElement.classList.remove(direction === 'down' ? 'is-entering-down' : 'is-entering-up'); - }); - - window.clearTimeout(viewsContainer._cleanplaatsViewAnimationTimer); - viewsContainer._cleanplaatsViewAnimationTimer = window.setTimeout(() => { - clearPanelViewAnimationState(currentElement); - clearPanelViewAnimationState(nextElement); - nextElement.classList.add('active'); - syncPanelViewContainerHeight(nextElement); - }, 340); - - CLEANPLAATS.panelState.activeView = nextView; - - if (persist) { - saveSettings().catch(error => { - console.error('Cleanplaats: Failed to store active panel view', error); - }); - } -} - -function setupGlobalTooltip() { - const tooltip = document.getElementById('cleanplaats-global-tooltip'); - if (!tooltip) return; - document.querySelectorAll('.cleanplaats-tooltip-icon').forEach(icon => { - icon.addEventListener('mouseenter', function () { - const text = icon.getAttribute('data-tooltip'); - if (!text) return; - tooltip.textContent = text; - tooltip.style.display = 'block'; - const rect = icon.getBoundingClientRect(); - const tooltipRect = tooltip.getBoundingClientRect(); - let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); - left = Math.max(8, Math.min(left, window.innerWidth - tooltipRect.width - 8)); - let top = rect.top - tooltipRect.height - 8; - if (top < 8) { - top = rect.bottom + 8; - } - tooltip.style.left = left + 'px'; - tooltip.style.top = top + 'px'; - tooltip.style.opacity = '1'; - }); - icon.addEventListener('mouseleave', function () { - tooltip.style.opacity = '0'; - tooltip.style.display = 'none'; - }); - }); -} - -function syncSellerAgeThresholdControlsState() { - const controls = document.getElementById('cleanplaats-seller-age-threshold-controls'); - const valueInput = document.getElementById('cleanplaats-seller-age-threshold-value'); - const unitSelect = document.getElementById('cleanplaats-seller-age-threshold-unit'); - const isEnabled = Boolean(CLEANPLAATS.settings.sellerAgeWarningEnabled); - - if (controls) { - controls.classList.toggle('is-disabled', !isEnabled); - } - - if (valueInput) { - valueInput.disabled = !isEnabled; - } - - if (unitSelect) { - unitSelect.disabled = !isEnabled; - } -} - -function handleSellerAgeThresholdChange() { - const valueInput = document.getElementById('cleanplaats-seller-age-threshold-value'); - const unitSelect = document.getElementById('cleanplaats-seller-age-threshold-unit'); - - if (!valueInput || !unitSelect) { - return; - } - - const nextValue = Math.min(99, Math.max(1, parseInt(valueInput.value, 10) || 1)); - valueInput.value = String(nextValue); - CLEANPLAATS.settings.sellerAgeWarningThresholdValue = nextValue; - CLEANPLAATS.settings.sellerAgeWarningThresholdUnit = unitSelect.value; - CLEANPLAATS.runtime.lastSellerAgeWarningKey = ''; - - saveSettings() - .then(() => { - showSettingFeedback(); - scheduleSellerAgeWarningCheck({ force: true }); - }) - .catch(error => { - console.error('Cleanplaats: Failed to save seller age threshold', error); - }); -} - -function handleSellerAgeThresholdInput() { - const valueInput = document.getElementById('cleanplaats-seller-age-threshold-value'); - if (!valueInput) { - return; - } - - const rawValue = String(valueInput.value || '').replace(/[^\d]/g, ''); - if (!rawValue) { - return; - } - - const nextValue = Math.min(99, Math.max(1, parseInt(rawValue, 10) || 1)); - valueInput.value = String(nextValue); - CLEANPLAATS.settings.sellerAgeWarningThresholdValue = nextValue; - CLEANPLAATS.runtime.lastSellerAgeWarningKey = ''; - - saveSettings() - .then(() => { - // Persist immediately so refreshes don't lose the typed value, - // but avoid re-triggering the warning toast on every keystroke. - }) - .catch(error => { - console.error('Cleanplaats: Failed to store seller age threshold value during input', error); - }); -} - -function setupEventListeners() { - const panel = document.getElementById('cleanplaats-panel'); - const toggle = document.getElementById('cleanplaats-toggle'); - const openPreferencesButton = document.getElementById('cleanplaats-open-preferences'); - const backToFiltersButton = document.getElementById('cleanplaats-back-to-filters'); - - if (panel) { - panel.addEventListener('click', (e) => { - if (panel.classList.contains('animating')) { - return; - } - - const isPanelCollapsed = panel.classList.contains('collapsed'); - let canToggle = false; - - if (isPanelCollapsed) { - if (e.target === panel) { - canToggle = true; - } - } else { - const header = document.getElementById('cleanplaats-header'); - if (header && header.contains(e.target)) { - if ( - e.target.id === 'cleanplaats-toggle' || - !e.target.closest('input, button, a, .cleanplaats-tooltip, .cleanplaats-switch') - ) { - canToggle = true; - } - } - } - - if (canToggle) { - e.preventDefault(); - e.stopPropagation(); - - const blacklistModal = document.getElementById('cleanplaats-blacklist-modal'); - const termsModal = document.getElementById('cleanplaats-terms-modal'); - if (blacklistModal && blacklistModal.style.display === 'block') { - blacklistModal.style.display = 'none'; - } - if (termsModal && termsModal.style.display === 'block') { - termsModal.style.display = 'none'; - } - - panel.classList.remove('collapsed-ready'); - updateCollapsedPanelIcon(panel); - panel.classList.add('animating'); - - CLEANPLAATS.panelState.isCollapsed = !CLEANPLAATS.panelState.isCollapsed; - panel.classList.toggle('collapsed', CLEANPLAATS.panelState.isCollapsed); - - if (toggle) { - toggle.textContent = CLEANPLAATS.panelState.isCollapsed ? '▲' : '▼'; - } - - const fallbackTimeout = setTimeout(() => { - panel.classList.remove('animating'); - if (CLEANPLAATS.panelState.isCollapsed) { - panel.classList.add('collapsed-ready'); - updateCollapsedPanelIcon(panel); - } - }, 600); - - const onTransitionEnd = (event) => { - if (CLEANPLAATS.panelState.isCollapsed && event.propertyName === 'width') { - panel.classList.add('collapsed-ready'); - updateCollapsedPanelIcon(panel); - panel.classList.remove('animating'); - panel.removeEventListener('transitionend', onTransitionEnd); - clearTimeout(fallbackTimeout); - } else if (!CLEANPLAATS.panelState.isCollapsed && event.propertyName === 'max-height') { - panel.classList.remove('animating'); - updateCollapsedPanelIcon(panel); - panel.removeEventListener('transitionend', onTransitionEnd); - clearTimeout(fallbackTimeout); - } - }; - panel.addEventListener('transitionend', onTransitionEnd); - - saveSettings(); - } - }); - } - - openPreferencesButton?.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - setActivePanelView('preferences'); - }); - - backToFiltersButton?.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - setActivePanelView('filters'); - }); - - ['removeTopAds', 'removeDagtoppers', 'removePromotedListings', - 'removeOpvalStickers', 'removeReservedListings', 'removeFavoriteRelatedAds', 'sellerAgeWarningEnabled'].forEach(id => { - const checkbox = document.getElementById(id); - if (checkbox) { - checkbox.addEventListener('change', handleCheckboxChange); - } - }); - - const sellerAgeThresholdValue = document.getElementById('cleanplaats-seller-age-threshold-value'); - const sellerAgeThresholdUnit = document.getElementById('cleanplaats-seller-age-threshold-unit'); - sellerAgeThresholdValue?.addEventListener('input', handleSellerAgeThresholdInput); - sellerAgeThresholdValue?.addEventListener('change', handleSellerAgeThresholdChange); - sellerAgeThresholdUnit?.addEventListener('change', handleSellerAgeThresholdChange); - syncSellerAgeThresholdControlsState(); - - const themeToggle = document.getElementById('cleanplaats-theme-toggle'); - if (themeToggle) { - themeToggle.addEventListener('click', handleThemeToggle); - themeToggle.addEventListener('keydown', (event) => { - if (event.key !== 'Enter' && event.key !== ' ') return; - event.preventDefault(); - handleThemeToggle(); - }); - } - - setupResultsDropdownListener(); - setupSortDropdownListener(); - setupMarketplaceSortSync(); -} - -function handleThemeToggle() { - const nextValue = !CLEANPLAATS.settings.darkMode; - CLEANPLAATS.settings.darkMode = nextValue; - applyDarkModeToDocument(nextValue); - syncDarkModeToggle(nextValue); - - saveSettings() - .then(() => { - showSettingFeedback(); - }) - .catch(error => { - console.error('Cleanplaats: Failed to apply dark mode', error); - CLEANPLAATS.settings.darkMode = !nextValue; - applyDarkModeToDocument(!nextValue); - syncDarkModeToggle(!nextValue); - }); -} - -function handleCheckboxChange(event) { - const setting = event.target.id; - const value = event.target.checked; - - CLEANPLAATS.settings[setting] = value; - - if (setting === 'sellerAgeWarningEnabled') { - CLEANPLAATS.runtime.lastSellerAgeWarningKey = ''; - syncSellerAgeThresholdControlsState(); - } - - saveSettings() - .then(() => { - if (setting === 'darkMode') { - applyDarkModeToDocument(value); - showSettingFeedback(); - return; - } - - if (setting === 'sellerAgeWarningEnabled') { - showSettingFeedback(); - scheduleSellerAgeWarningCheck({ force: true }); - return; - } - - resetPreviousChanges(); - performCleanup(); - - clearBubbleNotification(); - showSettingFeedback(); - checkForEmptyPage(); - updateStatsDisplay(); - }) - .catch(error => { - console.error('Cleanplaats: Failed to apply setting', error); - event.target.checked = !value; - if (setting === 'darkMode') { - CLEANPLAATS.settings[setting] = !value; - applyDarkModeToDocument(!value); - } - }); -} - -function applySettings() { - saveSettings() - .then(() => { - applyDarkModeToDocument(CLEANPLAATS.settings.darkMode); - resetPreviousChanges(); - performCleanup(); - }) - .catch(error => { - console.error('Cleanplaats: Failed to apply settings', error); - }); -} - -function setupResultsDropdownListener() { - const dropdown = document.getElementById('cleanplaats-results-dropdown'); - if (!dropdown) return; - - dropdown.addEventListener('change', (e) => { - const value = parseInt(e.target.value, 10); - - CLEANPLAATS.settings.resultsPerPage = value; - wakeUpBackground(); - - saveSettings().then(() => { - showSettingFeedback(); - - if (isSearchResultsPage()) { - setTimeout(() => { - window.location.reload(); - }, 1000); - } - }); - }); -} - -function setupSortDropdownListener() { - const dropdown = document.getElementById('cleanplaats-sort-dropdown'); - if (!dropdown) return; - - dropdown.addEventListener('change', (e) => { - const value = e.target.value; - - CLEANPLAATS.settings.defaultSortMode = value; - CLEANPLAATS.settings.sortPreferenceSource = 'cleanplaats'; - wakeUpBackground(); - - saveSettings().then(() => { - showSettingFeedback(); - - if (isSearchResultsPage()) { - setTimeout(() => { - window.location.reload(); - }, 1000); - } - }); - }); -} diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 3e84298..0000000 --- a/manifest.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "manifest_version": 3, - "name": "Cleanplaats - Marktplaats zonder spam", - "version": "2.0.7", - "description": "Zelf in de hand wat je wel én niet wil zien op Marktplaats door te filteren", - "author": "", - "permissions": [ - "storage", - "scripting", - "tabs", - "webNavigation", - "declarativeNetRequest", - "alarms" - ], - "host_permissions": [ - "*://*.marktplaats.nl/*", - "*://*.2dehands.be/*", - "*://*.2ememain.be/*" - ], - "background": { - "service_worker": "background.js", - "scripts": ["background.js"], - "preferred_environment": ["service_worker", "document"] - }, - "content_scripts": [ - { - "matches": [ - "*://*.marktplaats.nl/*", - "*://*.2dehands.be/*", - "*://*.2ememain.be/*" - ], - "js": ["theme-init.js"], - "css": ["dark-mode.css"], - "all_frames": true, - "run_at": "document_start" - }, - { - "matches": [ - "*://*.marktplaats.nl/*", - "*://*.2dehands.be/*", - "*://*.2ememain.be/*" - ], - "js": [ - "purify.min.js", - "content/shared.js", - "content/theme.js", - "content/storage.js", - "content/notifications.js", - "content/cleanup.js", - "content/blacklist.js", - "content/ui.js", - "content/observers.js", - "content/init.js", - "content.js" - ], - "css": ["content.css"], - "run_at": "document_end" - } - ], - "icons": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - }, - "web_accessible_resources": [ - { - "resources": ["icons/*", "dark-mode.css"], - "matches": ["*://*.marktplaats.nl/*", "*://*.2dehands.be/*", "*://*.2ememain.be/*"] - } - ], - "action": { - "default_title": "Cleanplaats", - "default_icon": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - } - }, - "browser_specific_settings": { - "gecko": { - "id": "cleanplaats@cleanplaats.dev", - "strict_min_version": "121.0" - }, - "gecko_android": { - "strict_min_version": "121.0" - } - } -} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..76fa88c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5989 @@ +{ + "name": "cleanplaats", + "version": "2.0.7", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cleanplaats", + "version": "2.0.7", + "hasInstallScript": true, + "dependencies": { + "react": "^19.2.5", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@wxt-dev/module-react": "^1.2.2", + "typescript": "^6.0.2", + "vitest": "^4.1.4", + "wxt": "^0.20.22" + } + }, + "node_modules/@1natsu/wait-element": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@1natsu/wait-element/-/wait-element-4.2.0.tgz", + "integrity": "sha512-Om0Q+WE9mNrpY4AwMTvkFiYHv8VM7TML3PvOqXy+w6kAjLjKhGYHYX+305+a6J8RVpds9s7IF2Z5aOPYwULFNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "many-keys-map": "^3.0.0" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@aklinker1/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@devicefarmer/adbkit": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit/-/adbkit-3.3.8.tgz", + "integrity": "sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@devicefarmer/adbkit-logcat": "^2.1.2", + "@devicefarmer/adbkit-monkey": "~1.2.1", + "bluebird": "~3.7", + "commander": "^9.1.0", + "debug": "~4.3.1", + "node-forge": "^1.3.1", + "split": "~1.0.1" + }, + "bin": { + "adbkit": "bin/adbkit" + }, + "engines": { + "node": ">= 0.10.4" + } + }, + "node_modules/@devicefarmer/adbkit-logcat": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-logcat/-/adbkit-logcat-2.1.3.tgz", + "integrity": "sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@devicefarmer/adbkit-monkey": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-monkey/-/adbkit-monkey-1.2.1.tgz", + "integrity": "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.10.4" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webext-core/fake-browser": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@webext-core/fake-browser/-/fake-browser-1.3.4.tgz", + "integrity": "sha512-nZcVWr3JpwpS5E6hKpbAwAMBM/AXZShnfW0F76udW8oLd6Kv0nbW6vFS07md4Na/0ntQonk3hFnlQYGYBAlTrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.merge": "^4.6.2" + } + }, + "node_modules/@webext-core/isolated-element": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@webext-core/isolated-element/-/isolated-element-1.1.5.tgz", + "integrity": "sha512-4m6oP8Vzm/68YO1QmkUOZqqUcmyBtA53tji2g00/nYXE3E3IceYgeub7eIqvXDV2Z7xU6cm6qO1IMt4XFVwtvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-potential-custom-element-name": "^1.0.1" + } + }, + "node_modules/@webext-core/match-patterns": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@webext-core/match-patterns/-/match-patterns-1.0.3.tgz", + "integrity": "sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wxt-dev/browser": { + "version": "0.1.40", + "resolved": "https://registry.npmjs.org/@wxt-dev/browser/-/browser-0.1.40.tgz", + "integrity": "sha512-h2/v/Hpkj5sz//h84ProqBaAcTsDFRKp9b/JVHOK/r7LT0XLE+ZDs5YN1BnFLUEHdM7G3fUjTyBG84cayXQshQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@wxt-dev/module-react": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@wxt-dev/module-react/-/module-react-1.2.2.tgz", + "integrity": "sha512-+lRLi1r9dAXpLySWSIWHLJ1h/nFzR20iQnx3RNrKyA6oJg4+ClOluVXozHjfPg9Okfy/umtffiOopGayASrg6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitejs/plugin-react": "^4.4.1 || ^5.0.0 || ^6.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/wxt-dev" + }, + "peerDependencies": { + "vite": "^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0", + "wxt": ">=0.19.16" + } + }, + "node_modules/@wxt-dev/storage": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@wxt-dev/storage/-/storage-1.2.8.tgz", + "integrity": "sha512-GWCFKgF5+d7eslOxUDFC70ypA9njupmJb1nQM8uZoX0J3sWT2BO5xJLzb1sYahWAfID9p2BMtnUBN1lkWxPsbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wxt-dev/browser": "^0.1.37", + "async-mutex": "^0.5.0", + "dequal": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/wxt-dev" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-differ": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-4.0.0.tgz", + "integrity": "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-launcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.0.tgz", + "integrity": "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.cjs" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chrome-launcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chrome-launcher/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chrome-launcher/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/configstore": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", + "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "atomically": "^2.0.3", + "dot-prop": "^9.0.0", + "graceful-fs": "^4.2.11", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "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/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/filesize": { + "version": "11.0.15", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.15.tgz", + "integrity": "sha512-30TpbYxQxCpi4XdVjkwXYQ37CzZltV38+P7MYroQ+4NK/Dmx9mxixFNrolzcmEIBsjT/uowC9T7kiy2+C12r1A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.8.0" + } + }, + "node_modules/firefox-profile": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/firefox-profile/-/firefox-profile-4.7.0.tgz", + "integrity": "sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "adm-zip": "~0.5.x", + "fs-extra": "^11.2.0", + "ini": "^4.1.3", + "minimist": "^1.2.8", + "xml2js": "^0.6.2" + }, + "bin": { + "firefox-profile": "lib/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/formdata-node": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz", + "integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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/fx-runner": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/fx-runner/-/fx-runner-1.4.0.tgz", + "integrity": "sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "commander": "2.9.0", + "shell-quote": "1.7.3", + "spawn-sync": "1.0.15", + "when": "3.7.7", + "which": "1.2.4", + "winreg": "0.0.12" + }, + "bin": { + "fx-runner": "bin/fx-runner" + } + }, + "node_modules/fx-runner/node_modules/commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "dev": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "dev": true, + "license": "MIT" + }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-absolute": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz", + "integrity": "sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "dev": true, + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "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/is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-relative": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz", + "integrity": "sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz", + "integrity": "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/latest-version": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", + "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz", + "integrity": "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/linkedom": { + "version": "0.18.12", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz", + "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.0.0", + "uhyphen": "^0.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": ">= 2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/many-keys-map": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/many-keys-map/-/many-keys-map-3.0.3.tgz", + "integrity": "sha512-1DiZmDHPXMBgMRjeUtHy1q1VYmeJscHxhIAexX9z/zjRMP80+0ETuPfssi8z+kMY4DwUgsKuHqpjxgmeA9gBNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/fregante" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "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/multimatch": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-6.0.0.tgz", + "integrity": "sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.5", + "array-differ": "^4.0.0", + "array-union": "^3.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nano-spawn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.1.0.tgz", + "integrity": "sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "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/nanospinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz", + "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-notifier": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-10.0.1.tgz", + "integrity": "sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.5", + "shellwords": "^0.1.1", + "uuid": "^8.3.2", + "which": "^2.0.2" + } + }, + "node_modules/node-notifier/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/node-notifier/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-notifier/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-notifier/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-shim": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", + "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/package-json": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", + "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ky": "^1.2.0", + "registry-auth-token": "^5.0.2", + "registry-url": "^6.0.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "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/pino": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz", + "integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "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/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/promise-toolbox": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/promise-toolbox/-/promise-toolbox-0.21.0.tgz", + "integrity": "sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==", + "dev": true, + "license": "ISC", + "dependencies": { + "make-error": "^1.3.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/publish-browser-extension": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/publish-browser-extension/-/publish-browser-extension-4.0.5.tgz", + "integrity": "sha512-EePAn3VIHJS/jqCuvs1NgPgoecCT8+RsES76hbgYe2Ze1dyvB0tX60C1PCrV8Z8fv56mW3E59s9Gd/GwWiw7dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "consola": "^3.4.2", + "dotenv": "^17.2.4", + "form-data-encoder": "^4.1.0", + "formdata-node": "^6.0.3", + "jsonwebtoken": "^9.0.3", + "listr2": "^10.1.0", + "ofetch": "^1.5.1", + "zod": "3.25.76 || ^4.3.6" + }, + "bin": { + "publish-extension": "bin/publish-extension.mjs" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/publish-browser-extension/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/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + }, + "engines": { + "node": ">=11.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "license": "MIT" + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-sync": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", + "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "concat-stream": "^1.4.7", + "os-shim": "^0.1.2" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-5.0.0.tgz", + "integrity": "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.2.tgz", + "integrity": "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "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": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "dev": true, + "license": "ISC" + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-6.1.0.tgz", + "integrity": "sha512-ocgNKyiqj7Hw7oHt7A7D3za3fq28eShe1EloL6hsoQgn7CF51Y4CqFT9ISG3rEy0JpA8CCz/sY5h5OovOn62VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.2", + "pathe": "^2.0.3", + "picomatch": "^4.0.4", + "pkg-types": "^2.3.0", + "scule": "^1.3.0", + "strip-literal": "^3.1.0", + "tinyglobby": "^0.2.16", + "unplugin": "^3.0.0", + "unplugin-utils": "^0.3.1" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/update-notifier": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", + "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "configstore": "^7.0.0", + "is-in-ci": "^1.0.0", + "is-installed-globally": "^1.0.0", + "is-npm": "^6.0.0", + "latest-version": "^9.0.0", + "pupa": "^3.1.0", + "semver": "^7.6.3", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "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", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-6.0.0.tgz", + "integrity": "sha512-oj4PVrT+pDh6GYf5wfUXkcZyekYS8kKPfLPXVl8qe324Ec6l4K2DUKNadRbZ3LQl0qGcDz+PyOo7ZAh00Y+JjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^7.0.0", + "es-module-lexer": "^2.0.0", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "vite": "^8.0.0" + }, + "bin": { + "vite-node": "dist/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://opencollective.com/antfu" + } + }, + "node_modules/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/web-ext-run": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/web-ext-run/-/web-ext-run-0.2.4.tgz", + "integrity": "sha512-rQicL7OwuqWdQWI33JkSXKcp7cuv1mJG8u3jRQwx/8aDsmhbTHs9ZRmNYOL+LX0wX8edIEQX8jj4bB60GoXtKA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@babel/runtime": "7.28.2", + "@devicefarmer/adbkit": "3.3.8", + "chrome-launcher": "1.2.0", + "debounce": "1.2.1", + "es6-error": "4.1.1", + "firefox-profile": "4.7.0", + "fx-runner": "1.4.0", + "multimatch": "6.0.0", + "node-notifier": "10.0.1", + "parse-json": "7.1.1", + "pino": "9.7.0", + "promise-toolbox": "0.21.0", + "set-value": "4.1.0", + "source-map-support": "0.5.21", + "strip-bom": "5.0.0", + "strip-json-comments": "5.0.2", + "tmp": "0.2.5", + "update-notifier": "7.3.1", + "watchpack": "2.4.4", + "zip-dir": "2.0.0" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/when": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/when/-/when-3.7.7.tgz", + "integrity": "sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw==", + "dev": true, + "license": "MIT" + }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.4.tgz", + "integrity": "sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-absolute": "^0.1.7", + "isexe": "^1.1.1" + }, + "bin": { + "which": "bin/which" + } + }, + "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/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/winreg": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-0.0.12.tgz", + "integrity": "sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ==", + "dev": true, + "license": "BSD" + }, + "node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wxt": { + "version": "0.20.22", + "resolved": "https://registry.npmjs.org/wxt/-/wxt-0.20.22.tgz", + "integrity": "sha512-njFI77H0dAbK/bQCN8u8QYiusg6GKDPMtsQDCqIfrh1oGHMHgrMEMgeGOlqAltG9OOGGB1DvFYDzTvxqfEKVKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@1natsu/wait-element": "^4.1.2", + "@aklinker1/rollup-plugin-visualizer": "5.12.0", + "@webext-core/fake-browser": "^1.3.4", + "@webext-core/isolated-element": "^1.1.3", + "@webext-core/match-patterns": "^1.0.3", + "@wxt-dev/browser": "^0.1.40", + "@wxt-dev/storage": "^1.0.0", + "async-mutex": "^0.5.0", + "c12": "^3.3.3", + "cac": "^6.7.14 || ^7.0.0", + "chokidar": "^5.0.0", + "ci-info": "^4.4.0", + "consola": "^3.4.2", + "defu": "^6.1.4", + "dotenv-expand": "^12.0.3", + "esbuild": "^0.27.1", + "filesize": "^11.0.15", + "get-port-please": "^3.2.0", + "giget": "^1.2.3 || ^2.0.0 || ^3.0.0", + "hookable": "^6.1.0", + "import-meta-resolve": "^4.2.0", + "is-wsl": "^3.1.1", + "json5": "^2.2.3", + "jszip": "^3.10.1", + "linkedom": "^0.18.12", + "magicast": "^0.5.2", + "nano-spawn": "^2.0.0", + "nanospinner": "^1.2.2", + "normalize-path": "^3.0.0", + "nypm": "^0.6.5", + "ohash": "^2.0.11", + "open": "^11.0.0", + "perfect-debounce": "^2.1.0", + "picomatch": "^4.0.3", + "prompts": "^2.4.2", + "publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.4", + "scule": "^1.3.0", + "tinyglobby": "^0.2.15", + "unimport": "^3.13.1 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "vite": "^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0", + "vite-node": "^3.2.4 || ^5.0.0 || ^6.0.0", + "web-ext-run": "^0.2.4" + }, + "bin": { + "wxt": "bin/wxt.mjs", + "wxt-publish-extension": "bin/wxt-publish-extension.mjs" + }, + "engines": { + "bun": ">=1.2.0", + "node": ">=20.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/wxt-dev" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/zip-dir/-/zip-dir-2.0.0.tgz", + "integrity": "sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0", + "jszip": "^3.2.2" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..26e37ed --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "cleanplaats", + "description": "Cleanplaats browser extension rewritten with WXT + React + TypeScript", + "private": true, + "version": "2.0.7", + "type": "module", + "scripts": { + "dev": "wxt", + "dev:firefox": "wxt -b firefox --mv3", + "build": "wxt build", + "build:firefox": "wxt build -b firefox --mv3", + "zip": "wxt zip", + "zip:firefox": "wxt zip -b firefox --mv3", + "compile": "tsc --noEmit", + "test": "vitest run", + "postinstall": "wxt prepare" + }, + "dependencies": { + "react": "^19.2.5", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@wxt-dev/module-react": "^1.2.2", + "typescript": "^6.0.2", + "vitest": "^4.1.4", + "wxt": "^0.20.22" + } +} diff --git a/public/icons/ChatGPT Image Apr 13, 2025, 05_49_24 PM.png b/public/icons/ChatGPT Image Apr 13, 2025, 05_49_24 PM.png new file mode 100644 index 0000000..04452d7 Binary files /dev/null and b/public/icons/ChatGPT Image Apr 13, 2025, 05_49_24 PM.png differ diff --git a/public/icons/darkmode_icon_128.png b/public/icons/darkmode_icon_128.png new file mode 100644 index 0000000..1105905 Binary files /dev/null and b/public/icons/darkmode_icon_128.png differ diff --git a/public/icons/icon128.png b/public/icons/icon128.png new file mode 100644 index 0000000..8d516c7 Binary files /dev/null and b/public/icons/icon128.png differ diff --git a/public/icons/icon16.png b/public/icons/icon16.png new file mode 100644 index 0000000..8e08971 Binary files /dev/null and b/public/icons/icon16.png differ diff --git a/public/icons/icon48.png b/public/icons/icon48.png new file mode 100644 index 0000000..be5f03a Binary files /dev/null and b/public/icons/icon48.png differ diff --git a/public/icons/marktplaats-logo-darkmode.svg b/public/icons/marktplaats-logo-darkmode.svg new file mode 100644 index 0000000..1781e83 --- /dev/null +++ b/public/icons/marktplaats-logo-darkmode.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/purify.min.js b/purify.min.js deleted file mode 100644 index 2ed0e76..0000000 --- a/purify.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! @license DOMPurify 3.2.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.5/LICENSE */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=R(Array.prototype.forEach),m=R(Array.prototype.lastIndexOf),p=R(Array.prototype.pop),f=R(Array.prototype.push),d=R(Array.prototype.splice),h=R(String.prototype.toLowerCase),g=R(String.prototype.toString),T=R(String.prototype.match),y=R(String.prototype.replace),E=R(String.prototype.indexOf),A=R(String.prototype.trim),_=R(Object.prototype.hasOwnProperty),S=R(RegExp.prototype.test),b=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:h;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function O(e){for(let t=0;t/gm),G=a(/\$\{[\w\W]*/gm),Y=a(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=a(/^aria-[\-\w]+$/),X=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),q=a(/^(?:\w+script|data):/i),$=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),K=a(/^html$/i),V=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var Z=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:$,CUSTOM_ELEMENT:V,DATA_ATTR:Y,DOCTYPE_NAME:K,ERB_EXPR:W,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:q,MUSTACHE_EXPR:B,TMPLIT_EXPR:G});const J=1,Q=3,ee=7,te=8,ne=9,oe=function(){return"undefined"==typeof window?null:window};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe();const o=e=>t(e);if(o.version="3.2.5",o.removed=[],!n||!n.document||n.document.nodeType!==ne||!n.Element)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:R,Element:O,NodeFilter:B,NamedNodeMap:W=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:G,DOMParser:Y,trustedTypes:j}=n,q=O.prototype,$=v(q,"cloneNode"),V=v(q,"remove"),re=v(q,"nextSibling"),ie=v(q,"childNodes"),ae=v(q,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let le,ce="";const{implementation:se,createNodeIterator:ue,createDocumentFragment:me,getElementsByTagName:pe}=r,{importNode:fe}=a;let de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof e&&"function"==typeof ae&&se&&void 0!==se.createHTMLDocument;const{MUSTACHE_EXPR:he,ERB_EXPR:ge,TMPLIT_EXPR:Te,DATA_ATTR:ye,ARIA_ATTR:Ee,IS_SCRIPT_OR_DATA:Ae,ATTR_WHITESPACE:_e,CUSTOM_ELEMENT:Se}=Z;let{IS_ALLOWED_URI:be}=Z,Ne=null;const Re=w({},[...L,...C,...x,...M,...U]);let we=null;const Oe=w({},[...z,...P,...H,...F]);let De=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,Le=null,Ce=!0,xe=!0,Ie=!1,Me=!0,ke=!1,Ue=!0,ze=!1,Pe=!1,He=!1,Fe=!1,Be=!1,We=!1,Ge=!0,Ye=!1,je=!0,Xe=!1,qe={},$e=null;const Ke=w({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Ze=w({},["audio","video","img","source","image","track"]);let Je=null;const Qe=w({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",tt="http://www.w3.org/2000/svg",nt="http://www.w3.org/1999/xhtml";let ot=nt,rt=!1,it=null;const at=w({},[et,tt,nt],g);let lt=w({},["mi","mo","mn","ms","mtext"]),ct=w({},["annotation-xml"]);const st=w({},["title","style","font","a","script"]);let ut=null;const mt=["application/xhtml+xml","text/html"];let pt=null,ft=null;const dt=r.createElement("form"),ht=function(e){return e instanceof RegExp||e instanceof Function},gt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ft||ft!==e){if(e&&"object"==typeof e||(e={}),e=D(e),ut=-1===mt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,pt="application/xhtml+xml"===ut?g:h,Ne=_(e,"ALLOWED_TAGS")?w({},e.ALLOWED_TAGS,pt):Re,we=_(e,"ALLOWED_ATTR")?w({},e.ALLOWED_ATTR,pt):Oe,it=_(e,"ALLOWED_NAMESPACES")?w({},e.ALLOWED_NAMESPACES,g):at,Je=_(e,"ADD_URI_SAFE_ATTR")?w(D(Qe),e.ADD_URI_SAFE_ATTR,pt):Qe,Ve=_(e,"ADD_DATA_URI_TAGS")?w(D(Ze),e.ADD_DATA_URI_TAGS,pt):Ze,$e=_(e,"FORBID_CONTENTS")?w({},e.FORBID_CONTENTS,pt):Ke,ve=_(e,"FORBID_TAGS")?w({},e.FORBID_TAGS,pt):{},Le=_(e,"FORBID_ATTR")?w({},e.FORBID_ATTR,pt):{},qe=!!_(e,"USE_PROFILES")&&e.USE_PROFILES,Ce=!1!==e.ALLOW_ARIA_ATTR,xe=!1!==e.ALLOW_DATA_ATTR,Ie=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Me=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ke=e.SAFE_FOR_TEMPLATES||!1,Ue=!1!==e.SAFE_FOR_XML,ze=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,Be=e.RETURN_DOM_FRAGMENT||!1,We=e.RETURN_TRUSTED_TYPE||!1,He=e.FORCE_BODY||!1,Ge=!1!==e.SANITIZE_DOM,Ye=e.SANITIZE_NAMED_PROPS||!1,je=!1!==e.KEEP_CONTENT,Xe=e.IN_PLACE||!1,be=e.ALLOWED_URI_REGEXP||X,ot=e.NAMESPACE||nt,lt=e.MATHML_TEXT_INTEGRATION_POINTS||lt,ct=e.HTML_INTEGRATION_POINTS||ct,De=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(De.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(De.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(De.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ke&&(xe=!1),Be&&(Fe=!0),qe&&(Ne=w({},U),we=[],!0===qe.html&&(w(Ne,L),w(we,z)),!0===qe.svg&&(w(Ne,C),w(we,P),w(we,F)),!0===qe.svgFilters&&(w(Ne,x),w(we,P),w(we,F)),!0===qe.mathMl&&(w(Ne,M),w(we,H),w(we,F))),e.ADD_TAGS&&(Ne===Re&&(Ne=D(Ne)),w(Ne,e.ADD_TAGS,pt)),e.ADD_ATTR&&(we===Oe&&(we=D(we)),w(we,e.ADD_ATTR,pt)),e.ADD_URI_SAFE_ATTR&&w(Je,e.ADD_URI_SAFE_ATTR,pt),e.FORBID_CONTENTS&&($e===Ke&&($e=D($e)),w($e,e.FORBID_CONTENTS,pt)),je&&(Ne["#text"]=!0),ze&&w(Ne,["html","head","body"]),Ne.table&&(w(Ne,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');le=e.TRUSTED_TYPES_POLICY,ce=le.createHTML("")}else void 0===le&&(le=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(j,c)),null!==le&&"string"==typeof ce&&(ce=le.createHTML(""));i&&i(e),ft=e}},Tt=w({},[...C,...x,...I]),yt=w({},[...M,...k]),Et=function(e){f(o.removed,{element:e});try{ae(e).removeChild(e)}catch(t){V(e)}},At=function(e,t){try{f(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){f(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(Fe||Be)try{Et(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},_t=function(e){let t=null,n=null;if(He)e=""+e;else{const t=T(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===ut&&ot===nt&&(e=''+e+"");const o=le?le.createHTML(e):e;if(ot===nt)try{t=(new Y).parseFromString(o,ut)}catch(e){}if(!t||!t.documentElement){t=se.createDocument(ot,"template",null);try{t.documentElement.innerHTML=rt?ce:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),ot===nt?pe.call(t,ze?"html":"body")[0]:ze?t.documentElement:i},St=function(e){return ue.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},bt=function(e){return e instanceof G&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof W)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Nt=function(e){return"function"==typeof R&&e instanceof R};function Rt(e,t,n){u(e,(e=>{e.call(o,t,n,ft)}))}const wt=function(e){let t=null;if(Rt(de.beforeSanitizeElements,e,null),bt(e))return Et(e),!0;const n=pt(e.nodeName);if(Rt(de.uponSanitizeElement,e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Nt(e.firstElementChild)&&S(/<[/\w!]/g,e.innerHTML)&&S(/<[/\w!]/g,e.textContent))return Et(e),!0;if(e.nodeType===ee)return Et(e),!0;if(Ue&&e.nodeType===te&&S(/<[/\w]/g,e.data))return Et(e),!0;if(!Ne[n]||ve[n]){if(!ve[n]&&Dt(n)){if(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n))return!1;if(De.tagNameCheck instanceof Function&&De.tagNameCheck(n))return!1}if(je&&!$e[n]){const t=ae(e)||e.parentNode,n=ie(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=$(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,re(e))}}}return Et(e),!0}return e instanceof O&&!function(e){let t=ae(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});const n=h(e.tagName),o=h(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===tt?t.namespaceURI===nt?"svg"===n:t.namespaceURI===et?"svg"===n&&("annotation-xml"===o||lt[o]):Boolean(Tt[n]):e.namespaceURI===et?t.namespaceURI===nt?"math"===n:t.namespaceURI===tt?"math"===n&&ct[o]:Boolean(yt[n]):e.namespaceURI===nt?!(t.namespaceURI===tt&&!ct[o])&&!(t.namespaceURI===et&&!lt[o])&&!yt[n]&&(st[n]||!Tt[n]):!("application/xhtml+xml"!==ut||!it[e.namespaceURI]))}(e)?(Et(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!S(/<\/no(script|embed|frames)/i,e.innerHTML)?(ke&&e.nodeType===Q&&(t=e.textContent,u([he,ge,Te],(e=>{t=y(t,e," ")})),e.textContent!==t&&(f(o.removed,{element:e.cloneNode()}),e.textContent=t)),Rt(de.afterSanitizeElements,e,null),!1):(Et(e),!0)},Ot=function(e,t,n){if(Ge&&("id"===t||"name"===t)&&(n in r||n in dt))return!1;if(xe&&!Le[t]&&S(ye,t));else if(Ce&&S(Ee,t));else if(!we[t]||Le[t]){if(!(Dt(e)&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,e)||De.tagNameCheck instanceof Function&&De.tagNameCheck(e))&&(De.attributeNameCheck instanceof RegExp&&S(De.attributeNameCheck,t)||De.attributeNameCheck instanceof Function&&De.attributeNameCheck(t))||"is"===t&&De.allowCustomizedBuiltInElements&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n)||De.tagNameCheck instanceof Function&&De.tagNameCheck(n))))return!1}else if(Je[t]);else if(S(be,y(n,_e,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==E(n,"data:")||!Ve[e]){if(Ie&&!S(Ae,y(n,_e,"")));else if(n)return!1}else;return!0},Dt=function(e){return"annotation-xml"!==e&&T(e,Se)},vt=function(e){Rt(de.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||bt(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=pt(a);let m="value"===a?c:A(c);if(n.attrName=s,n.attrValue=m,n.keepAttr=!0,n.forceKeepAttr=void 0,Rt(de.uponSanitizeAttribute,e,n),m=n.attrValue,!Ye||"id"!==s&&"name"!==s||(At(a,e),m="user-content-"+m),Ue&&S(/((--!?|])>)|<\/(style|title)/i,m)){At(a,e);continue}if(n.forceKeepAttr)continue;if(At(a,e),!n.keepAttr)continue;if(!Me&&S(/\/>/i,m)){At(a,e);continue}ke&&u([he,ge,Te],(e=>{m=y(m,e," ")}));const f=pt(e.nodeName);if(Ot(f,s,m)){if(le&&"object"==typeof j&&"function"==typeof j.getAttributeType)if(l);else switch(j.getAttributeType(f,s)){case"TrustedHTML":m=le.createHTML(m);break;case"TrustedScriptURL":m=le.createScriptURL(m)}try{l?e.setAttributeNS(l,a,m):e.setAttribute(a,m),bt(e)?Et(e):p(o.removed)}catch(e){}}}Rt(de.afterSanitizeAttributes,e,null)},Lt=function e(t){let n=null;const o=St(t);for(Rt(de.beforeSanitizeShadowDOM,t,null);n=o.nextNode();)Rt(de.uponSanitizeShadowNode,n,null),wt(n),vt(n),n.content instanceof s&&e(n.content);Rt(de.afterSanitizeShadowDOM,t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(rt=!e,rt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Nt(e)){if("function"!=typeof e.toString)throw b("toString is not a function");if("string"!=typeof(e=e.toString()))throw b("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Pe||gt(t),o.removed=[],"string"==typeof e&&(Xe=!1),Xe){if(e.nodeName){const t=pt(e.nodeName);if(!Ne[t]||ve[t])throw b("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof R)n=_t("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===J&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!ke&&!ze&&-1===e.indexOf("<"))return le&&We?le.createHTML(e):e;if(n=_t(e),!n)return Fe?null:We?ce:""}n&&He&&Et(n.firstChild);const c=St(Xe?e:n);for(;i=c.nextNode();)wt(i),vt(i),i.content instanceof s&&Lt(i.content);if(Xe)return e;if(Fe){if(Be)for(l=me.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(we.shadowroot||we.shadowrootmode)&&(l=fe.call(a,l,!0)),l}let m=ze?n.outerHTML:n.innerHTML;return ze&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&S(K,n.ownerDocument.doctype.name)&&(m="\n"+m),ke&&u([he,ge,Te],(e=>{m=y(m,e," ")})),le&&We?le.createHTML(m):m},o.setConfig=function(){gt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Pe=!0},o.clearConfig=function(){ft=null,Pe=!1},o.isValidAttribute=function(e,t,n){ft||gt({});const o=pt(e),r=pt(t);return Ot(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&f(de[e],t)},o.removeHook=function(e,t){if(void 0!==t){const n=m(de[e],t);return-1===n?void 0:d(de[e],n,1)[0]}return p(de[e])},o.removeHooks=function(e){de[e]=[]},o.removeAllHooks=function(){de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return re})); -//# sourceMappingURL=purify.min.js.map diff --git a/src/background/index.ts b/src/background/index.ts new file mode 100644 index 0000000..adb752d --- /dev/null +++ b/src/background/index.ts @@ -0,0 +1,40 @@ +import { + createBackgroundRuntimeState, + type BackgroundState, + type BackgroundRuntime, +} from '@/background/types'; +import { DEFAULT_SETTINGS } from '@/shared/constants/settings'; +import { KeepAliveService } from '@/background/services/keepalive'; +import { SettingsRepository } from '@/shared/storage/repository'; +import { createListenerController } from '@/background/services/listeners'; + +export async function initializeBackground(): Promise { + const state: BackgroundState = { + resultsPerPage: String(DEFAULT_SETTINGS.resultsPerPage), + defaultSortMode: DEFAULT_SETTINGS.defaultSortMode, + sortPreferenceSource: DEFAULT_SETTINGS.sortPreferenceSource, + }; + const runtimeState = createBackgroundRuntimeState(); + const runtime: BackgroundRuntime = { + wakeupNavigationFilters: runtimeState.wakeupNavigationFilters, + }; + const keepAlive = new KeepAliveService(runtimeState); + const settingsRepository = new SettingsRepository(); + + const listenerController = createListenerController({ + browserApi: browser, + state, + runtime, + keepAlive, + settingsRepository, + updateDarkModeStartupScript: async (_enabled) => { + // theme-init is loaded statically via manifest content scripts. + // Keep async hook as no-op to preserve prior behavior. + }, + }); + + await listenerController.initialize(); + listenerController.registerRuntimeListeners(); + keepAlive.setup(); +} + diff --git a/src/background/services/hash-url.ts b/src/background/services/hash-url.ts new file mode 100644 index 0000000..deb1f4d --- /dev/null +++ b/src/background/services/hash-url.ts @@ -0,0 +1,91 @@ +import { SORT_MODES } from '@/shared/constants/settings'; +import type { SortMode, SortPreferenceSource } from '@/shared/types/state'; + +export function parseHashOptions(hashStr: string): Record { + const options: Record = {}; + if (!hashStr || hashStr.length < 2) { + return options; + } + + const hashKeysValues = hashStr.substring(1).split('|'); + for (const hashKeyValue of hashKeysValues) { + const keyValue = hashKeyValue.split(':'); + if (keyValue.length !== 2) { + continue; + } + + const [key, value] = keyValue; + if (!key || !value) { + continue; + } + + options[key] = value; + } + + return options; +} + +export function buildHashOptions(options: Record): string { + const entries = Object.entries(options).filter(([, value]) => Boolean(value && value !== '')); + if (entries.length === 0) { + return ''; + } + + const serialized = entries + .map(([key, value]) => `${key}:${value}`) + .join('|'); + + return `#${serialized}`; +} + +export type ModifyUrlInput = { + urlString: string; + resultsPerPage: string; + defaultSortMode: SortMode; + sortPreferenceSource: SortPreferenceSource; +}; + +export function getModifiedUrlIfNeeded({ + urlString, + resultsPerPage, + defaultSortMode, + sortPreferenceSource, +}: ModifyUrlInput): string | null { + const url = new URL(urlString); + const options = parseHashOptions(url.hash); + let needsRewrite = false; + const hasExplicitSort = Boolean(options.sortBy && options.sortOrder); + const shouldApplyCleanplaatsSort = sortPreferenceSource !== 'marketplace'; + + if (!Object.prototype.hasOwnProperty.call(options, 'limit') || options.limit !== resultsPerPage) { + options.limit = resultsPerPage; + needsRewrite = true; + } + + if (shouldApplyCleanplaatsSort && defaultSortMode !== 'standard') { + const sortConfig = SORT_MODES[defaultSortMode]; + if ( + sortConfig + && ( + !hasExplicitSort + || options.sortBy !== sortConfig.sortBy + || options.sortOrder !== sortConfig.sortOrder + ) + ) { + options.sortBy = sortConfig.sortBy; + options.sortOrder = sortConfig.sortOrder; + needsRewrite = true; + } + } else if (shouldApplyCleanplaatsSort && defaultSortMode === 'standard' && hasExplicitSort) { + delete options.sortBy; + delete options.sortOrder; + needsRewrite = true; + } + + if (needsRewrite) { + url.hash = buildHashOptions(options); + return url.href; + } + + return null; +} diff --git a/src/background/services/keepalive.ts b/src/background/services/keepalive.ts new file mode 100644 index 0000000..4b8461a --- /dev/null +++ b/src/background/services/keepalive.ts @@ -0,0 +1,91 @@ +import { WAKEUP_NAVIGATION_FILTERS } from '@/shared/constants/domains'; +import type { BackgroundRuntimeState } from '@/background/types'; + +type BrowserApi = typeof browser; +type WebNavigationDetails = Parameters< + BrowserApi['webNavigation']['onBeforeNavigate']['addListener'] +>[0] extends (details: infer Details) => unknown + ? Details + : never; +type AlarmDetails = Parameters[0] extends ( + alarm: infer Alarm, +) => unknown + ? Alarm + : never; + +export class KeepAliveService { + private readonly browserApi = browser; + + private alarmName = 'cleanplaats-keepalive'; + + constructor(private readonly state: BackgroundRuntimeState) {} + + setup(): void { + if (typeof browser === 'undefined') { + return; + } + + this.browserApi.alarms.create(this.alarmName, { + delayInMinutes: 2, + periodInMinutes: 2, + }); + + if (!this.browserApi.alarms.onAlarm.hasListener(this.handleAlarm)) { + this.browserApi.alarms.onAlarm.addListener(this.handleAlarm); + } + + if (this.browserApi.webNavigation?.onBeforeNavigate) { + this.browserApi.webNavigation.onBeforeNavigate.addListener( + this.handleNavigationActivity, + { url: [...WAKEUP_NAVIGATION_FILTERS] }, + ); + } + } + + resetToActiveMode = (): void => { + if (typeof browser === 'undefined') { + return; + } + + this.state.lastMarketplaceActivity = Date.now(); + this.browserApi.alarms.clear(this.alarmName); + this.browserApi.alarms.create(this.alarmName, { + delayInMinutes: 2, + periodInMinutes: 2, + }); + }; + + private handleNavigationActivity = ( + details: WebNavigationDetails, + ): void => { + if (details.frameId !== 0) { + return; + } + this.state.lastMarketplaceActivity = Date.now(); + }; + + private handleAlarm = (alarm: AlarmDetails): void => { + if (alarm.name !== this.alarmName) { + return; + } + + const minutesSinceActivity = (Date.now() - this.state.lastMarketplaceActivity) / (1000 * 60); + + if (minutesSinceActivity > 30) { + this.browserApi.alarms.clear(this.alarmName); + this.browserApi.alarms.create(this.alarmName, { + delayInMinutes: 10, + periodInMinutes: 10, + }); + return; + } + + if (minutesSinceActivity > 10) { + this.browserApi.alarms.clear(this.alarmName); + this.browserApi.alarms.create(this.alarmName, { + delayInMinutes: 5, + periodInMinutes: 5, + }); + } + }; +} diff --git a/src/background/services/listeners.ts b/src/background/services/listeners.ts new file mode 100644 index 0000000..0e99daf --- /dev/null +++ b/src/background/services/listeners.ts @@ -0,0 +1,240 @@ +import { RUNTIME_MESSAGE_ACTIONS } from '@/shared/messages/runtime'; +import { SettingsRepository } from '@/shared/storage/repository'; +import type { + BackgroundState, + BackgroundRuntime, + KeepAliveController, + SettingsSnapshot, +} from '@/background/types'; +import { refreshSettingsIntoState } from '@/background/services/settings'; +import { updateApiRequestRules } from '@/background/services/rules'; +import { + createNavigationHandlers, + type NavigationHandlers, +} from '@/background/services/navigation'; + +type BrowserApi = typeof browser; + +type StorageChange = { + oldValue?: unknown; + newValue?: unknown; +}; + +type Dependencies = { + browserApi: BrowserApi; + state: BackgroundState; + runtime: BackgroundRuntime; + keepAlive: KeepAliveController; + settingsRepository: SettingsRepository; + updateDarkModeStartupScript: (enabled: boolean) => Promise; +}; + +export type ListenerController = { + initialize: () => Promise; + registerRuntimeListeners: () => void; +}; + +const parseStoredSettings = (raw: string | undefined): SettingsSnapshot => { + const parsed = raw ? (JSON.parse(raw) as Partial) : {}; + return { + resultsPerPage: + typeof parsed.resultsPerPage === 'number' || typeof parsed.resultsPerPage === 'string' + ? String(parsed.resultsPerPage) + : '30', + defaultSortMode: + parsed.defaultSortMode === 'date_new_old' + || parsed.defaultSortMode === 'date_old_new' + || parsed.defaultSortMode === 'price_low_high' + || parsed.defaultSortMode === 'price_high_low' + || parsed.defaultSortMode === 'distance' + || parsed.defaultSortMode === 'standard' + ? parsed.defaultSortMode + : 'standard', + sortPreferenceSource: parsed.sortPreferenceSource === 'marketplace' ? 'marketplace' : 'cleanplaats', + darkMode: Boolean(parsed.darkMode), + }; +}; + +type StorageOnChangedParameters = Parameters< + BrowserApi['storage']['onChanged']['addListener'] +>[0] extends (changes: infer TChanges, areaName: infer TAreaName) => unknown + ? { changes: TChanges; areaName: TAreaName } + : { changes: Record; areaName: string }; + +const updateStateFromSnapshot = ( + state: BackgroundState, + snapshot: SettingsSnapshot, +): boolean => { + let changed = false; + + if (state.resultsPerPage !== snapshot.resultsPerPage) { + state.resultsPerPage = snapshot.resultsPerPage; + changed = true; + } + + if (state.defaultSortMode !== snapshot.defaultSortMode) { + state.defaultSortMode = snapshot.defaultSortMode; + changed = true; + } + + if (state.sortPreferenceSource !== snapshot.sortPreferenceSource) { + state.sortPreferenceSource = snapshot.sortPreferenceSource; + changed = true; + } + + return changed; +}; + +export const createListenerController = (dependencies: Dependencies): ListenerController => { + const { + browserApi, + keepAlive, + runtime, + settingsRepository, + state, + updateDarkModeStartupScript, + } = dependencies; + + let navigationHandlers: NavigationHandlers | null = null; + + const ensureNavigationHandlers = (): NavigationHandlers => { + if (navigationHandlers) { + return navigationHandlers; + } + + navigationHandlers = createNavigationHandlers({ + browserApi, + state, + }); + return navigationHandlers; + }; + + const refreshSettingsAndRules = async (): Promise => { + try { + const rawSettings = await settingsRepository.getRawSettingsValue(); + const snapshot = parseStoredSettings(rawSettings); + + const changed = updateStateFromSnapshot(state, snapshot); + await updateDarkModeStartupScript(snapshot.darkMode); + + if (changed) { + await updateApiRequestRules(state.resultsPerPage); + } + } catch (error) { + console.error('Cleanplaats: Error refreshing settings in background', error); + } + }; + + const handleStorageChanges = async ( + changes: StorageOnChangedParameters['changes'], + areaName: StorageOnChangedParameters['areaName'], + ): Promise => { + if (areaName !== 'local' || !changes.cleanplaatsSettings) { + return; + } + + try { + const snapshot = parseStoredSettings( + typeof changes.cleanplaatsSettings.newValue === 'string' + ? changes.cleanplaatsSettings.newValue + : undefined, + ); + + const changed = updateStateFromSnapshot(state, snapshot); + await updateDarkModeStartupScript(snapshot.darkMode); + + if (changed) { + await updateApiRequestRules(state.resultsPerPage); + } + } catch (error) { + console.error('Cleanplaats: Error handling storage change', error); + } + }; + + const registerMessageListener = (): void => { + browserApi.runtime.onMessage.addListener( + (message: unknown, _sender: unknown, sendResponse: (response?: unknown) => void): boolean => { + const action = (message as { action?: string })?.action; + + if (action === RUNTIME_MESSAGE_ACTIONS.keepAlive) { + keepAlive.resetToActiveMode(); + void refreshSettingsAndRules(); + sendResponse({ status: 'acknowledged', timestamp: Date.now() }); + return true; + } + + if (action === RUNTIME_MESSAGE_ACTIONS.forceRefresh) { + keepAlive.resetToActiveMode(); + void refreshSettingsAndRules(); + sendResponse({ status: 'refreshed', timestamp: Date.now() }); + return true; + } + + sendResponse({ status: 'ignored' }); + return true; + }, + ); + }; + + const registerStorageListener = (): void => { + browserApi.storage.onChanged.addListener( + (changes: Record, areaName: string) => { + void handleStorageChanges(changes, areaName); + }, + ); + }; + + const registerNavigationListeners = (): void => { + const handlers = ensureNavigationHandlers(); + const filter = { url: [...runtime.wakeupNavigationFilters] }; + + browserApi.webNavigation.onBeforeNavigate.addListener( + handlers.handleBeforeNavigate, + filter, + ); + browserApi.webNavigation.onHistoryStateUpdated.addListener( + handlers.handleHistoryStateUpdated, + filter, + ); + }; + + const setupInstallListener = (): void => { + browserApi.runtime.onInstalled.addListener(async (details: { reason: string }) => { + if (details.reason !== 'install' && details.reason !== 'update') { + return; + } + + try { + const existingRules = await browserApi.declarativeNetRequest.getDynamicRules(); + if (!existingRules.length) return; + + await browserApi.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: existingRules.map((rule: { id: number }) => rule.id), + }); + } catch (error) { + console.error('Cleanplaats: Failed clearing dynamic rules on install/update', error); + } + }); + }; + + const initialize = async (): Promise => { + await refreshSettingsIntoState(state, settingsRepository); + const rawSettings = await settingsRepository.getRawSettingsValue(); + const snapshot = parseStoredSettings(rawSettings); + + await updateDarkModeStartupScript(Boolean(snapshot.darkMode)); + await updateApiRequestRules(state.resultsPerPage); + }; + + const registerRuntimeListeners = (): void => { + registerMessageListener(); + registerStorageListener(); + registerNavigationListeners(); + setupInstallListener(); + }; + + return { + initialize, + registerRuntimeListeners, + }; +}; diff --git a/src/background/services/navigation.ts b/src/background/services/navigation.ts new file mode 100644 index 0000000..dea4bd6 --- /dev/null +++ b/src/background/services/navigation.ts @@ -0,0 +1,86 @@ +import type { BackgroundState } from '@/background/types'; +import { HASH_URL_PATTERNS } from '@/shared/constants/domains'; +import { getModifiedUrlIfNeeded } from '@/background/services/hash-url'; + +type BrowserApi = typeof browser; +type BeforeNavigateDetails = Parameters< + BrowserApi['webNavigation']['onBeforeNavigate']['addListener'] +>[0] extends (details: infer Details) => unknown + ? Details + : never; +type HistoryStateUpdatedDetails = Parameters< + BrowserApi['webNavigation']['onHistoryStateUpdated']['addListener'] +>[0] extends (details: infer Details) => unknown + ? Details + : never; + +export type NavigationHandlers = { + handleBeforeNavigate: (details: BeforeNavigateDetails) => void; + handleHistoryStateUpdated: (details: HistoryStateUpdatedDetails) => void; +}; + +type Dependencies = { + browserApi: BrowserApi; + state: BackgroundState; +}; + +const isTopFrame = (details: { frameId: number; parentFrameId: number }): boolean => + details.frameId === 0 && details.parentFrameId === -1; + +const isSupportedHashNavigation = (url: string): boolean => + HASH_URL_PATTERNS.some((pattern) => url.startsWith(pattern)); + +const rewriteUrlIfNeeded = ( + browserApi: BrowserApi, + details: BeforeNavigateDetails | HistoryStateUpdatedDetails, + state: BackgroundState, +): void => { + if (!isTopFrame(details)) return; + if (!isSupportedHashNavigation(details.url)) return; + + const rewrittenUrl = getModifiedUrlIfNeeded({ + urlString: details.url, + resultsPerPage: state.resultsPerPage, + defaultSortMode: state.defaultSortMode, + sortPreferenceSource: state.sortPreferenceSource, + }); + + if (!rewrittenUrl || rewrittenUrl === details.url) { + return; + } + + void browserApi.tabs.update(details.tabId, { url: rewrittenUrl }); + + if ( + 'transitionType' in details && + typeof details.transitionType === 'undefined' + ) { + setTimeout(() => { + void browserApi.tabs + .get(details.tabId) + .then((tab: { url?: string }) => { + if (tab?.url === rewrittenUrl) { + void browserApi.tabs.reload(details.tabId); + } + }) + .catch((error: unknown) => { + console.warn('Cleanplaats: Failed checking tab before reload', error); + }); + }, 150); + } +}; + +export const createNavigationHandlers = ( + dependencies: Dependencies, +): NavigationHandlers => { + const { browserApi, state } = dependencies; + + return { + handleBeforeNavigate: (details) => { + rewriteUrlIfNeeded(browserApi, details, state); + }, + handleHistoryStateUpdated: (details) => { + rewriteUrlIfNeeded(browserApi, details, state); + }, + }; +}; diff --git a/src/background/services/rules.ts b/src/background/services/rules.ts new file mode 100644 index 0000000..730ebe2 --- /dev/null +++ b/src/background/services/rules.ts @@ -0,0 +1,97 @@ +import { API_RULE_ID, API_URL_PATTERNS } from '@/shared/constants/domains'; + +const browserApi = browser; +type DynamicRule = { + id: number; + priority: number; + action: { + type: 'redirect'; + redirect: { + transform: { + queryTransform: { + removeParams: string[]; + addOrReplaceParams: Array<{ key: string; value: string }>; + }; + }; + }; + }; + condition: { + urlFilter: string; + resourceTypes: Array<'xmlhttprequest'>; + }; +}; + +export function shouldModifyApiRules(resultsPerPage: string): boolean { + return resultsPerPage !== '30'; +} + +function buildUrlFilter(patterns: readonly string[]): string { + return patterns.map((pattern) => pattern.replace('*', '')).join('|'); +} + +export function buildDynamicRules(resultsPerPage: string): DynamicRule[] { + if (!shouldModifyApiRules(resultsPerPage)) { + return []; + } + + const rule: DynamicRule = { + id: API_RULE_ID, + priority: 1, + action: { + type: 'redirect', + redirect: { + transform: { + queryTransform: { + removeParams: [], + addOrReplaceParams: [], + }, + }, + }, + }, + condition: { + urlFilter: buildUrlFilter(API_URL_PATTERNS), + resourceTypes: ['xmlhttprequest'], + }, + }; + + rule.action.redirect.transform.queryTransform.addOrReplaceParams.push({ + key: 'limit', + value: resultsPerPage, + }); + + return [rule]; +} + +export async function updateApiRequestRules(resultsPerPage: string): Promise { + const removeRuleIds = [API_RULE_ID]; + const addRules = buildDynamicRules(resultsPerPage); + + try { + await browserApi.declarativeNetRequest.updateDynamicRules({ + removeRuleIds, + addRules, + } as never); + console.info('Cleanplaats: Dynamic API rules updated', { + resultsPerPage, + ruleCount: addRules.length, + }); + } catch (error) { + console.error('Cleanplaats: Failed to update dynamic API rules', { + resultsPerPage, + error, + }); + } +} + +export async function clearAllDynamicRules(): Promise { + const existingRules = await browserApi.declarativeNetRequest.getDynamicRules(); + const removeRuleIds = existingRules.map((rule: { id: number }) => rule.id); + + if (!removeRuleIds.length) { + return; + } + + await browserApi.declarativeNetRequest.updateDynamicRules({ + removeRuleIds, + } as never); +} diff --git a/src/background/services/settings.ts b/src/background/services/settings.ts new file mode 100644 index 0000000..792bf3f --- /dev/null +++ b/src/background/services/settings.ts @@ -0,0 +1,27 @@ +import { SettingsRepository } from '@/shared/storage/repository'; +import type { CleanplaatsSettings } from '@/shared/types/state'; +import { DEFAULT_SETTINGS } from '@/shared/constants/settings'; +import type { BackgroundState } from '@/background/types'; + +export async function loadInitialSettings( + repository: SettingsRepository, +): Promise { + try { + const { settings } = await repository.load(undefined); + return settings; + } catch (error) { + console.error('Cleanplaats: Failed to load initial background settings', error); + return { ...DEFAULT_SETTINGS }; + } +} + +export async function refreshSettingsIntoState( + state: BackgroundState, + repository: SettingsRepository, +): Promise { + const settings = await loadInitialSettings(repository); + state.resultsPerPage = String(settings.resultsPerPage); + state.defaultSortMode = settings.defaultSortMode; + state.sortPreferenceSource = settings.sortPreferenceSource; +} + diff --git a/src/background/types.ts b/src/background/types.ts new file mode 100644 index 0000000..a997152 --- /dev/null +++ b/src/background/types.ts @@ -0,0 +1,41 @@ +import type { CleanplaatsSortMode, SortPreferenceSource } from '@/shared/types/state'; +import type { WAKEUP_NAVIGATION_FILTERS } from '@/shared/constants/domains'; + +export type BackgroundState = { + resultsPerPage: string; + defaultSortMode: CleanplaatsSortMode; + sortPreferenceSource: SortPreferenceSource; +}; + +export type BackgroundRuntime = { + wakeupNavigationFilters: typeof WAKEUP_NAVIGATION_FILTERS; +}; + +export type BackgroundRuntimeState = BackgroundState & { + lastMarketplaceActivity: number; + wakeupNavigationFilters: typeof WAKEUP_NAVIGATION_FILTERS; +}; + +export type KeepAliveController = { + setup: () => void; + resetToActiveMode: () => void; +}; + +export type SettingsSnapshot = { + resultsPerPage: string; + defaultSortMode: CleanplaatsSortMode; + sortPreferenceSource: SortPreferenceSource; + darkMode: boolean; +}; + +export const createBackgroundRuntimeState = (): BackgroundRuntimeState => ({ + resultsPerPage: '30', + defaultSortMode: 'standard', + sortPreferenceSource: 'cleanplaats', + lastMarketplaceActivity: Date.now(), + wakeupNavigationFilters: [ + { hostSuffix: 'marktplaats.nl' }, + { hostSuffix: '2dehands.be' }, + { hostSuffix: '2ememain.be' }, + ] as const, +}); diff --git a/src/content/bootstrap.ts b/src/content/bootstrap.ts new file mode 100644 index 0000000..dbea83a --- /dev/null +++ b/src/content/bootstrap.ts @@ -0,0 +1,117 @@ +import { bindBlacklistRepository, injectBlacklistButtons } from '@/content/services/blacklist-inject'; +import { wakeUpBackground, setupPeriodicWakeUp } from '@/content/services/background-wake'; +import { + performCleanup, + performInitialCleanup, + removePersistentGoogleAds, + resetPreviousChanges, +} from '@/content/services/cleanup'; +import { + bindNotificationsRepository, + checkForEmptyPage, + getExtensionVersion, + showOnboarding, + scheduleSellerAgeWarningCheck, +} from '@/content/services/notifications'; +import { setupAllObservers } from '@/content/services/observers'; +import { setupMarketplaceSortSync } from '@/content/services/sort-sync'; +import { applyDarkModeToDocument, setupWebchatCollisionAvoidance } from '@/content/services/theme'; +import { + getState, + loadInitialState, + patchSettings, + registerSettingsStorageSync, + saveSettings, +} from '@/content/runtime/store'; +import { mountControlPanel } from '@/content/panel/mount'; +import { SettingsRepository } from '@/shared/storage/repository'; + +const applyDarkModeFromSync = (enabled: boolean): void => { + patchSettings({ darkMode: enabled }); + const panel = document.getElementById('cleanplaats-panel'); + applyDarkModeToDocument(enabled, panel, getState().settings); +}; + +export const initCleanplaats = async (): Promise => { + console.log('Cleanplaats: Initializing...'); + + const repository = new SettingsRepository(); + bindNotificationsRepository(repository); + bindBlacklistRepository(repository); + + await loadInitialState(repository); + + applyDarkModeToDocument(getState().settings.darkMode, null, getState().settings); + + registerSettingsStorageSync(applyDarkModeFromSync); + + wakeUpBackground(); + setupPeriodicWakeUp(); + + const currentVersion = getExtensionVersion(); + + mountControlPanel({ + repository, + onMounted: (panel) => { + applyDarkModeToDocument(getState().settings.darkMode, panel, getState().settings); + }, + }); + + const stateAfterMount = getState(); + const { observer: webchatObserver } = setupWebchatCollisionAvoidance(stateAfterMount.observers.webchat); + stateAfterMount.observers.webchat = webchatObserver; + + setupAllObservers(); + setupMarketplaceSortSync(repository); + + void saveSettings(repository) + .then(() => { + applyDarkModeToDocument( + getState().settings.darkMode, + document.getElementById('cleanplaats-panel'), + getState().settings, + ); + resetPreviousChanges(getState()); + performCleanup(getState()); + }) + .catch((error) => { + console.error('Cleanplaats: Failed to apply settings', error); + }); + + scheduleSellerAgeWarningCheck({ resetState: true }); + showOnboarding(currentVersion); + + const tryCleanup = (): void => { + if (document.querySelector('.hz-Listing') || document.querySelector('#adsense-container')) { + performInitialCleanup(getState()); + injectBlacklistButtons(); + setTimeout(checkForEmptyPage, 300); + + let attempts = 0; + const maxAttempts = 10; + const interval = window.setInterval(() => { + removePersistentGoogleAds(getState()); + + document.querySelectorAll('#banner-top-dt').forEach((banner) => { + if (banner.parentNode) { + banner.parentNode.removeChild(banner); + } + }); + + document.body.offsetHeight; + attempts++; + if ( + (!document.querySelector('#banner-right-container') + && !document.querySelector('#banner-top-dt')) + || attempts >= maxAttempts + ) { + clearInterval(interval); + } + }, 80); + } else { + setTimeout(tryCleanup, 60); + } + }; + + tryCleanup(); +}; diff --git a/src/content/constants/ui.ts b/src/content/constants/ui.ts new file mode 100644 index 0000000..98ca46d --- /dev/null +++ b/src/content/constants/ui.ts @@ -0,0 +1,5 @@ +export const CLEANPLAATS_DARK_MODE_CLASS = 'cleanplaats-dark-mode'; +export const CLEANPLAATS_TWH_SITE_CLASS = 'cleanplaats-site-twh'; +export const CLEANPLAATS_FLOATING_OFFSET_VAR = '--cleanplaats-floating-offset'; +export const CLEANPLAATS_DARK_LOGO_PATH = 'icons/marktplaats-logo-darkmode.svg'; +export const MARKTPLAATS_DESKTOP_LOGO_MATCH = /\/tenant--nlnl(?:\.[a-z0-9]+)?\.svg$/i; diff --git a/src/content/locale/panel-text.ts b/src/content/locale/panel-text.ts new file mode 100644 index 0000000..4223ea7 --- /dev/null +++ b/src/content/locale/panel-text.ts @@ -0,0 +1,189 @@ +import type { CleanplaatsLocaleText } from '@/shared/types/state'; +import { is2ememainLocale } from '@/content/utils/site'; + +export const getPanelLocaleText = (): CleanplaatsLocaleText => { + if (is2ememainLocale()) { + return { + feedbackLabel: 'Retour', + feedbackText: 'Issues GitHub', + feedbackAriaLabel: + 'Ouvrir GitHub issues pour les demandes de fonctionnalité, modifications et bugs', + reviewAriaLabel: (linkLabel) => `Laisser un avis sur Cleanplaats sur ${linkLabel}`, + supportTitle: 'Soutenir Cleanplaats', + supportButton: 'Soutenir Cleanplaats', + optionsTitle: 'Options de filtrage', + topAdLabel: 'Pub au top', + topAdTooltip: "Masque les annonces marquées 'Pub au top'", + topAdTooltipTwh: "Masque les annonces marquées 'Pub au top'", + dagtoppersLabel: 'Tops du jour', + dagtoppersTooltip: "Supprime les annonces marquées 'Top du jour'", + promotedListingsLabel: 'Annonces professionnelles', + promotedListingsTooltip: + "Masque les annonces de boutiques et d'entreprises, y compris sur la page d'accueil dans 'Pour vous' et 'Près de chez vous'", + stickersLabel: 'Autocollants promotionnels', + stickersTooltip: 'Supprime les annonces avec des autocollants promotionnels', + reservedLabel: 'Réservées', + reservedTooltip: "Masque les annonces marquées 'Réservé'", + favoriteRelatedAdsLabel: 'Annonces similaires dans les favoris', + favoriteRelatedAdsTooltip: + 'Masque la liste des annonces similaires affichée dans les favoris', + sellerAgeWarningLabel: 'Alerte compte vendeur récent', + sellerAgeWarningTooltip: + "Affiche un avertissement sur une page d'annonce si le compte vendeur est plus récent que votre seuil.", + sellerAgeWarningThresholdLabel: 'Avertir en dessous de', + sellerAgeWarningThresholdValueAriaLabel: 'Valeur seuil pour le compte vendeur récent', + sellerAgeWarningThresholdUnitAriaLabel: 'Unité seuil pour le compte vendeur récent', + sellerAgeWarningThresholdUnits: { + days: 'jours', + weeks: 'semaines', + months: 'mois', + years: 'ans', + }, + sellerAgeWarningToastTitle: 'Compte vendeur récent', + sellerAgeWarningToastMessage: (sellerName, sellerAgeText, thresholdLabel) => + `${sellerName} est sur la plateforme depuis ${sellerAgeText}. Votre seuil est ${thresholdLabel}.`, + preferencesLabel: 'Préférences', + backLabel: '← Retour', + preferencesIntro: '', + darkModeLabel: 'Mode sombre', + darkModeTooltip: + 'Active un thème sombre pour 2ememain et le panneau Cleanplaats. Expérimental: si la visibilité pose problème, désactivez-le.', + resultsPerPageLabel: 'Résultats par page :', + defaultSortLabel: 'Tri par défaut :', + sortOptions: { + standard: 'Standard', + date_new_old: 'Plus récentes', + date_old_new: 'Plus anciennes', + price_low_high: 'Prix ↑', + price_high_low: 'Prix ↓', + distance: 'Distance', + }, + statsTitle: 'Éléments supprimés', + statsTop: 'Top :', + statsDagtoppers: 'Tops du jour :', + statsBusiness: 'Professionnel :', + statsStickers: 'Autocollants :', + statsOther: 'Autres :', + statsTotal: 'Total :', + manageTerms: 'Gérer les termes masqués dans le titre', + manageSellers: 'Gérer les vendeurs masqués', + termsModalTitle: 'Termes masqués', + termsEmpty: 'Aucun terme ajouté', + hiddenButton: 'Masqué', + unhideButton: 'Afficher', + termInputPlaceholder: 'Saisissez un terme', + termInputHelp: 'Les annonces sont masquées si ce terme apparaît dans le titre.', + addButton: 'Ajouter', + closeButton: 'Fermer', + sellersModalTitle: 'Vendeurs masqués', + sellersEmpty: 'Aucun vendeur ajouté', + sellerInputPlaceholder: 'ex. Catawiki', + sellerInputHelp: + 'Vous voulez ajouter plusieurs noms à la fois ? Séparez-les avec des virgules ou des points-virgules.', + hideSellerButton: 'Masquer le vendeur', + hiddenSellerButton: 'Vendeur masqué', + hideSellerButtonAriaLabel: 'Masquer ce vendeur', + blacklistToastHint: 'Gérez les vendeurs masqués via le panneau', + blacklistToastHiddenSuffix: 'masqué', + blacklistToastHiddenPluralSuffix: 'vendeurs masqués', + blacklistToastShownSuffix: "n'est plus masqué", + blacklistToastShownHint: 'Ce vendeur est à nouveau visible dans les résultats', + termToastHidden: (term) => + `Toutes les annonces contenant le terme '${term}' sont désormais masquées.`, + termToastShown: (term) => + `Les annonces contenant le terme '${term}' sont à nouveau affichées.`, + }; + } + + const is2dehands = location.hostname.includes('2dehands.be'); + + return { + feedbackLabel: 'Feedback', + feedbackText: 'GitHub issues', + feedbackAriaLabel: 'Open GitHub issues voor functieverzoeken, wijzigingen en bugs', + reviewAriaLabel: (linkLabel) => `Laat een review achter voor Cleanplaats op ${linkLabel}`, + supportTitle: 'Steun Cleanplaats met een kleine bijdrage', + supportButton: 'Steun Cleanplaats', + optionsTitle: 'Filteropties', + topAdLabel: 'Topadvertenties', + topAdTooltip: is2dehands + ? "Verbergt 'Topadvertentie' en 'Topzoekertje' listings" + : "Verwijdert betaalde 'Topadvertentie' advertenties", + topAdTooltipTwh: "Verbergt 'Topadvertentie' en 'Topzoekertje' listings", + dagtoppersLabel: 'Dagtoppers', + dagtoppersTooltip: "Verwijdert 'Dagtopper' advertenties", + promotedListingsLabel: 'Bedrijfsadvertenties', + promotedListingsTooltip: + "Verbergt advertenties van bedrijven en winkels, zoals Catawiki, ook op de homepage bij 'Voor jou' en 'In je buurt'", + stickersLabel: 'Opvalstickers', + stickersTooltip: 'Verwijdert advertenties met opvalstickers', + reservedLabel: 'Gereserveerde', + reservedTooltip: "Verbergt advertenties die 'Gereserveerd' zijn", + favoriteRelatedAdsLabel: 'Gerelateerde advertenties bij favorieten', + favoriteRelatedAdsTooltip: + 'Verbergt het blok met gerelateerde advertenties op de favorietenpagina', + sellerAgeWarningLabel: 'Waarschuwing voor nieuwe verkoperaccounts', + sellerAgeWarningTooltip: + 'Toont op een advertentiepagina een waarschuwing als het verkopersaccount jonger is dan jouw ingestelde grens.', + sellerAgeWarningThresholdLabel: 'Waarschuwen onder', + sellerAgeWarningThresholdValueAriaLabel: 'Drempelwaarde voor waarschuwing nieuwe verkoperaccounts', + sellerAgeWarningThresholdUnitAriaLabel: 'Drempeleenheid voor waarschuwing nieuwe verkoperaccounts', + sellerAgeWarningThresholdUnits: { + days: 'dagen', + weeks: 'weken', + months: 'maanden', + years: 'jaar', + }, + sellerAgeWarningToastTitle: 'Nieuw verkoperaccount', + sellerAgeWarningToastMessage: (sellerName, sellerAgeText, thresholdLabel) => + `${sellerName} zit pas ${sellerAgeText}. Jouw grens staat op ${thresholdLabel}. Verberg verkoper via de knop onder de naam.`, + preferencesLabel: 'Voorkeuren', + backLabel: '← Terug', + preferencesIntro: '', + darkModeLabel: 'Donkere modus', + darkModeTooltip: + 'Schakelt een donker thema in voor Marktplaats en het Cleanplaats-paneel. Experimenteel: werkt meestal goed, maar zet het uit als iets slecht leesbaar is.', + resultsPerPageLabel: 'Resultaten per pagina:', + defaultSortLabel: 'Standaard sortering:', + sortOptions: { + standard: 'Standaard', + date_new_old: 'Nieuw eerst', + date_old_new: 'Oud eerst', + price_low_high: 'Prijs ↑', + price_high_low: 'Prijs ↓', + distance: 'Afstand', + }, + statsTitle: 'Verwijderde items', + statsTop: 'Top:', + statsDagtoppers: 'Dagtoppers:', + statsBusiness: 'Bedrijf:', + statsStickers: 'Stickers:', + statsOther: 'Overig:', + statsTotal: 'Totaal:', + manageTerms: 'Beheer blacklist-termen in titels', + manageSellers: 'Beheer verborgen verkopers', + termsModalTitle: 'Blacklist termen', + termsEmpty: 'Geen termen toegevoegd', + hiddenButton: 'Verborgen', + unhideButton: 'Opheffen', + termInputPlaceholder: 'Voer een term in', + termInputHelp: 'Advertenties worden verborgen als deze term in de titel voorkomt.', + addButton: 'Toevoegen', + closeButton: 'Sluiten', + sellersModalTitle: 'Verborgen verkopers', + sellersEmpty: 'Geen verkopers toegevoegd', + sellerInputPlaceholder: 'bijv. Catawiki', + sellerInputHelp: + "Wil je meerdere namen tegelijk toevoegen? Scheid ze dan met komma's of puntkomma's.", + hideSellerButton: 'Verkoper verbergen', + hiddenSellerButton: 'Verkoper verborgen', + hideSellerButtonAriaLabel: 'Verberg deze verkoper', + blacklistToastHint: 'Beheer verborgen verkopers via het paneel', + blacklistToastHiddenSuffix: 'verborgen', + blacklistToastHiddenPluralSuffix: 'verkopers verborgen', + blacklistToastShownSuffix: 'niet meer verborgen', + blacklistToastShownHint: 'Deze verkoper is weer zichtbaar in de resultaten', + termToastHidden: (term) => `Alle advertenties met de term '${term}' zijn nu verborgen.`, + termToastShown: (term) => `Advertenties met de term '${term}' worden weer getoond.`, + }; +}; diff --git a/src/content/panel/CleanplaatsPanel.tsx b/src/content/panel/CleanplaatsPanel.tsx new file mode 100644 index 0000000..a655f62 --- /dev/null +++ b/src/content/panel/CleanplaatsPanel.tsx @@ -0,0 +1,918 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, + type FormEvent, +} from 'react'; +import { getPanelLocaleText } from '@/content/locale/panel-text'; +import { getState, patchPanelState, patchSettings, saveSettings } from '@/content/runtime/store'; +import { useCleanplaatsStore } from '@/content/panel/use-cleanplaats-store'; +import { setActivePanelViewDom } from '@/content/panel/panel-view'; +import { + addSellersToBlacklist, + removeSellerFromBlacklist, +} from '@/content/services/blacklist-inject'; +import { + performCleanup, + resetPreviousChanges, +} from '@/content/services/cleanup'; +import { unhideListingsByTerm } from '@/content/services/blacklist-terms'; +import { + applyDarkModeToDocument, + updateCollapsedPanelIcon, +} from '@/content/services/theme'; +import { + checkForEmptyPage, + clearBubbleNotification, + scheduleSellerAgeWarningCheck, + showBlacklistTermToast, + showUnblacklistTermToast, + showUnblacklistToast, +} from '@/content/services/notifications'; +import { wakeUpBackground } from '@/content/services/background-wake'; +import { getReviewCTAConfig, isSearchResultsPage } from '@/content/utils/site'; +import type { CleanplaatsPanelState, CleanplaatsSettings, SortMode } from '@/shared/types/state'; +import type { SettingsRepository } from '@/shared/storage/repository'; +import { CLEANPLAATS_DARK_MODE_CLASS } from '@/content/constants/ui'; + +type Props = { + repository: SettingsRepository; + onMounted?: (panel: HTMLDivElement) => void; +}; + +const SORT_MODES: SortMode[] = [ + 'standard', + 'date_new_old', + 'date_old_new', + 'price_low_high', + 'price_high_low', + 'distance', +]; + +export function CleanplaatsPanel({ repository, onMounted }: Props) { + const { settings, panelState, featureFlags, stats } = useCleanplaatsStore(); + const panelRef = useRef(null); + const filtersRef = useRef(null); + const preferencesRef = useRef(null); + const viewsRef = useRef(null); + const tooltipRef = useRef(null); + + const [termsOpen, setTermsOpen] = useState(false); + const [sellersOpen, setSellersOpen] = useState(false); + const [termInput, setTermInput] = useState(''); + const [sellerInput, setSellerInput] = useState(''); + + const panelText = getPanelLocaleText(); + const reviewCTA = getReviewCTAConfig(); + + useLayoutEffect(() => { + if (panelRef.current) { + onMounted?.(panelRef.current); + } + }, [onMounted]); + + useEffect(() => { + const panel = panelRef.current; + const tooltip = tooltipRef.current; + if (!panel || !tooltip) return; + + const showTip = (text: string, icon: HTMLElement): void => { + tooltip.textContent = text; + tooltip.style.display = 'block'; + const rect = icon.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + let left = rect.left + rect.width / 2 - tooltipRect.width / 2; + left = Math.max(8, Math.min(left, window.innerWidth - tooltipRect.width - 8)); + let top = rect.top - tooltipRect.height - 8; + if (top < 8) { + top = rect.bottom + 8; + } + tooltip.style.left = `${left}px`; + tooltip.style.top = `${top}px`; + tooltip.style.opacity = '1'; + }; + + const hideTip = (): void => { + tooltip.style.opacity = '0'; + tooltip.style.display = 'none'; + }; + + const onEnter = (e: MouseEvent): void => { + const target = e.target as HTMLElement | null; + const icon = target?.closest?.('.cleanplaats-tooltip-icon') as HTMLElement | null; + if (!icon) return; + const text = icon.getAttribute('data-tooltip'); + if (!text) return; + showTip(text, icon); + }; + + const onLeave = (): void => { + hideTip(); + }; + + panel.addEventListener('mouseenter', onEnter, true); + panel.addEventListener('mouseleave', onLeave, true); + return () => { + panel.removeEventListener('mouseenter', onEnter, true); + panel.removeEventListener('mouseleave', onLeave, true); + }; + }, []); + + const persist = useCallback(async (): Promise => { + await saveSettings(repository); + }, [repository]); + + const setView = useCallback( + (view: CleanplaatsPanelState['activeView'], animated = true): void => { + const current = getState().panelState.activeView; + patchPanelState({ activeView: view }); + setActivePanelViewDom({ + activeView: current, + nextView: view, + filtersView: filtersRef.current, + preferencesView: preferencesRef.current, + viewsContainer: viewsRef.current, + animated, + onComplete: () => { + void persist(); + }, + }); + }, + [persist], + ); + + useEffect(() => { + const panel = panelRef.current; + if (!panel) return; + const collapsed = featureFlags.autoCollapse || panelState.isCollapsed; + panel.classList.toggle('collapsed', collapsed); + if (collapsed) { + panel.classList.add('collapsed-ready'); + updateCollapsedPanelIcon(panel, settings); + } else { + panel.classList.remove('collapsed-ready'); + updateCollapsedPanelIcon(panel, settings); + } + }, [featureFlags.autoCollapse, panelState.isCollapsed, settings]); + + const handlePanelClick = (e: React.MouseEvent): void => { + const panel = panelRef.current; + if (!panel || panel.classList.contains('animating')) return; + + const isPanelCollapsed = panel.classList.contains('collapsed'); + let canToggle = false; + + if (isPanelCollapsed) { + if (e.target === panel) { + canToggle = true; + } + } else { + const header = document.getElementById('cleanplaats-header'); + const target = e.target as Node; + if (header?.contains(target)) { + if ( + (e.target as HTMLElement).id === 'cleanplaats-toggle' + || !(e.target as HTMLElement).closest?.('input, button, a, .cleanplaats-tooltip, .cleanplaats-switch') + ) { + canToggle = true; + } + } + } + + if (!canToggle) return; + + e.preventDefault(); + e.stopPropagation(); + + setTermsOpen(false); + setSellersOpen(false); + + panel.classList.remove('collapsed-ready'); + updateCollapsedPanelIcon(panel, settings); + panel.classList.add('animating'); + + const nextCollapsed = !getState().panelState.isCollapsed; + patchPanelState({ isCollapsed: nextCollapsed }); + panel.classList.toggle('collapsed', nextCollapsed); + + const toggle = document.getElementById('cleanplaats-toggle'); + if (toggle) { + toggle.textContent = nextCollapsed ? '▲' : '▼'; + } + + const fallbackTimeout = window.setTimeout(() => { + panel.classList.remove('animating'); + if (nextCollapsed) { + panel.classList.add('collapsed-ready'); + updateCollapsedPanelIcon(panel, getState().settings); + } + }, 600); + + const onTransitionEnd = (event: TransitionEvent): void => { + if (nextCollapsed && event.propertyName === 'width') { + panel.classList.add('collapsed-ready'); + updateCollapsedPanelIcon(panel, getState().settings); + panel.classList.remove('animating'); + panel.removeEventListener('transitionend', onTransitionEnd); + clearTimeout(fallbackTimeout); + } else if (!nextCollapsed && event.propertyName === 'max-height') { + panel.classList.remove('animating'); + updateCollapsedPanelIcon(panel, getState().settings); + panel.removeEventListener('transitionend', onTransitionEnd); + clearTimeout(fallbackTimeout); + } + }; + panel.addEventListener('transitionend', onTransitionEnd); + + void persist(); + }; + + const handleThemeToggle = (): void => { + const next = !settings.darkMode; + patchSettings({ darkMode: next }); + applyDarkModeToDocument(next, panelRef.current, getState().settings); + void persist().catch((error) => { + console.error('Cleanplaats: Failed to apply dark mode', error); + patchSettings({ darkMode: !next }); + applyDarkModeToDocument(!next, panelRef.current, getState().settings); + }); + }; + + const applyFilterSetting = async (key: keyof CleanplaatsSettings, value: boolean): Promise => { + patchSettings({ [key]: value } as Partial); + if (key === 'sellerAgeWarningEnabled') { + getState().runtime.lastSellerAgeWarningKey = ''; + } + try { + await persist(); + if (key === 'sellerAgeWarningEnabled') { + scheduleSellerAgeWarningCheck({ force: true }); + return; + } + resetPreviousChanges(getState()); + performCleanup(getState()); + clearBubbleNotification(); + checkForEmptyPage(); + } catch (error) { + console.error('Cleanplaats: Failed to apply setting', error); + } + }; + + const handleCheckbox = + (key: keyof CleanplaatsSettings) => (e: React.ChangeEvent) => { + void applyFilterSetting(key, e.target.checked); + }; + + const handleResultsChange = (e: React.ChangeEvent): void => { + const value = Number.parseInt(e.target.value, 10) as CleanplaatsSettings['resultsPerPage']; + patchSettings({ resultsPerPage: value }); + wakeUpBackground(); + void persist().then(() => { + if (isSearchResultsPage()) { + setTimeout(() => window.location.reload(), 1000); + } + }); + }; + + const handleSortChange = (e: React.ChangeEvent): void => { + const value = e.target.value as SortMode; + patchSettings({ defaultSortMode: value, sortPreferenceSource: 'cleanplaats' }); + wakeUpBackground(); + void persist().then(() => { + if (isSearchResultsPage()) { + setTimeout(() => window.location.reload(), 1000); + } + }); + }; + + const handleThresholdChange = (): void => { + const valueInput = document.getElementById( + 'cleanplaats-seller-age-threshold-value', + ) as HTMLInputElement | null; + const unitSelect = document.getElementById( + 'cleanplaats-seller-age-threshold-unit', + ) as HTMLSelectElement | null; + if (!valueInput || !unitSelect) return; + + const nextValue = Math.min(99, Math.max(1, Number.parseInt(valueInput.value, 10) || 1)); + valueInput.value = String(nextValue); + patchSettings({ + sellerAgeWarningThresholdValue: nextValue, + sellerAgeWarningThresholdUnit: unitSelect.value as CleanplaatsSettings['sellerAgeWarningThresholdUnit'], + }); + getState().runtime.lastSellerAgeWarningKey = ''; + void persist().then(() => { + scheduleSellerAgeWarningCheck({ force: true }); + }); + }; + + const handleThresholdInput = (e: React.FormEvent): void => { + const raw = String(e.currentTarget.value || '').replace(/\D/g, ''); + if (!raw) return; + const nextValue = Math.min(99, Math.max(1, Number.parseInt(raw, 10) || 1)); + e.currentTarget.value = String(nextValue); + patchSettings({ sellerAgeWarningThresholdValue: nextValue }); + getState().runtime.lastSellerAgeWarningKey = ''; + void persist(); + }; + + const addTerm = (e?: FormEvent): void => { + e?.preventDefault(); + const term = termInput.trim(); + if (!term || settings.blacklistedTerms.includes(term)) return; + patchSettings({ blacklistedTerms: [...settings.blacklistedTerms, term] }); + setTermInput(''); + void persist().then(() => { + performCleanup(getState()); + showBlacklistTermToast(term); + }); + }; + + const removeTerm = (term: string): void => { + patchSettings({ + blacklistedTerms: settings.blacklistedTerms.filter((t) => t !== term), + }); + void persist().then(() => { + unhideListingsByTerm(term); + performCleanup(getState()); + showUnblacklistTermToast(term); + }); + }; + + const addSellersFromInput = (): void => { + const names = sellerInput + .split(/[;,]+/) + .map((n) => n.trim()) + .filter(Boolean); + if (names.length === 0) return; + setSellerInput(''); + void addSellersToBlacklist(names); + }; + + const removeSeller = (sellerName: string): void => { + showUnblacklistToast(sellerName); + void removeSellerFromBlacklist(sellerName); + }; + + const logoUrl = browser.runtime.getURL('icons/icon128.png'); + + return ( + <> +
    +
    +
    +

    + + Cleanplaats +

    +
    + + +
    +
    + +
    + +
    +
    +
    + { + e.stopPropagation(); + }} + > + + {panelText.supportButton} + +
    +
    {panelText.optionsTitle}
    + + {( + [ + ['removeTopAds', panelText.topAdLabel, panelText.topAdTooltip], + ['removeDagtoppers', panelText.dagtoppersLabel, panelText.dagtoppersTooltip], + ['removePromotedListings', panelText.promotedListingsLabel, panelText.promotedListingsTooltip], + ['removeOpvalStickers', panelText.stickersLabel, panelText.stickersTooltip], + ['removeReservedListings', panelText.reservedLabel, panelText.reservedTooltip], + ] as const + ).map(([id, label, tip]) => ( +
    + + +
    + ))} + + + +
    + + +
    + +
    + + +
    +
    + + {featureFlags.showStats ? ( +
    +
    {panelText.statsTitle}
    +
    + {panelText.statsTop} + + {stats.topAdsRemoved} + +
    +
    + {panelText.statsDagtoppers} + + {stats.dagtoppersRemoved} + +
    +
    + {panelText.statsBusiness} + + {stats.promotedListingsRemoved} + +
    +
    + {panelText.statsStickers} + + {stats.opvalStickersRemoved} + +
    +
    + {panelText.statsOther} + + {stats.otherAdsRemoved} + +
    +
    + {panelText.statsTotal} + + {stats.totalRemoved} + +
    +
    + ) : null} + + + +
    + +
    +
    +
    + +
    {panelText.preferencesLabel}
    +
    +
    +
    +
    + + +
    + +
    +
    + + +
    +
    + + e.stopPropagation()} + /> + +
    +
    +
    +
    +
    + +
    e.stopPropagation()} + > + {sellersOpen ? ( +
    +

    {panelText.sellersModalTitle}

    +
      + {settings.blacklistedSellers.length === 0 ? ( +
    • + {panelText.sellersEmpty} +
    • + ) : ( + settings.blacklistedSellers.map((seller) => ( +
    • + {seller} + +
    • + )) + )} +
    +
    + setSellerInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + addSellersFromInput(); + } + }} + /> + +
    +
    {panelText.sellerInputHelp}
    + +
    + ) : null} +
    + +
    e.stopPropagation()} + > + {termsOpen ? ( +
    +

    {panelText.termsModalTitle}

    +
      + {settings.blacklistedTerms.length === 0 ? ( +
    • + {panelText.termsEmpty} +
    • + ) : ( + settings.blacklistedTerms.map((term) => ( +
    • + {term} + +
    • + )) + )} +
    +
    + setTermInput(e.target.value)} + /> + +
    +
    {panelText.termInputHelp}
    + +
    + ) : null} +
    +
    +
    + +
    + + ); +} diff --git a/src/content/panel/mount.tsx b/src/content/panel/mount.tsx new file mode 100644 index 0000000..b8d7135 --- /dev/null +++ b/src/content/panel/mount.tsx @@ -0,0 +1,36 @@ +import { createRoot, type Root } from 'react-dom/client'; +import { createPortal } from 'react-dom'; +import { CleanplaatsPanel } from '@/content/panel/CleanplaatsPanel'; +import type { SettingsRepository } from '@/shared/storage/repository'; + +import '@/styles/dark-mode.css'; + +export type MountOptions = { + repository: SettingsRepository; + onMounted?: (panel: HTMLDivElement) => void; +}; + +let root: Root | null = null; + +export const mountControlPanel = (options: MountOptions): void => { + if (document.getElementById('cleanplaats-panel')) { + return; + } + + const container = document.createElement('div'); + container.id = 'cleanplaats-panel-root'; + document.body.appendChild(container); + + root = createRoot(container); + const panelProps = + options.onMounted === undefined + ? { repository: options.repository } + : { repository: options.repository, onMounted: options.onMounted }; + root.render(createPortal(, document.body)); +}; + +export const unmountControlPanel = (): void => { + root?.unmount(); + root = null; + document.getElementById('cleanplaats-panel-root')?.remove(); +}; diff --git a/src/content/panel/panel-view.ts b/src/content/panel/panel-view.ts new file mode 100644 index 0000000..a024430 --- /dev/null +++ b/src/content/panel/panel-view.ts @@ -0,0 +1,155 @@ +import type { CleanplaatsPanelState } from '@/shared/types/state'; + +type PanelView = CleanplaatsPanelState['activeView']; + +const clearPanelViewAnimationState = (viewElement: HTMLElement | null): void => { + if (!viewElement) { + return; + } + viewElement.classList.remove( + 'active', + 'is-entering', + 'is-leaving', + 'is-entering-down', + 'is-entering-up', + 'is-leaving-down', + 'is-leaving-up', + ); +}; + +const getPanelViewDirection = (fromView: PanelView, toView: PanelView): 'up' | 'down' | 'none' => { + if (fromView === toView) { + return 'none'; + } + return toView === 'preferences' ? 'down' : 'up'; +}; + +const syncPanelViewContainerHeight = (viewsContainer: HTMLElement | null, activeView: HTMLElement | null): void => { + if (!viewsContainer || !activeView) { + return; + } + viewsContainer.style.height = `${activeView.scrollHeight}px`; +}; + +const measurePanelViewHeight = ( + viewElement: HTMLElement, + viewsContainer: HTMLElement, +): number => { + const clone = viewElement.cloneNode(true) as HTMLElement; + const measurementWrapper = document.createElement('div'); + clone.removeAttribute('id'); + clone.querySelectorAll('[id]').forEach((element) => { + element.removeAttribute('id'); + }); + + clearPanelViewAnimationState(clone); + clone.classList.add('active'); + clone.setAttribute('aria-hidden', 'true'); + clone.style.position = 'relative'; + clone.style.visibility = 'hidden'; + clone.style.pointerEvents = 'none'; + clone.style.opacity = '0'; + clone.style.transform = 'translateY(0)'; + + measurementWrapper.setAttribute('aria-hidden', 'true'); + measurementWrapper.style.position = 'absolute'; + measurementWrapper.style.top = '0'; + measurementWrapper.style.right = '0'; + measurementWrapper.style.left = '0'; + measurementWrapper.style.visibility = 'hidden'; + measurementWrapper.style.pointerEvents = 'none'; + measurementWrapper.style.opacity = '0'; + measurementWrapper.style.overflow = 'visible'; + + measurementWrapper.appendChild(clone); + viewsContainer.appendChild(measurementWrapper); + const height = clone.getBoundingClientRect().height; + measurementWrapper.remove(); + + return height; +}; + +export const setActivePanelViewDom = (options: { + activeView: PanelView; + nextView: PanelView; + filtersView: HTMLElement | null; + preferencesView: HTMLElement | null; + viewsContainer: HTMLElement | null; + animated: boolean; + onComplete: (nextView: PanelView) => void; +}): void => { + const { + activeView: currentView, + nextView, + filtersView, + preferencesView, + viewsContainer, + animated, + onComplete, + } = options; + + if (!filtersView || !preferencesView || !viewsContainer) { + return; + } + + const currentElement = currentView === 'preferences' ? preferencesView : filtersView; + const nextElement = nextView === 'preferences' ? preferencesView : filtersView; + + if (currentView === nextView) { + clearPanelViewAnimationState(filtersView); + clearPanelViewAnimationState(preferencesView); + nextElement.classList.add('active'); + syncPanelViewContainerHeight(viewsContainer, nextElement); + onComplete(nextView); + return; + } + + if (!animated) { + clearPanelViewAnimationState(filtersView); + clearPanelViewAnimationState(preferencesView); + nextElement.classList.add('active'); + syncPanelViewContainerHeight(viewsContainer, nextElement); + onComplete(nextView); + return; + } + + const direction = getPanelViewDirection(currentView, nextView); + const fromHeight = currentElement.scrollHeight; + const nextHeight = measurePanelViewHeight(nextElement, viewsContainer); + + clearPanelViewAnimationState(filtersView); + clearPanelViewAnimationState(preferencesView); + + currentElement.classList.add( + 'active', + 'is-leaving', + direction === 'down' ? 'is-leaving-up' : 'is-leaving-down', + ); + nextElement.classList.add( + 'active', + 'is-entering', + direction === 'down' ? 'is-entering-down' : 'is-entering-up', + ); + + viewsContainer.style.height = `${fromHeight}px`; + void viewsContainer.offsetHeight; + + requestAnimationFrame(() => { + viewsContainer.style.height = `${nextHeight}px`; + currentElement.classList.remove(direction === 'down' ? 'is-leaving-up' : 'is-leaving-down'); + nextElement.classList.remove(direction === 'down' ? 'is-entering-down' : 'is-entering-up'); + }); + + window.clearTimeout( + (viewsContainer as HTMLElement & { _cleanplaatsViewAnimationTimer?: number }) + ._cleanplaatsViewAnimationTimer, + ); + (viewsContainer as HTMLElement & { _cleanplaatsViewAnimationTimer?: number })._cleanplaatsViewAnimationTimer = + window.setTimeout(() => { + clearPanelViewAnimationState(currentElement); + clearPanelViewAnimationState(nextElement); + nextElement.classList.add('active'); + syncPanelViewContainerHeight(viewsContainer, nextElement); + onComplete(nextView); + }, 340); +}; diff --git a/src/content/panel/use-cleanplaats-store.ts b/src/content/panel/use-cleanplaats-store.ts new file mode 100644 index 0000000..dc8dc1b --- /dev/null +++ b/src/content/panel/use-cleanplaats-store.ts @@ -0,0 +1,12 @@ +import { useSyncExternalStore } from 'react'; +import { getStoreSnapshot, subscribe, type StoreListener } from '@/content/runtime/store'; +import type { CleanplaatsState } from '@/shared/types/state'; + +export const useCleanplaatsStore = (): CleanplaatsState => { + const snapshot = useSyncExternalStore( + subscribe as (onStoreChange: StoreListener) => () => void, + getStoreSnapshot, + getStoreSnapshot, + ); + return snapshot.state; +}; diff --git a/src/content/runtime/store.ts b/src/content/runtime/store.ts new file mode 100644 index 0000000..e4d5cfc --- /dev/null +++ b/src/content/runtime/store.ts @@ -0,0 +1,191 @@ +import { LOCAL_STORAGE_KEYS, STORAGE_KEYS } from '@/shared/constants/storage'; +import { DEFAULT_PANEL_STATE, DEFAULT_SETTINGS } from '@/shared/constants/settings'; +import { SettingsRepository } from '@/shared/storage/repository'; +import type { + CleanplaatsPanelState, + CleanplaatsSettings, + CleanplaatsState, +} from '@/shared/types/state'; + +export type StoreListener = () => void; + +const createInitialRuntimeState = (): CleanplaatsState => ({ + settings: { ...DEFAULT_SETTINGS }, + stats: { + topAdsRemoved: 0, + dagtoppersRemoved: 0, + promotedListingsRemoved: 0, + opvalStickersRemoved: 0, + otherAdsRemoved: 0, + totalRemoved: 0, + }, + observers: { + mutation: null, + ads: null, + webchat: null, + sellerAge: null, + }, + runtime: { + lastSellerAgeWarningKey: '', + sellerAgeCheckTimer: 0, + }, + featureFlags: { + showStats: true, + autoCollapse: false, + firstRun: true, + }, + panelState: { ...DEFAULT_PANEL_STATE }, +}); + +let state: CleanplaatsState = createInitialRuntimeState(); +let storeVersion = 0; +const listeners = new Set(); + +export const getStoreVersion = (): number => storeVersion; + +let cachedStoreSnapshot: { version: number; state: CleanplaatsState } | null = null; + +/** Snapshot for React `useSyncExternalStore` (stable reference until version bumps). */ +export const getStoreSnapshot = (): { version: number; state: CleanplaatsState } => { + if (!cachedStoreSnapshot || cachedStoreSnapshot.version !== storeVersion) { + cachedStoreSnapshot = { version: storeVersion, state }; + } + return cachedStoreSnapshot; +}; + +export const getState = (): CleanplaatsState => state; + +export const subscribe = (listener: StoreListener): (() => void) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +}; + +export const emit = (): void => { + storeVersion += 1; + listeners.forEach((listener) => { + listener(); + }); +}; + +export const resetStats = (): void => { + const stats = state.stats; + stats.topAdsRemoved = 0; + stats.dagtoppersRemoved = 0; + stats.promotedListingsRemoved = 0; + stats.opvalStickersRemoved = 0; + stats.otherAdsRemoved = 0; + stats.totalRemoved = 0; + emit(); +}; + +export const updateTotalRemoved = (): void => { + const s = state.stats; + s.totalRemoved = + s.topAdsRemoved + + s.dagtoppersRemoved + + s.promotedListingsRemoved + + s.opvalStickersRemoved + + s.otherAdsRemoved; +}; + +export const patchSettings = (partial: Partial): void => { + state.settings = { ...state.settings, ...partial }; + emit(); +}; + +export const patchPanelState = (partial: Partial): void => { + state.panelState = { ...state.panelState, ...partial }; + emit(); +}; + +export const setFirstRunFlag = (firstRun: boolean): void => { + state.featureFlags.firstRun = firstRun; + emit(); +}; + +let storageSyncRegistered = false; + +export const registerSettingsStorageSync = ( + onDarkModeFromSync: (enabled: boolean) => void, +): void => { + if (storageSyncRegistered || !browser.storage.onChanged.addListener) { + return; + } + + browser.storage.onChanged.addListener( + (changes: Record, areaName: string) => { + if (areaName !== 'local' || !changes[STORAGE_KEYS.settings]?.newValue) { + return; + } + + try { + const newValue = changes[STORAGE_KEYS.settings]?.newValue; + const nextSettings = + typeof newValue === 'string' ? (JSON.parse(newValue) as { darkMode?: boolean }) : {}; + const darkModeEnabled = Boolean(nextSettings?.darkMode); + + if (state.settings.darkMode !== darkModeEnabled) { + patchSettings({ darkMode: darkModeEnabled }); + onDarkModeFromSync(darkModeEnabled); + } else { + persistDarkModeFromStore(); + } + } catch (error) { + console.error('Cleanplaats: Failed to sync dark mode from storage', error); + } + }, + ); + + storageSyncRegistered = true; +}; + +const persistDarkModeFromStore = (): void => { + try { + window.localStorage.setItem( + LOCAL_STORAGE_KEYS.darkMode, + state.settings.darkMode ? 'true' : 'false', + ); + } catch { + /* ignore */ + } +}; + +export const notifyStatsChanged = (): void => { + updateTotalRemoved(); + emit(); +}; + +export const saveSettings = async (repository: SettingsRepository): Promise => { + await repository.saveSettings(state.settings, state.panelState); + emit(); +}; + +export const loadInitialState = async (repository: SettingsRepository): Promise => { + const raw = (await browser.storage.local.get([ + STORAGE_KEYS.settings, + STORAGE_KEYS.panelState, + STORAGE_KEYS.firstRun, + ])) as Record; + + const loaded = await repository.load(); + state.settings = loaded.settings; + state.panelState = loaded.panelState; + + const firstRunKeyPresent = Object.prototype.hasOwnProperty.call(raw, STORAGE_KEYS.firstRun); + if (!firstRunKeyPresent) { + await repository.markFirstRunCompleted(); + state.featureFlags.firstRun = true; + } else { + state.featureFlags.firstRun = loaded.firstRun; + } + + emit(); +}; + +export const markFirstRunCompleted = async (repository: SettingsRepository): Promise => { + await repository.markFirstRunCompleted(); + state.featureFlags.firstRun = false; + emit(); +}; diff --git a/src/content/services/background-wake.ts b/src/content/services/background-wake.ts new file mode 100644 index 0000000..0100cc7 --- /dev/null +++ b/src/content/services/background-wake.ts @@ -0,0 +1,59 @@ +import { isSearchResultsPage } from '@/content/utils/site'; + +declare global { + interface Window { + cleanplaatsWakeUpTimeout?: number; + } +} + +export const wakeUpBackground = (): void => { + try { + browser.runtime.sendMessage({ action: 'keepAlive' }, (response: unknown) => { + if (browser.runtime.lastError) { + console.log( + 'Cleanplaats: Background script not responding, this is normal if it was sleeping', + ); + setTimeout(() => { + try { + browser.runtime.sendMessage({ action: 'forceRefresh' }, () => { + if (!browser.runtime.lastError) { + console.log('Cleanplaats: Background script force-refreshed successfully'); + } + }); + } catch (e) { + console.log('Cleanplaats: Force refresh also failed:', e); + } + }, 100); + } else { + console.log('Cleanplaats: Background script is awake', response); + } + }); + } catch (error) { + console.log('Cleanplaats: Could not wake background script:', error); + } +}; + +export const setupPeriodicWakeUp = (): void => { + if (typeof browser === 'undefined') return; + + console.log('Cleanplaats: Setting up periodic background wake-up for Firefox'); + + setInterval(() => { + if (isSearchResultsPage()) { + wakeUpBackground(); + } + }, 30000); + + ['click', 'scroll', 'keydown'].forEach((eventType) => { + document.addEventListener( + eventType, + () => { + if (isSearchResultsPage()) { + clearTimeout(window.cleanplaatsWakeUpTimeout); + window.cleanplaatsWakeUpTimeout = window.setTimeout(wakeUpBackground, 1000); + } + }, + { passive: true }, + ); + }); +}; diff --git a/src/content/services/blacklist-inject.ts b/src/content/services/blacklist-inject.ts new file mode 100644 index 0000000..f28cac0 --- /dev/null +++ b/src/content/services/blacklist-inject.ts @@ -0,0 +1,248 @@ +import { getPanelLocaleText } from '@/content/locale/panel-text'; +import { getState, patchSettings, saveSettings } from '@/content/runtime/store'; +import { performCleanup } from '@/content/services/cleanup'; +import { + showBlacklistToast, + showBulkBlacklistToast, +} from '@/content/services/notifications'; +import type { SettingsRepository } from '@/shared/storage/repository'; +import { isProductDetailPage } from '@/content/utils/site'; + +let repositoryRef!: SettingsRepository; + +export const bindBlacklistRepository = (repository: SettingsRepository): void => { + repositoryRef = repository; +}; + +export const addSellersToBlacklist = async (sellerNames: string[]): Promise => { + const { settings } = getState(); + const normalizedSellerNames = sellerNames + .map((name) => name.trim()) + .filter(Boolean) + .filter((name, index, arr) => arr.indexOf(name) === index) + .filter((name) => !settings.blacklistedSellers.includes(name)); + + if (normalizedSellerNames.length === 0) return; + + patchSettings({ + blacklistedSellers: [...settings.blacklistedSellers, ...normalizedSellerNames], + }); + await saveSettings(repositoryRef); + performCleanup(getState()); + injectBlacklistButtons(); + + if (normalizedSellerNames.length === 1) { + showBlacklistToast(normalizedSellerNames[0] ?? ''); + return; + } + + showBulkBlacklistToast(normalizedSellerNames.length); +}; + +export const injectProductDetailBlacklistButton = (): void => { + const panelText = getPanelLocaleText(); + const sellerRoot = document.querySelector('.SellerInfoSmall-root'); + const sellerNameElement = sellerRoot?.querySelector( + '.SellerInfoSmall-name a, .SellerInfoSmall-name', + ); + const existingRow = document.querySelector('.cleanplaats-detail-blacklist-row'); + + if (!isProductDetailPage() || !sellerRoot || !sellerNameElement) { + existingRow?.remove(); + return; + } + + const sellerName = sellerNameElement.textContent?.trim(); + if (!sellerName) { + existingRow?.remove(); + return; + } + + const { settings } = getState(); + const isBlacklisted = settings.blacklistedSellers.includes(sellerName); + const detailRow = existingRow ?? document.createElement('div'); + detailRow.className = 'cleanplaats-detail-blacklist-row'; + + const button = document.createElement('button'); + button.className = 'cleanplaats-blacklist-btn cleanplaats-detail-blacklist-btn'; + button.type = 'button'; + button.tabIndex = 0; + button.textContent = isBlacklisted ? panelText.hiddenSellerButton : panelText.hideSellerButton; + button.disabled = isBlacklisted; + button.setAttribute('aria-disabled', isBlacklisted ? 'true' : 'false'); + + if (!isBlacklisted) { + button.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + void addSellersToBlacklist([sellerName]); + }); + } + + detailRow.replaceChildren(button); + + if (!existingRow) { + sellerRoot.insertAdjacentElement('afterend', detailRow); + } +}; + +export const injectBlacklistButtons = (): void => { + const panelText = getPanelLocaleText(); + const { settings } = getState(); + + document.querySelectorAll('.hz-Listing').forEach((listingEl) => { + const listing = listingEl as HTMLElement; + const oldBtn = listing.querySelector('.cleanplaats-blacklist-btn-row'); + const oldTopRight = listing.querySelector('.cleanplaats-seller-topright-mobile'); + const oldInlineBtn = listing.querySelector('.cleanplaats-inline-btn'); + + let sellerName: string | null = listing.dataset.cleanplaatsSellerName || null; + let sellerElement: Element | null = null; + let isCarAdvert = false; + + const carSellerElement = listing.querySelector( + '.hz-Listing-sellerName, .hz-Listing-sellerName-new', + ); + if (carSellerElement) { + sellerName = carSellerElement.textContent?.trim() ?? null; + sellerElement = carSellerElement; + isCarAdvert = true; + } else { + const sellerNameEl = listing.querySelector( + '.hz-Listing-seller-name, .hz-Listing-seller-name-new', + ); + if (sellerNameEl) { + sellerName = sellerNameEl.textContent?.trim() ?? null; + const sellerLink = sellerNameEl.closest('a'); + sellerElement = sellerLink ? sellerLink.parentElement || sellerLink : sellerNameEl; + isCarAdvert = false; + } + } + + if (sellerName) { + listing.dataset.cleanplaatsSellerName = sellerName; + } + + if (!sellerName) return; + + if (settings.blacklistedSellers.includes(sellerName)) { + listing.setAttribute('data-cleanplaats-hidden', 'true'); + listing.style.display = 'none'; + return; + } + + if (window.innerWidth < 700) { + if (oldTopRight && (oldTopRight as HTMLElement).dataset.cleanplaatsSellerName === sellerName) { + return; + } + + if (oldBtn) oldBtn.remove(); + if (oldInlineBtn) oldInlineBtn.remove(); + if (oldTopRight) oldTopRight.remove(); + + const topRow = document.createElement('div'); + topRow.className = 'cleanplaats-seller-topright-mobile'; + topRow.dataset.cleanplaatsSellerName = sellerName; + topRow.innerHTML = ` + ${sellerName} + + `; + const content = listing.querySelector( + '.hz-Listing-listview-content, .hz-Listing-listview-content-new', + ); + if (content?.firstChild) { + content.insertBefore(topRow, content.firstChild); + } else if (content) { + content.appendChild(topRow); + } + topRow.querySelector('.cleanplaats-blacklist-btn-mobile')?.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (window.confirm(`Wil je alle advertenties van ${sellerName} verbergen?`)) { + void addSellersToBlacklist([sellerName]); + } + }); + return; + } + + if (!sellerElement) return; + + if (oldBtn) oldBtn.remove(); + if (oldTopRight) oldTopRight.remove(); + if (oldInlineBtn) oldInlineBtn.remove(); + + if (isCarAdvert && carSellerElement) { + const carEl = carSellerElement as HTMLElement; + carEl.style.display = 'inline-flex'; + carEl.style.alignItems = 'center'; + carEl.style.gap = '8px'; + + const btn = document.createElement('button'); + btn.className = 'cleanplaats-blacklist-btn cleanplaats-inline-btn'; + btn.textContent = panelText.hideSellerButton; + btn.type = 'button'; + btn.tabIndex = 0; + (btn as HTMLElement).style.marginLeft = '8px'; + + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + void addSellersToBlacklist([sellerName]); + }); + + carSellerElement.appendChild(btn); + } else { + const btnRow = document.createElement('div'); + btnRow.className = 'cleanplaats-blacklist-btn-row'; + + const btn = document.createElement('button'); + btn.className = 'cleanplaats-blacklist-btn'; + btn.textContent = panelText.hideSellerButton; + btn.type = 'button'; + btn.tabIndex = 0; + + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + void addSellersToBlacklist([sellerName]); + }); + + btnRow.appendChild(btn); + + if (sellerElement.parentNode) { + sellerElement.parentNode.insertBefore(btnRow, sellerElement.nextSibling); + } + } + }); + + injectProductDetailBlacklistButton(); +}; + +export const removeSellerFromBlacklist = async (sellerName: string): Promise => { + const { settings } = getState(); + patchSettings({ + blacklistedSellers: settings.blacklistedSellers.filter((s) => s !== sellerName), + }); + await saveSettings(repositoryRef); + + document.querySelectorAll('.hz-Listing').forEach((listingEl) => { + const listing = listingEl as HTMLElement; + const sellerNameEl = listing.querySelector( + '.hz-Listing-seller-name, .hz-Listing-seller-name-new, .hz-Listing-seller-link, .hz-Listing-sellerName, .hz-Listing-sellerName-new', + ); + if (!sellerNameEl) return; + if (sellerNameEl.textContent?.trim() === sellerName) { + listing.removeAttribute('data-cleanplaats-hidden'); + listing.style.display = ''; + } + }); + performCleanup(getState()); + injectBlacklistButtons(); +}; diff --git a/src/content/services/blacklist-terms.ts b/src/content/services/blacklist-terms.ts new file mode 100644 index 0000000..66c18f6 --- /dev/null +++ b/src/content/services/blacklist-terms.ts @@ -0,0 +1,19 @@ +import { getListingTitleText } from '@/shared/utils/selectors'; + +export const unhideListingsByTerm = (term: string): void => { + document.querySelectorAll('.hz-Link').forEach((link) => { + const title = getListingTitleText(link); + if (title.includes(term.toLowerCase())) { + const listingEl = link.closest('.hz-StructuredListing') || link; + listingEl.removeAttribute('data-cleanplaats-hidden'); + (listingEl as HTMLElement).style.display = ''; + } + }); + document.querySelectorAll('.hz-Listing').forEach((listing) => { + const title = getListingTitleText(listing); + if (title.includes(term.toLowerCase())) { + listing.removeAttribute('data-cleanplaats-hidden'); + (listing as HTMLElement).style.display = ''; + } + }); +}; diff --git a/src/content/services/cleanup.ts b/src/content/services/cleanup.ts new file mode 100644 index 0000000..d949233 --- /dev/null +++ b/src/content/services/cleanup.ts @@ -0,0 +1,505 @@ +import { notifyStatsChanged } from '@/content/runtime/store'; +import { getListingTitleText } from '@/shared/utils/selectors'; +import type { CleanplaatsState } from '@/shared/types/state'; + +export const hideElement = (element: Element): boolean => { + if (!element || element.hasAttribute('data-cleanplaats-hidden')) { + return false; + } + + try { + element.setAttribute('data-original-style', (element as HTMLElement).style.cssText); + element.setAttribute('data-cleanplaats-hidden', 'true'); + (element as HTMLElement).style.display = 'none !important'; + + return true; + } catch (error) { + console.error('Cleanplaats: Error hiding element', error); + return false; + } +}; + +const findAndHideListings = (selector: string, textContent: string | string[]): number => { + let count = 0; + const expectedTexts = Array.isArray(textContent) + ? textContent.map((text) => text.trim().toLowerCase()) + : [textContent.trim().toLowerCase()]; + + try { + document.querySelectorAll(selector).forEach((el) => { + const elementText = el.textContent?.trim().toLowerCase(); + if (elementText && expectedTexts.includes(elementText)) { + const listing = el.closest('.hz-Listing'); + if (listing && !listing.hasAttribute('data-cleanplaats-hidden') && hideElement(listing)) { + count++; + } + } + }); + } catch (error) { + console.error(`Cleanplaats: Error finding "${String(textContent)}" listings`, error); + } + + return count; +}; + +const isHomepagePartnerListing = (listing: Element): boolean => { + const hrefs = Array.from(listing.querySelectorAll('a[href]')) + .map((link) => (link as HTMLAnchorElement).href || link.getAttribute('href') || '') + .filter(Boolean); + + return hrefs.some((href) => /\/a\d+(?:[-/?]|$)/i.test(href)); +}; + +const removeTopAdvertisements = (state: CleanplaatsState): void => { + const is2dehands = location.hostname.includes('2dehands.be'); + const is2ememain = location.hostname.includes('2ememain.be'); + const labels = is2ememain + ? ['Pub au top'] + : is2dehands + ? ['Topzoekertje', 'Topadvertentie'] + : ['Topadvertentie']; + const priorityBadgeSelector = [ + '.hz-Listing-priority span', + '.hz-Listing-priority-new', + '[class*="hz-Listing-priority-new"]', + ].join(', '); + const removedCount = labels.reduce( + (total, label) => total + findAndHideListings(priorityBadgeSelector, label), + 0, + ); + state.stats.topAdsRemoved += removedCount; +}; + +const removeDagtoppers = (state: CleanplaatsState): void => { + const priorityBadgeSelector = [ + '.hz-Listing-priority span', + '.hz-Listing-priority-new', + '[class*="hz-Listing-priority-new"]', + ].join(', '); + const removedCount = findAndHideListings(priorityBadgeSelector, 'Dagtopper'); + state.stats.dagtoppersRemoved += removedCount; +}; + +const removePromotedListings = (state: CleanplaatsState): void => { + let count = 0; + const visitWebsiteLabels = location.hostname.includes('2ememain.be') + ? ['Visiter le site internet'] + : ['Bezoek website']; + + const selectors = ['.hz-Listing-seller-link', '.hz-Listing-seller-external-link']; + + selectors.forEach((selector) => { + document.querySelectorAll(selector).forEach((sellerLink) => { + try { + const hasVisitWebsite = Array.from(sellerLink.querySelectorAll('span, a')).some((el) => + visitWebsiteLabels.includes(el.textContent?.trim() ?? ''), + ); + + if (hasVisitWebsite) { + const listing = sellerLink.closest('.hz-Listing'); + if (listing && !listing.hasAttribute('data-cleanplaats-hidden') && hideElement(listing)) { + count++; + } + } + } catch (error) { + console.error('Cleanplaats: Error processing promoted listing', error); + } + }); + }); + + document.querySelectorAll('.hz-StructuredListing').forEach((listing) => { + try { + if (listing.hasAttribute('data-cleanplaats-hidden') || !isHomepagePartnerListing(listing)) { + return; + } + + if (hideElement(listing)) { + count++; + } + } catch (error) { + console.error('Cleanplaats: Error processing homepage partner listing', error); + } + }); + + state.stats.promotedListingsRemoved += count; +}; + +const removeOpvalStickerListings = (state: CleanplaatsState): void => { + let count = 0; + const stickerSelectors = [ + '.hz-Listing-Opvalsticker-wrapper, .hz-Listing-Opvalsticker-wrapper-new', + '[data-testid="listing-opval-sticker"]', + ]; + + stickerSelectors.forEach((selector) => { + document.querySelectorAll(selector).forEach((sticker) => { + try { + const listing = sticker.closest('.hz-Listing'); + if (listing && !listing.hasAttribute('data-cleanplaats-hidden') && hideElement(listing)) { + count++; + } + } catch (error) { + console.error('Cleanplaats: Error processing sticker listing', error); + } + }); + }); + + state.stats.opvalStickersRemoved += count; +}; + +const removeReservedListings = (state: CleanplaatsState): void => { + const count = findAndHideListings('.hz-Listing-price, [class*="ListingPrice_hz-Listing-price"]', [ + 'gereserveerd', + 'réservé', + ]); + state.stats.otherAdsRemoved += count; +}; + +const removeAllAds = (state: CleanplaatsState): void => { + let count = 0; + const marktplaatsMarketingBannerSelector = '.MpCard-mpCardBanner, img[alt="Marktplaats Marketing Banner"]'; + const marktplaatsMarketingBannerWrapperSelector = 'div[role="button"][tabindex]'; + const getMarktplaatsMarketingBannerContainer = (element: Element | null): Element | null => { + if (!(element instanceof Element)) { + return null; + } + + const bannerCard = element.closest('.MpCard-mpCardBanner'); + if (bannerCard) { + const bannerWrapper = bannerCard.closest(marktplaatsMarketingBannerWrapperSelector); + if (bannerWrapper?.querySelector(marktplaatsMarketingBannerSelector)) { + return bannerWrapper; + } + + return bannerCard; + } + + const bannerWrapper = element.closest(marktplaatsMarketingBannerWrapperSelector); + if (bannerWrapper?.querySelector(marktplaatsMarketingBannerSelector)) { + return bannerWrapper; + } + + return element.closest('img[alt="Marktplaats Marketing Banner"]'); + }; + + const isMarktplaatsSponsoredNotice = (element: Element | null): boolean => { + if (!element) return false; + + const text = (element.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase(); + return text.includes('de volgorde van de resultaten wordt mede bepaald door betaalde opvalmogelijkheden'); + }; + + const isMarktplaatsMarketingBanner = (element: Element | null): boolean => { + if (!element) return false; + + if ( + element.matches?.('.MpCard-mpCardBanner') + || element.querySelector?.(marktplaatsMarketingBannerSelector) + ) { + return true; + } + + const bannerImage = element.querySelector?.('img[alt="Marktplaats Marketing Banner"]'); + return Boolean(bannerImage); + }; + + const safeHide = (selector: string): void => { + try { + const elements = document.querySelectorAll(selector); + elements.forEach((el) => { + if (!el.hasAttribute('data-cleanplaats-hidden') && hideElement(el)) { + count++; + } + + const parentLi = el.closest('li.bannerContainerLoading'); + if (parentLi && !parentLi.hasAttribute('data-cleanplaats-hidden')) { + hideElement(parentLi); + } + + const feedBanner = el.closest('.hz-FeedBannerBlock, .Banners-bannerFeedItem'); + if (feedBanner && !feedBanner.hasAttribute('data-cleanplaats-hidden')) { + hideElement(feedBanner); + } + + const topBanner = el.closest('.BannerTop-root, #top-banner-root'); + if (topBanner && !topBanner.hasAttribute('data-cleanplaats-hidden')) { + hideElement(topBanner); + } + }); + } catch (error) { + console.log('Cleanplaats: Error hiding ads', error); + } + }; + + document.querySelectorAll('.hz-Listing-imageOverlayLabel').forEach((overlay) => { + if (overlay.textContent?.trim() === 'Homepagina-advertentie') { + const link = overlay.closest('.hz-Link.hz-Link--block'); + if (link && !link.hasAttribute('data-cleanplaats-hidden')) { + hideElement(link); + count++; + } + } + }); + + const adSelectors = [ + '#adsense-root', + '#adsense-container', + '#adsense-container-bottom-lazy', + '#similar-items-root', + '.AdmarktSimilarItemsContainer', + '.AdmarktSimilarItems-root', + '.AdmarktSimilarItems-headerTitle', + '#adBlock', + '.ndfc-wrapper[data-testid="ndfc-generic-text"]', + '[data-testid="ndfc-close"]', + '.MpCard-mpCardBanner', + 'div[role="button"][tabindex] > .MpCard-mpCardBanner', + 'img[alt="Marktplaats Marketing Banner"]', + '.hz-Banner', + '.hz-Banner--fluid', + '.BannerTop-root', + '#banner-rubrieks-dt', + '#banner-top-dt', + '#banner-top-dt-container', + '#top-banner-root', + '[data-google-query-id]', + '[id*="google_ads_iframe"]', + '[id*="google_ads_top_frame"]', + '[aria-label="Advertisement"]', + '[title="3rd party ad content"]', + '.i_.div', + '[data-ad-container]', + '[data-bg="true"]', + '[class*="adsbygoogle"]', + 'ins.adsbygoogle', + 'iframe[src*="googleads"]', + 'iframe[src*="doubleclick"]', + '[id*="div-gpt-ad"]', + '.hz-Listings__container--cas[data-testid="BottomBlockLazyListings"]', + '[class*="creative"]', + '#google_ads_top_frame', + '.creative', + 'li.bannerContainerLoading', + '.bannerContainerLoading', + '.bannerContainerLoading .hz-Banner', + '.bannerContainerLoading .hz-Banner--fluid', + ]; + + adSelectors.forEach((selector) => { + safeHide(selector); + }); + + document.querySelectorAll('.ndfc-wrapper, [data-testid="ndfc-generic-text"]').forEach((notice) => { + if (isMarktplaatsSponsoredNotice(notice) && hideElement(notice)) { + count++; + } + }); + + document.querySelectorAll('.MpCard-mpCardBanner, img[alt="Marktplaats Marketing Banner"]').forEach((banner) => { + const bannerCard = getMarktplaatsMarketingBannerContainer(banner) || banner; + if (isMarktplaatsMarketingBanner(bannerCard) && hideElement(bannerCard)) { + count++; + } + + const bannerWrapper = bannerCard.parentElement; + if ( + bannerWrapper instanceof Element + && bannerWrapper !== bannerCard + && bannerWrapper.childElementCount === 1 + && !bannerWrapper.hasAttribute('data-cleanplaats-hidden') + ) { + hideElement(bannerWrapper); + } + }); + + state.stats.otherAdsRemoved += count; +}; + +export const removePersistentGoogleAds = (state: CleanplaatsState): void => { + let count = 0; + + document + .querySelectorAll( + '#adsense-root, .creative, div[id^="google_ads_iframe"], div[data-google-query-id], div[aria-label="Advertisement"]', + ) + .forEach((ad) => { + try { + const gridItem = ad.closest('.hz-Link.hz-Link--block'); + if (gridItem && gridItem.parentNode) { + gridItem.parentNode.removeChild(gridItem); + count++; + return; + } + if (ad.parentNode) { + ad.parentNode.removeChild(ad); + count++; + } + } catch (error) { + console.error('Cleanplaats: Error removing persistent ad', error); + } + }); + + document.querySelectorAll('#banner-right-container').forEach((banner) => { + if (banner.parentNode) { + banner.parentNode.removeChild(banner); + count++; + } + }); + + document.querySelectorAll('#banner-top-dt-container').forEach((container) => { + if (container.parentNode) { + container.parentNode.removeChild(container); + count++; + } + }); + + document.querySelectorAll('.BannerTop-root').forEach((banner) => { + const hasAdContent = banner.querySelector( + '.hz-Banner, .hz-Banner--fluid, iframe, [data-google-query-id], [id*="google_ads_iframe"], ins.adsbygoogle', + ); + if (!hasAdContent && banner.parentNode) { + banner.parentNode.removeChild(banner); + count++; + } + }); + + document.querySelectorAll('#top-banner-root').forEach((container) => { + const hasVisibleContent = Array.from(container.children).some( + (child) => (child as HTMLElement).offsetParent !== null, + ); + if (!hasVisibleContent && container.parentNode) { + container.parentNode.removeChild(container); + count++; + } + }); + + document.querySelectorAll('.hz-FeedBannerBlock, .Banners-bannerFeedItem').forEach((banner) => { + if ( + banner.childElementCount === 0 + || Array.from(banner.children).every((child) => (child as HTMLElement).offsetParent === null) + ) { + if (banner.parentNode) { + banner.parentNode.removeChild(banner); + count++; + } + } + }); + + state.stats.otherAdsRemoved += count; +}; + +const removeSimilarAdsSections = (state: CleanplaatsState): void => { + let count = 0; + + document.querySelectorAll('.SimilarAdsList-related-ads-section').forEach((section) => { + if (hideElement(section)) { + count++; + } + }); + + state.stats.otherAdsRemoved += count; +}; + +const removeNonFeatureBuyerBanner = (state: CleanplaatsState): void => { + let count = 0; + + document + .querySelectorAll( + '#notifications-root, .NonFeatureBuyerBanner-root, .feature-banner[data-testid="50-percent-off-banner"]', + ) + .forEach((element) => { + const banner = + element.id === 'notifications-root' + ? element + : element.closest('#notifications-root') + || element.closest('.feature-banner[data-testid="50-percent-off-banner"]') + || element; + + if (hideElement(banner)) { + count++; + } + }); + + state.stats.otherAdsRemoved += count; +}; + +const applyBlacklist = (state: CleanplaatsState): void => { + document.querySelectorAll('.hz-Listing').forEach((listing) => { + const sellerNameEl = listing.querySelector( + '.hz-Listing-seller-name, .hz-Listing-seller-name-new, .hz-Listing-seller-link, .hz-Listing-sellerName, .hz-Listing-sellerName-new', + ); + if (!sellerNameEl) return; + const sellerName = sellerNameEl.textContent?.trim() ?? ''; + if (state.settings.blacklistedSellers.includes(sellerName)) { + listing.setAttribute('data-cleanplaats-hidden', 'true'); + (listing as HTMLElement).style.display = 'none'; + } + }); + + document.querySelectorAll('.hz-Link').forEach((link) => { + const title = getListingTitleText(link); + if (!title) return; + state.settings.blacklistedTerms.forEach((term) => { + if (title.includes(term.toLowerCase())) { + const listingEl = link.closest('.hz-StructuredListing') || link; + listingEl.setAttribute('data-cleanplaats-hidden', 'true'); + (listingEl as HTMLElement).style.display = 'none'; + } + }); + }); + + document.querySelectorAll('.hz-Listing').forEach((listing) => { + const title = getListingTitleText(listing); + if (!title) return; + state.settings.blacklistedTerms.forEach((term) => { + if (title.includes(term.toLowerCase())) { + listing.setAttribute('data-cleanplaats-hidden', 'true'); + (listing as HTMLElement).style.display = 'none'; + } + }); + }); +}; + +export const performCleanup = (state: CleanplaatsState): void => { + removeAllAds(state); + removePersistentGoogleAds(state); + if (state.settings.removeFavoriteRelatedAds) removeSimilarAdsSections(state); + removeNonFeatureBuyerBanner(state); + + if (state.settings.removeTopAds) removeTopAdvertisements(state); + if (state.settings.removeDagtoppers) removeDagtoppers(state); + if (state.settings.removePromotedListings) removePromotedListings(state); + if (state.settings.removeOpvalStickers) removeOpvalStickerListings(state); + if (state.settings.removeReservedListings) removeReservedListings(state); + + applyBlacklist(state); + notifyStatsChanged(); +}; + +export const resetPreviousChanges = (state: CleanplaatsState): void => { + state.stats.topAdsRemoved = 0; + state.stats.dagtoppersRemoved = 0; + state.stats.promotedListingsRemoved = 0; + state.stats.opvalStickersRemoved = 0; + state.stats.otherAdsRemoved = 0; + state.stats.totalRemoved = 0; + notifyStatsChanged(); + + document.querySelectorAll('[data-cleanplaats-hidden]').forEach((el) => { + try { + (el as HTMLElement).style.cssText = el.getAttribute('data-original-style') ?? ''; + el.removeAttribute('data-cleanplaats-hidden'); + el.removeAttribute('data-original-style'); + } catch (error) { + console.error('Cleanplaats: Error restoring element', error); + } + }); +}; + +export const performInitialCleanup = (state: CleanplaatsState): void => { + try { + performCleanup(state); + } catch (error) { + console.error('Cleanplaats: Initial cleanup failed', error); + } +}; diff --git a/src/content/services/notifications.ts b/src/content/services/notifications.ts new file mode 100644 index 0000000..120e00e --- /dev/null +++ b/src/content/services/notifications.ts @@ -0,0 +1,566 @@ +import { getPanelLocaleText } from '@/content/locale/panel-text'; +import { getState, patchPanelState, saveSettings } from '@/content/runtime/store'; +import { parseSellerAgeToDays, thresholdToDays } from '@/shared/utils/seller-age'; +import { CLEANPLAATS_UPDATE_NOTES } from '@/shared/constants/update-notes'; +import type { SettingsRepository } from '@/shared/storage/repository'; +import { isProductDetailPage } from '@/content/utils/site'; + +import { performCleanup } from '@/content/services/cleanup'; + +let notificationTimeout = 0; + +export const getExtensionVersion = (): string => { + try { + const manifest = browser.runtime.getManifest(); + if (manifest && typeof manifest.version === 'string') { + return manifest.version; + } + } catch (error) { + console.error('Cleanplaats: Failed to read extension version', error); + } + return ''; +}; + +const clearSellerAgeWarningToast = (): void => { + const toast = document.getElementById('cleanplaats-seller-age-warning-toast'); + if (toast) { + toast.classList.remove('visible'); + setTimeout(() => { + toast.remove(); + }, 300); + } +}; + +const getSellerAgeWarningThresholdLabel = (): string => { + const panelText = getPanelLocaleText(); + const { settings } = getState(); + const value = Math.max(1, parseInt(String(settings.sellerAgeWarningThresholdValue), 10) || 1); + const unit = settings.sellerAgeWarningThresholdUnit; + const unitLabel = + panelText.sellerAgeWarningThresholdUnits[unit] + ?? panelText.sellerAgeWarningThresholdUnits.months; + + return `${value} ${unitLabel}`; +}; + +const getSellerAgeInfoFromPage = () => { + const sellerRows = Array.from( + document.querySelectorAll('.SellerInfoSmall-root .SellerInfoSmall-row'), + ); + const sellerAgeRow = sellerRows.find((row) => parseSellerAgeToDays(row.textContent ?? '') !== null); + const sellerNameElement = document.querySelector( + '.SellerInfoSmall-root .SellerInfoSmall-name a, .SellerInfoSmall-root .SellerInfoSmall-name', + ); + const sellerAgeText = sellerAgeRow?.textContent?.trim() ?? ''; + const sellerName = sellerNameElement?.textContent?.trim() ?? 'Deze verkoper'; + const sellerAgeDays = parseSellerAgeToDays(sellerAgeText); + + if (!sellerAgeText || sellerAgeDays === null) { + return null; + } + + return { + sellerName, + sellerAgeText, + sellerAgeDays, + }; +}; + +const showSellerAgeWarningToast = ({ + sellerName, + sellerAgeText, +}: { + sellerName: string; + sellerAgeText: string; +}): void => { + const panelText = getPanelLocaleText(); + const thresholdLabel = getSellerAgeWarningThresholdLabel(); + + clearSellerAgeWarningToast(); + + const toast = document.createElement('div'); + toast.className = 'cleanplaats-blacklist-toast cleanplaats-blacklist-toast-warning'; + toast.id = 'cleanplaats-seller-age-warning-toast'; + + toast.innerHTML = ` +
    + ! +
    + ${panelText.sellerAgeWarningToastTitle} + ${panelText.sellerAgeWarningToastMessage(sellerName, sellerAgeText, thresholdLabel)} +
    +
    + `; + + document.body.appendChild(toast); + setTimeout(() => { + requestAnimationFrame(() => toast.classList.add('visible')); + }, 50); + + window.setTimeout(() => { + toast.classList.remove('visible'); + setTimeout(() => toast.remove(), 300); + }, 5200); +}; + +const maybeShowSellerAgeWarning = (options: { force?: boolean } = {}): void => { + const force = options.force === true; + const { settings, runtime } = getState(); + + if (!isProductDetailPage()) { + clearSellerAgeWarningToast(); + return; + } + + if (!settings.sellerAgeWarningEnabled) { + clearSellerAgeWarningToast(); + return; + } + + const sellerAgeInfo = getSellerAgeInfoFromPage(); + if (!sellerAgeInfo) { + clearSellerAgeWarningToast(); + return; + } + + const thresholdDays = thresholdToDays( + settings.sellerAgeWarningThresholdValue, + settings.sellerAgeWarningThresholdUnit, + ); + if (sellerAgeInfo.sellerAgeDays >= thresholdDays) { + clearSellerAgeWarningToast(); + return; + } + + const warningKey = `${location.pathname}|${sellerAgeInfo.sellerAgeText}|${String(thresholdDays)}`; + if (!force && runtime.lastSellerAgeWarningKey === warningKey) { + return; + } + + runtime.lastSellerAgeWarningKey = warningKey; + showSellerAgeWarningToast(sellerAgeInfo); +}; + +export const scheduleSellerAgeWarningCheck = (options: { + force?: boolean; + resetState?: boolean; +} = {}): void => { + const force = options.force === true; + const resetState = options.resetState === true; + const { runtime } = getState(); + + if (resetState) { + runtime.lastSellerAgeWarningKey = ''; + } + + window.clearTimeout(runtime.sellerAgeCheckTimer); + runtime.sellerAgeCheckTimer = window.setTimeout(() => { + maybeShowSellerAgeWarning({ force }); + }, 180); +}; + +export const clearAllNotifications = (): void => { + const notifications = document.querySelectorAll('[id^="cleanplaats-"]'); + notifications.forEach((notification) => { + if ( + notification.classList.contains('cleanplaats-empty-notification') + || notification.id === 'cleanplaats-loading' + || notification.id === 'cleanplaats-seller-age-warning-toast' + ) { + notification.remove(); + } + }); +}; + +export const clearBubbleNotification = (): void => { + const toast = document.getElementById('cleanplaats-bubble-notification'); + if (toast) { + toast.classList.remove('visible'); + setTimeout(() => { + toast.remove(); + }, 300); + } +}; + +export const showBubbleNotification = (message: string): void => { + let toast = document.getElementById('cleanplaats-bubble-notification'); + + if (toast) { + const messageElement = toast.querySelector('.cleanplaats-toast-message span'); + if (messageElement) { + messageElement.textContent = message; + } + } else { + toast = document.createElement('div'); + toast.className = 'cleanplaats-blacklist-toast'; + toast.id = 'cleanplaats-bubble-notification'; + + toast.innerHTML = ` +
    + +
    + ${message} +
    +
    + `; + + document.body.appendChild(toast); + setTimeout(() => requestAnimationFrame(() => toast?.classList.add('visible')), 0); + } + + const t = toast as HTMLElement & { timeoutId?: number }; + if (t.timeoutId) { + clearTimeout(t.timeoutId); + } + + t.timeoutId = window.setTimeout(() => { + toast?.classList.remove('visible'); + setTimeout(() => { + toast?.remove(); + }, 300); + }, 5000); +}; + +export const showBlacklistToast = (sellerName: string): void => { + const panelText = getPanelLocaleText(); + const toast = document.createElement('div'); + toast.className = 'cleanplaats-blacklist-toast'; + + toast.innerHTML = ` +
    + 👁 +
    + ${sellerName} ${panelText.blacklistToastHiddenSuffix} + ${panelText.blacklistToastHint} +
    +
    + `; + + document.body.appendChild(toast); + setTimeout(() => { + requestAnimationFrame(() => toast.classList.add('visible')); + }, 50); + + setTimeout(() => { + toast.classList.remove('visible'); + setTimeout(() => toast.remove(), 300); + }, 3000); +}; + +export const showBulkBlacklistToast = (count: number): void => { + const panelText = getPanelLocaleText(); + const toast = document.createElement('div'); + toast.className = 'cleanplaats-blacklist-toast'; + + toast.innerHTML = ` +
    + 👁 +
    + ${count} ${panelText.blacklistToastHiddenPluralSuffix} + ${panelText.blacklistToastHint} +
    +
    + `; + + document.body.appendChild(toast); + setTimeout(() => { + requestAnimationFrame(() => toast.classList.add('visible')); + }, 50); + + setTimeout(() => { + toast.classList.remove('visible'); + setTimeout(() => toast.remove(), 300); + }, 3000); +}; + +export const showUnblacklistToast = (sellerName: string): void => { + const panelText = getPanelLocaleText(); + const toast = document.createElement('div'); + toast.className = 'cleanplaats-blacklist-toast'; + + toast.innerHTML = ` +
    + 👁 +
    + ${sellerName} ${panelText.blacklistToastShownSuffix} + ${panelText.blacklistToastShownHint} +
    +
    + `; + + document.body.appendChild(toast); + setTimeout(() => { + requestAnimationFrame(() => toast.classList.add('visible')); + }, 50); + + setTimeout(() => { + toast.classList.remove('visible'); + setTimeout(() => toast.remove(), 300); + }, 3000); +}; + +export const showBlacklistTermToast = (term: string): void => { + const panelText = getPanelLocaleText(); + const toast = document.createElement('div'); + toast.className = 'cleanplaats-blacklist-toast'; + toast.innerHTML = ` +
    + 🔎 +
    + '${term}' ${panelText.blacklistToastHiddenSuffix} + ${panelText.termToastHidden(term)} +
    +
    + `; + document.body.appendChild(toast); + setTimeout(() => { + requestAnimationFrame(() => toast.classList.add('visible')); + }, 50); + setTimeout(() => { + toast.classList.remove('visible'); + setTimeout(() => toast.remove(), 300); + }, 3000); +}; + +export const showUnblacklistTermToast = (term: string): void => { + const panelText = getPanelLocaleText(); + const toast = document.createElement('div'); + toast.className = 'cleanplaats-blacklist-toast'; + toast.innerHTML = ` +
    + 🔎 +
    + '${term}' ${panelText.blacklistToastShownSuffix} + ${panelText.termToastShown(term)} +
    +
    + `; + document.body.appendChild(toast); + setTimeout(() => { + requestAnimationFrame(() => toast.classList.add('visible')); + }, 50); + setTimeout(() => { + toast.classList.remove('visible'); + setTimeout(() => toast.remove(), 300); + }, 3000); +}; + +const showFirstTimeOnboarding = (): void => { + const onboarding = document.createElement('div'); + onboarding.className = 'cleanplaats-onboarding'; + onboarding.id = 'cleanplaats-onboarding'; + + onboarding.innerHTML = ` +
    +
    +

    🎉 Welkom bij Cleanplaats!

    + +
    +
    +
    + 1 +

    Cleanplaats verwijdert automatisch advertenties en promotionele content

    +
    +
    + 2 +

    Gebruik het configuratiescherm rechtsonder om de filtering aan te passen. Je opent en sluit het paneel via het pijltje bovenin.

    +
    +
    + 3 +

    Bekijk statistieken over verwijderde items in het configuratiescherm

    +
    +
    + +
    + `; + + document.body.appendChild(onboarding); + + ['cleanplaats-onboarding-close', 'cleanplaats-onboarding-got-it'].forEach((id) => { + document.getElementById(id)?.addEventListener('click', () => { + onboarding.classList.add('cleanplaats-fade-out'); + setTimeout(() => onboarding.remove(), 300); + }); + }); + + setTimeout(() => { + if (onboarding.parentNode) { + onboarding.classList.add('cleanplaats-fade-out'); + setTimeout(() => onboarding.remove(), 300); + } + }, 15000); +}; + +const shouldShowUpdatePopup = (currentVersion: string): boolean => { + if (!currentVersion) { + return false; + } + + return getState().panelState.lastSeenVersion !== currentVersion; +}; + +const showUpdatePopup = (version: string): void => { + const existingPopup = document.getElementById('cleanplaats-update-popup'); + if (existingPopup) { + existingPopup.remove(); + } + + const updateContent = CLEANPLAATS_UPDATE_NOTES[version] ?? { + intro: + 'Cleanplaats heeft een nieuwe update gekregen met verbeteringen en onderhoud aan de extensie.', + highlights: [ + 'Diverse verbeteringen en fixes voor de huidige resultaatpagina’s.', + 'Kleine verfijningen aan het paneel en de filtering.', + 'Onderhoudswerk om Cleanplaats stabiel te houden op nieuwe sitewijzigingen.', + ], + note: 'Zie je een probleem of heb je een idee? Gebruik de GitHub-link in het paneel.', + }; + + const popup = document.createElement('div'); + popup.className = 'cleanplaats-info-overlay cleanplaats-info-overlay--visible'; + popup.id = 'cleanplaats-update-popup'; + popup.setAttribute('role', 'dialog'); + popup.setAttribute('aria-modal', 'true'); + popup.setAttribute('aria-hidden', 'false'); + + const stepsMarkup = updateContent.highlights.map((step) => `
  • ${step}
  • `).join(''); + + popup.innerHTML = ` +
    +
    + + Nieuwe update +

    Wat is er nieuw? (${version})

    +

    ${updateContent.intro}

    +
    +
      ${stepsMarkup}
    +

    ${updateContent.note}

    + +
    + `; + + const closePopup = (): void => { + popup.classList.remove('cleanplaats-info-overlay--visible'); + popup.setAttribute('aria-hidden', 'true'); + setTimeout(() => popup.remove(), 200); + document.removeEventListener('keydown', handleKeydown); + }; + + const handleKeydown = (event: KeyboardEvent): void => { + if (event.key === 'Escape') { + closePopup(); + } + }; + + popup.addEventListener('click', (event) => { + if (event.target === popup) { + closePopup(); + } + }); + + document.addEventListener('keydown', handleKeydown); + document.body.appendChild(popup); + const popupLogo = document.getElementById('cleanplaats-update-popup-logo'); + if (popupLogo instanceof HTMLImageElement) { + popupLogo.src = browser.runtime.getURL('icons/icon128.png'); + } + document.getElementById('cleanplaats-update-popup-close')?.addEventListener('click', () => { + closePopup(); + showBubbleNotification(`Veel plezier met ${version}`); + }); +}; + +const showWelcomeToast = (): void => { + const { panelState, stats } = getState(); + if ( + panelState.hasShownWelcomeToast + || location.pathname !== '/' + || location.hostname !== 'www.marktplaats.nl' + ) { + return; + } + + patchPanelState({ hasShownWelcomeToast: true }); + void saveSettings(repositoryRef); + + const toast = document.createElement('div'); + toast.className = 'cleanplaats-toast'; + toast.id = 'cleanplaats-toast'; + + const totalRemoved = stats.totalRemoved; + const message = + totalRemoved > 0 + ? `Cleanplaats is actief (${totalRemoved} items verwijderd)` + : 'Cleanplaats is actief'; + + toast.innerHTML = ` +
    + + ${message} +
    + `; + + document.body.appendChild(toast); + setTimeout(() => toast.classList.add('visible'), 100); + setTimeout(() => { + toast.classList.remove('visible'); + setTimeout(() => toast.remove(), 300); + }, 3000); +}; + +let repositoryRef!: SettingsRepository; + +export const bindNotificationsRepository = (repository: SettingsRepository): void => { + repositoryRef = repository; +}; + +export const showOnboarding = (currentVersion = ''): void => { + const { featureFlags } = getState(); + if (featureFlags.firstRun) { + if (currentVersion) { + patchPanelState({ lastSeenVersion: currentVersion }); + void saveSettings(repositoryRef).catch((error) => { + console.error('Cleanplaats: Failed to store initial version state', error); + }); + } + showFirstTimeOnboarding(); + } else if (shouldShowUpdatePopup(currentVersion)) { + patchPanelState({ lastSeenVersion: currentVersion }); + void saveSettings(repositoryRef).catch((error) => { + console.error('Cleanplaats: Failed to store seen update version', error); + }); + showUpdatePopup(currentVersion); + } else { + showWelcomeToast(); + } +}; + +export const checkForEmptyPage = (): void => { + clearTimeout(notificationTimeout); + + notificationTimeout = window.setTimeout(() => { + performCleanup(getState()); + + const visibleListings = document.querySelectorAll('.hz-Listing:not([data-cleanplaats-hidden])'); + const totalListings = document.querySelectorAll('.hz-Listing'); + const hiddenCount = totalListings.length - visibleListings.length; + + if (hiddenCount === 0) return; + + clearAllNotifications(); + + if (visibleListings.length === 0) { + showBubbleNotification( + 'De pagina is leeg omdat deze helemaal uit advertenties bestond! Probeer een volgende pagina of wijzig de filters.', + ); + } else if (visibleListings.length < 5) { + const listingWord = visibleListings.length === 1 ? 'resultaat' : 'resultaten'; + const removedWord = hiddenCount === 1 ? 'advertentie' : 'advertenties'; + showBubbleNotification( + `Er ${visibleListings.length === 1 ? 'is' : 'zijn'} nog ${visibleListings.length} ${listingWord} over nadat Cleanplaats ${hiddenCount} ${removedWord} heeft verwijderd.`, + ); + } + }, 1000); +}; diff --git a/src/content/services/observers.ts b/src/content/services/observers.ts new file mode 100644 index 0000000..2aee112 --- /dev/null +++ b/src/content/services/observers.ts @@ -0,0 +1,193 @@ +import { getState } from '@/content/runtime/store'; +import { performCleanup } from '@/content/services/cleanup'; +import { + checkForEmptyPage, + clearBubbleNotification, + scheduleSellerAgeWarningCheck, +} from '@/content/services/notifications'; +import { injectBlacklistButtons } from '@/content/services/blacklist-inject'; +import { syncHeaderLogoForDarkMode } from '@/content/services/theme'; +import { wakeUpBackground } from '@/content/services/background-wake'; + +let lastUrl = location.href; + +const performCleanupAndCheckForEmptyPage = (): void => { + const existingNotification = document.getElementById('cleanplaats-empty-notification'); + if (existingNotification) { + existingNotification.remove(); + } + + clearBubbleNotification(); + scheduleSellerAgeWarningCheck({ resetState: true }); + + const checkContentLoaded = window.setInterval(() => { + if (document.querySelector('.hz-Listing') || document.querySelector('#adsense-container')) { + clearInterval(checkContentLoaded); + console.log('Cleanplaats: Running cleanup after navigation'); + performCleanup(getState()); + injectBlacklistButtons(); + + setTimeout(checkForEmptyPage, 500); + } + }, 100); +}; + +export const setupObservers = (): void => { + const state = getState(); + + if (state.observers.mutation) { + state.observers.mutation.disconnect(); + } + + const observer = new MutationObserver((mutations) => { + if (lastUrl !== location.href) { + console.log('Cleanplaats: URL changed from', lastUrl, 'to', location.href); + lastUrl = location.href; + state.runtime.lastSellerAgeWarningKey = ''; + performCleanupAndCheckForEmptyPage(); + } + + let shouldCleanup = false; + let shouldSyncHeaderLogo = false; + + for (const mutation of mutations) { + if (mutation.type === 'childList' && mutation.addedNodes.length) { + const listingMutationTarget = + mutation.target?.nodeType === Node.ELEMENT_NODE + ? (mutation.target as Element).closest?.('.hz-Listing') + : null; + + if (window.innerWidth < 700 && listingMutationTarget) { + shouldCleanup = true; + break; + } + + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as Element; + if ( + el.classList?.contains('hz-Header-logo-desktop') + || el.classList?.contains('mp-Header-logo') + || el.querySelector?.('.hz-Header-logo-desktop, .mp-Header-logo') + ) { + shouldSyncHeaderLogo = true; + } + + if ( + el.classList?.contains('SellerInfoSmall-root') + || el.querySelector?.('.SellerInfoSmall-root') + ) { + scheduleSellerAgeWarningCheck(); + } + + if ( + el.classList?.contains('hz-Listing') + || el.querySelector?.('.hz-Listing') + || el.classList?.contains('MpCard-mpCardBanner') + || el.querySelector?.('.MpCard-mpCardBanner, img[alt="Marktplaats Marketing Banner"]') + || el.classList?.contains('SimilarAdsList-related-ads-section') + || el.querySelector?.('.SimilarAdsList-related-ads-section') + || el.id === 'notifications-root' + || el.classList?.contains('NonFeatureBuyerBanner-root') + || el.classList?.contains('feature-banner') + || el.querySelector?.( + '#notifications-root, .NonFeatureBuyerBanner-root, .feature-banner[data-testid="50-percent-off-banner"]', + ) + || el.id?.includes('ad') + || el.id === 'similar-items-root' + || el.querySelector?.( + '#similar-items-root, .AdmarktSimilarItemsContainer, .AdmarktSimilarItems-root', + ) + || el.classList?.contains('hz-Banner') + || el.querySelector?.('[data-google-query-id]') + || el.classList?.contains('hz-FeedBannerBlock') + || el.classList?.contains('Banners-bannerFeedItem') + || el.id === 'banner-top-dt-container' + || el.querySelector?.('#banner-top-dt, #banner-top-dt-container') + ) { + shouldCleanup = true; + break; + } + } + } + } + + if (mutation.type === 'attributes') { + const target = mutation.target as Element; + if (target?.classList?.contains('SellerInfoSmall-root')) { + scheduleSellerAgeWarningCheck(); + } + + if ( + target?.classList?.contains('hz-FeedBannerBlock') + || target?.classList?.contains('Banners-bannerFeedItem') + || target?.classList?.contains('MpCard-mpCardBanner') + || target?.classList?.contains('SimilarAdsList-related-ads-section') + || target?.classList?.contains('NonFeatureBuyerBanner-root') + || target?.classList?.contains('feature-banner') + || target?.classList?.contains('AdmarktSimilarItemsContainer') + || target?.classList?.contains('AdmarktSimilarItems-root') + || target?.id === 'notifications-root' + || target?.id === 'similar-items-root' + || target?.id === 'banner-right-container' + || target?.id === 'banner-top-dt-container' + ) { + shouldCleanup = true; + } + } + + if (shouldCleanup) break; + } + + if (state.settings.darkMode && shouldSyncHeaderLogo) { + syncHeaderLogoForDarkMode(true); + } + + if (shouldCleanup) { + performCleanup(state); + injectBlacklistButtons(); + } + }); + + observer.observe(document, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'style', 'hidden', 'aria-hidden'], + }); + + state.observers.mutation = observer; +}; + +const handleNavigation = (): void => { + wakeUpBackground(); + window.dispatchEvent(new Event('navigation')); +}; + +export const setupNavigationDetection = (): void => { + window.addEventListener('popstate', handleNavigation); + + const originalPushState = history.pushState; + history.pushState = function pushStateWithHook(...args: Parameters) { + originalPushState.apply(this, args); + }; + + const originalReplaceState = history.replaceState; + history.replaceState = function replaceStateWithHook( + ...args: Parameters + ) { + originalReplaceState.apply(this, args); + }; + + document.addEventListener('click', (e) => { + const link = (e.target as Element | null)?.closest?.('a[href]'); + if (link instanceof HTMLAnchorElement && link.hostname === window.location.hostname) { + setTimeout(() => handleNavigation(), 100); + } + }); +}; + +export const setupAllObservers = (): void => { + setupObservers(); + setupNavigationDetection(); +}; diff --git a/src/content/services/sort-sync.ts b/src/content/services/sort-sync.ts new file mode 100644 index 0000000..a2790ec --- /dev/null +++ b/src/content/services/sort-sync.ts @@ -0,0 +1,45 @@ +import { wakeUpBackground } from '@/content/services/background-wake'; +import { getState, patchSettings, saveSettings } from '@/content/runtime/store'; +import { getSortModeFromLabel, isMarketplaceSortDropdown } from '@/content/utils/sort'; +import type { SettingsRepository } from '@/shared/storage/repository'; +import type { SortMode } from '@/shared/types/state'; + +export const syncCleanplaatsSortMode = async ( + sortMode: SortMode | null, + repository: SettingsRepository, +): Promise => { + if (!sortMode) return; + + const current = getState().settings; + const modeChanged = current.defaultSortMode !== sortMode; + const sourceChanged = current.sortPreferenceSource !== 'marketplace'; + if (!modeChanged && !sourceChanged) return; + + patchSettings({ defaultSortMode: sortMode, sortPreferenceSource: 'marketplace' }); + wakeUpBackground(); + try { + await saveSettings(repository); + } catch (error) { + console.error('Cleanplaats: Failed to sync sort mode from page selection', error); + } +}; + +export const setupMarketplaceSortSync = (repository: SettingsRepository): void => { + if (document.body?.dataset.cleanplaatsSortSyncBound === 'true') return; + if (document.body) { + document.body.dataset.cleanplaatsSortSyncBound = 'true'; + } + + document.addEventListener( + 'change', + (event) => { + const target = event.target; + if (!isMarketplaceSortDropdown(target)) return; + + const selectedOption = target.options[target.selectedIndex]; + const sortMode = getSortModeFromLabel(selectedOption?.textContent ?? target.value); + void syncCleanplaatsSortMode(sortMode, repository); + }, + true, + ); +}; diff --git a/src/content/services/theme.ts b/src/content/services/theme.ts new file mode 100644 index 0000000..538d3dc --- /dev/null +++ b/src/content/services/theme.ts @@ -0,0 +1,184 @@ +import { + CLEANPLAATS_DARK_LOGO_PATH, + CLEANPLAATS_DARK_MODE_CLASS, + CLEANPLAATS_FLOATING_OFFSET_VAR, + CLEANPLAATS_TWH_SITE_CLASS, + MARKTPLAATS_DESKTOP_LOGO_MATCH, +} from '@/content/constants/ui'; +import { is2dehandsFamilySite, isMarktplaatsSite } from '@/content/utils/site'; +import { LOCAL_STORAGE_KEYS } from '@/shared/constants/storage'; +import type { CleanplaatsSettings } from '@/shared/types/state'; + +export const persistDarkModePreference = (enabled: boolean): void => { + try { + window.localStorage.setItem(LOCAL_STORAGE_KEYS.darkMode, enabled ? 'true' : 'false'); + } catch (error) { + console.warn('Cleanplaats: Failed to persist dark mode in localStorage', error); + } +}; + +export const syncSiteThemeClass = (): void => { + document.documentElement.classList.toggle(CLEANPLAATS_TWH_SITE_CLASS, is2dehandsFamilySite()); +}; + +export const getCollapsedPanelIconUrl = (darkMode: boolean): string => { + const iconPath = darkMode ? 'icons/darkmode_icon_128.png' : 'icons/icon128.png'; + return browser.runtime.getURL(iconPath); +}; + +export const updateCollapsedPanelIcon = ( + panel: HTMLElement | null, + settings: CleanplaatsSettings, +): void => { + if (!panel) return; + + if (panel.classList.contains('collapsed-ready')) { + panel.style.backgroundImage = `url('${getCollapsedPanelIconUrl(settings.darkMode)}')`; + panel.style.backgroundRepeat = 'no-repeat'; + panel.style.backgroundPosition = 'center'; + panel.style.backgroundSize = 'contain'; + return; + } + + panel.style.removeProperty('background-image'); + panel.style.removeProperty('background-repeat'); + panel.style.removeProperty('background-position'); + panel.style.removeProperty('background-size'); +}; + +export const syncHeaderLogoForDarkMode = (enabled: boolean): void => { + document.querySelectorAll('.hz-Header-logo-desktop').forEach((img) => { + if (!(img instanceof HTMLImageElement)) return; + + const currentSource = img.getAttribute('src') || ''; + const originalSource = img.dataset.cleanplaatsOriginalSrc || currentSource; + + if (!img.dataset.cleanplaatsOriginalSrc) { + img.dataset.cleanplaatsOriginalSrc = currentSource; + } + + if (!MARKTPLAATS_DESKTOP_LOGO_MATCH.test(originalSource)) { + return; + } + + const nextSource = enabled + ? browser.runtime.getURL(CLEANPLAATS_DARK_LOGO_PATH) + : originalSource; + + if (currentSource !== nextSource) { + img.setAttribute('src', nextSource); + } + }); + + document.querySelectorAll('.mp-Header-logo').forEach((link) => { + if (!(link instanceof HTMLElement)) return; + + if (enabled && isMarktplaatsSite()) { + link.style.backgroundImage = `url("${browser.runtime.getURL(CLEANPLAATS_DARK_LOGO_PATH)}")`; + link.style.backgroundRepeat = 'no-repeat'; + link.style.backgroundPosition = 'center'; + link.style.backgroundSize = 'contain'; + return; + } + + link.style.removeProperty('background-image'); + link.style.removeProperty('background-repeat'); + link.style.removeProperty('background-position'); + link.style.removeProperty('background-size'); + }); +}; + +export const applyDarkModeToDocument = ( + enabled: boolean, + panel: HTMLElement | null, + settings: CleanplaatsSettings, +): void => { + const isEnabled = Boolean(enabled); + syncSiteThemeClass(); + document.documentElement.classList.toggle(CLEANPLAATS_DARK_MODE_CLASS, isEnabled); + persistDarkModePreference(isEnabled); + syncHeaderLogoForDarkMode(isEnabled); + + if (panel) { + panel.classList.toggle(CLEANPLAATS_DARK_MODE_CLASS, isEnabled); + updateCollapsedPanelIcon(panel, settings); + } +}; + +const isElementVisuallyVisible = (element: Element): boolean => { + if (!(element instanceof Element)) return false; + + const style = window.getComputedStyle(element); + if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { + return false; + } + + const rect = element.getBoundingClientRect(); + return ( + rect.width > 0 + && rect.height > 0 + && rect.bottom > 0 + && rect.right > 0 + && rect.top < window.innerHeight + && rect.left < window.innerWidth + ); +}; + +export const updateFloatingUiOffsetForWebchat = (): void => { + const webchatToggle = document.querySelector( + '[data-cognigy-webchat-toggle="true"], #webchatWindowToggleButton', + ); + + let offset = 0; + + if (webchatToggle && isElementVisuallyVisible(webchatToggle)) { + const rect = webchatToggle.getBoundingClientRect(); + const gap = 16; + offset = Math.max(0, Math.ceil(rect.height + gap)); + } + + document.documentElement.style.setProperty(CLEANPLAATS_FLOATING_OFFSET_VAR, `${offset}px`); +}; + +export type WebchatObserverHandle = { + disconnect: () => void; +}; + +export const setupWebchatCollisionAvoidance = ( + existing: MutationObserver | null, +): { observer: MutationObserver; handle: WebchatObserverHandle } => { + updateFloatingUiOffsetForWebchat(); + + if (existing) { + existing.disconnect(); + } + + let rafId = 0; + const scheduleOffsetUpdate = (): void => { + if (rafId) return; + rafId = window.requestAnimationFrame(() => { + rafId = 0; + updateFloatingUiOffsetForWebchat(); + }); + }; + + const observer = new MutationObserver(scheduleOffsetUpdate); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style', 'class', 'hidden', 'aria-hidden'], + }); + + window.addEventListener('resize', scheduleOffsetUpdate, { passive: true }); + + return { + observer, + handle: { + disconnect: () => { + observer.disconnect(); + window.removeEventListener('resize', scheduleOffsetUpdate); + }, + }, + }; +}; diff --git a/theme-init.js b/src/content/theme/early-dark-mode-css.ts similarity index 66% rename from theme-init.js rename to src/content/theme/early-dark-mode-css.ts index 432a71b..acd83fd 100644 --- a/theme-init.js +++ b/src/content/theme/early-dark-mode-css.ts @@ -1,11 +1,5 @@ -(() => { - const browserAPI = typeof browser !== 'undefined' ? browser : chrome; - const DARK_MODE_CLASS = 'cleanplaats-dark-mode'; - const TWH_SITE_CLASS = 'cleanplaats-site-twh'; - const THEME_STORAGE_KEY = 'cleanplaats:darkMode'; - const STORAGE_KEY = 'cleanplaatsSettings'; - const EARLY_STYLE_ID = 'cleanplaats-early-dark-mode'; - const EARLY_DARK_MODE_CSS = ` +/** Inlined critical dark-mode rules (same as legacy theme-init.js) for document_start paint. */ +export const EARLY_DARK_MODE_CSS = ` html.cleanplaats-dark-mode, html.cleanplaats-dark-mode body, html.cleanplaats-dark-mode .hz-Page, @@ -122,69 +116,3 @@ html.cleanplaats-dark-mode [class*="Skeleton-withAnimation"]::before { ) !important; } `; - - function ensureEarlyDarkModeStyle(enabled) { - const existing = document.getElementById(EARLY_STYLE_ID); - - if (!enabled) { - existing?.remove(); - return; - } - - if (existing) { - return; - } - - const style = document.createElement('style'); - style.id = EARLY_STYLE_ID; - style.textContent = EARLY_DARK_MODE_CSS; - (document.head || document.documentElement).appendChild(style); - } - - function syncSiteThemeClass() { - const isTwhSite = location.hostname.includes('2dehands.be') || location.hostname.includes('2ememain.be'); - document.documentElement.classList.toggle(TWH_SITE_CLASS, isTwhSite); - } - - function applyDarkMode(enabled) { - const isEnabled = Boolean(enabled); - syncSiteThemeClass(); - document.documentElement.classList.toggle(DARK_MODE_CLASS, isEnabled); - ensureEarlyDarkModeStyle(isEnabled); - } - - function readDarkModePreference() { - try { - const storedDarkMode = window.localStorage.getItem(THEME_STORAGE_KEY); - if (storedDarkMode === 'true' || storedDarkMode === 'false') { - return storedDarkMode === 'true'; - } - } catch (error) { - console.warn('Cleanplaats: Failed to read dark mode from localStorage during startup', error); - } - - return false; - } - - function registerStorageSync() { - if (!browserAPI?.storage?.onChanged?.addListener) { - return; - } - - browserAPI.storage.onChanged.addListener((changes, areaName) => { - if (areaName !== 'local' || !changes[STORAGE_KEY]) { - return; - } - - try { - const settings = JSON.parse(changes[STORAGE_KEY].newValue || '{}'); - applyDarkMode(settings?.darkMode); - } catch (error) { - console.error('Cleanplaats: Failed to sync startup dark mode', error); - } - }); - } - - applyDarkMode(readDarkModePreference()); - registerStorageSync(); -})(); diff --git a/src/content/utils/site.ts b/src/content/utils/site.ts new file mode 100644 index 0000000..c85718c --- /dev/null +++ b/src/content/utils/site.ts @@ -0,0 +1,40 @@ +import type { ReviewCtaConfig } from '@/shared/types/state'; + +export const is2ememainLocale = (): boolean => location.hostname.includes('2ememain.be'); + +export const is2dehandsFamilySite = (): boolean => + location.hostname.includes('2dehands.be') || location.hostname.includes('2ememain.be'); + +export const isMarktplaatsSite = (): boolean => location.hostname.includes('marktplaats.nl'); + +export const isProductDetailPage = (): boolean => /\/v\//.test(window.location.pathname); + +export const isSearchResultsPage = (): boolean => { + const url = window.location.href; + return ( + url.includes('marktplaats.nl/l/') + || url.includes('marktplaats.nl/q/') + || url.includes('2dehands.be/l/') + || url.includes('2dehands.be/q/') + || url.includes('2ememain.be/l/') + || url.includes('2ememain.be/q/') + ); +}; + +export const getReviewCTAConfig = (): ReviewCtaConfig => { + const runtimeUrl = browser.runtime?.getURL ? browser.runtime.getURL('') : ''; + const isFirefox = + runtimeUrl.startsWith('moz-extension://') || navigator.userAgent.includes('Firefox'); + + if (isFirefox) { + return { + linkLabel: 'Firefox Add-ons', + url: 'https://addons.mozilla.org/nl/firefox/addon/cleanplaats-marktplaats-filter/reviews/', + }; + } + + return { + linkLabel: 'Chrome Web Store', + url: 'https://chromewebstore.google.com/detail/cleanplaats-marktplaats-z/peebdbeclpkljmfocjifjpjlngfpfhjp/reviews', + }; +}; diff --git a/src/content/utils/sort.ts b/src/content/utils/sort.ts new file mode 100644 index 0000000..7a99a33 --- /dev/null +++ b/src/content/utils/sort.ts @@ -0,0 +1,18 @@ +import { MARKTPLAATS_SORT_LABEL_TO_MODE } from '@/shared/constants/settings'; +import type { SortMode } from '@/shared/types/state'; + +export const normalizeSortLabel = (label: string): string => label.trim().toLowerCase(); + +export const getSortModeFromLabel = (label: string): SortMode | null => + MARKTPLAATS_SORT_LABEL_TO_MODE[normalizeSortLabel(label)] ?? null; + +export const isMarketplaceSortDropdown = (element: EventTarget | null): element is HTMLSelectElement => { + if (!(element instanceof HTMLSelectElement)) return false; + + const ariaLabel = normalizeSortLabel(element.getAttribute('aria-label') ?? ''); + if (ariaLabel === 'sorteer op') return true; + + return Array.from(element.options ?? []).some( + (option) => normalizeSortLabel(option.textContent ?? '') === 'datum (nieuw-oud)', + ); +}; diff --git a/src/entrypoints/.gitkeep b/src/entrypoints/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/entrypoints/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/entrypoints/background.ts b/src/entrypoints/background.ts new file mode 100644 index 0000000..4b9d945 --- /dev/null +++ b/src/entrypoints/background.ts @@ -0,0 +1,5 @@ +import { initializeBackground } from '@/background'; + +export default defineBackground(() => { + initializeBackground(); +}); diff --git a/src/entrypoints/main.content.ts b/src/entrypoints/main.content.ts new file mode 100644 index 0000000..4665a0b --- /dev/null +++ b/src/entrypoints/main.content.ts @@ -0,0 +1,19 @@ +import '../styles/content.css'; +import { initCleanplaats } from '@/content/bootstrap'; + +export default defineContentScript({ + matches: ['*://*.marktplaats.nl/*', '*://*.2dehands.be/*', '*://*.2ememain.be/*'], + runAt: 'document_end', + cssInjectionMode: 'manifest', + main() { + const start = (): void => { + void initCleanplaats(); + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', start, { once: true }); + } else { + start(); + } + }, +}); diff --git a/src/entrypoints/theme-init.content.ts b/src/entrypoints/theme-init.content.ts new file mode 100644 index 0000000..b455656 --- /dev/null +++ b/src/entrypoints/theme-init.content.ts @@ -0,0 +1,82 @@ +import '@/styles/dark-mode.css'; +import { EARLY_DARK_MODE_CSS } from '@/content/theme/early-dark-mode-css'; +import { LOCAL_STORAGE_KEYS, STORAGE_KEYS } from '@/shared/constants/storage'; +import { CLEANPLAATS_DARK_MODE_CLASS, CLEANPLAATS_TWH_SITE_CLASS } from '@/content/constants/ui'; + +const EARLY_STYLE_ID = 'cleanplaats-early-dark-mode'; + +const ensureEarlyDarkModeStyle = (enabled: boolean): void => { + const existing = document.getElementById(EARLY_STYLE_ID); + + if (!enabled) { + existing?.remove(); + return; + } + + if (existing) { + return; + } + + const style = document.createElement('style'); + style.id = EARLY_STYLE_ID; + style.textContent = EARLY_DARK_MODE_CSS; + (document.head || document.documentElement).appendChild(style); +}; + +const syncSiteThemeClass = (): void => { + const isTwhSite = + location.hostname.includes('2dehands.be') || location.hostname.includes('2ememain.be'); + document.documentElement.classList.toggle(CLEANPLAATS_TWH_SITE_CLASS, isTwhSite); +}; + +const applyDarkMode = (enabled: boolean): void => { + const isEnabled = Boolean(enabled); + syncSiteThemeClass(); + document.documentElement.classList.toggle(CLEANPLAATS_DARK_MODE_CLASS, isEnabled); + ensureEarlyDarkModeStyle(isEnabled); +}; + +const readDarkModePreference = (): boolean => { + try { + const storedDarkMode = window.localStorage.getItem(LOCAL_STORAGE_KEYS.darkMode); + if (storedDarkMode === 'true' || storedDarkMode === 'false') { + return storedDarkMode === 'true'; + } + } catch (error) { + console.warn('Cleanplaats: Failed to read dark mode from localStorage during startup', error); + } + + return false; +}; + +const registerStorageSync = (): void => { + if (!browser.storage.onChanged.addListener) { + return; + } + + browser.storage.onChanged.addListener( + (changes: Record, areaName: string) => { + if (areaName !== 'local' || !changes[STORAGE_KEYS.settings]) { + return; + } + + try { + const raw = changes[STORAGE_KEYS.settings]?.newValue; + const settings = typeof raw === 'string' ? (JSON.parse(raw) as { darkMode?: boolean }) : {}; + applyDarkMode(Boolean(settings?.darkMode)); + } catch (error) { + console.error('Cleanplaats: Failed to sync startup dark mode', error); + } + }, + ); +}; + +export default defineContentScript({ + matches: ['*://*.marktplaats.nl/*', '*://*.2dehands.be/*', '*://*.2ememain.be/*'], + runAt: 'document_start', + allFrames: true, + main() { + applyDarkMode(readDarkModePreference()); + registerStorageSync(); + }, +}); diff --git a/src/shared/constants/domains.ts b/src/shared/constants/domains.ts new file mode 100644 index 0000000..0b379cd --- /dev/null +++ b/src/shared/constants/domains.ts @@ -0,0 +1,28 @@ +export const HOST_MATCH_PATTERNS = [ + '*://*.marktplaats.nl/*', + '*://*.2dehands.be/*', + '*://*.2ememain.be/*', +] as const; + +export const HASH_URL_PATTERNS = [ + 'https://www.marktplaats.nl/l/', + 'https://www.marktplaats.nl/q/', + 'https://www.2dehands.be/l/', + 'https://www.2dehands.be/q/', + 'https://www.2ememain.be/l/', + 'https://www.2ememain.be/q/', +] as const; + +export const API_URL_PATTERNS = [ + 'https://www.marktplaats.nl/lrp/api/search*', + 'https://www.2dehands.be/lrp/api/search*', + 'https://www.2ememain.be/lrp/api/search*', +] as const; + +export const API_RULE_ID = 1; + +export const WAKEUP_NAVIGATION_FILTERS = [ + { hostSuffix: 'marktplaats.nl' }, + { hostSuffix: '2dehands.be' }, + { hostSuffix: '2ememain.be' }, +] as const; diff --git a/src/shared/constants/settings.ts b/src/shared/constants/settings.ts new file mode 100644 index 0000000..d4e84b3 --- /dev/null +++ b/src/shared/constants/settings.ts @@ -0,0 +1,58 @@ +import type { + CleanplaatsPanelState, + CleanplaatsSettings, + SortMode, + SortModeConfig, +} from '@/shared/types/state'; + +export const SORT_MODES: Record = { + standard: { sortBy: 'OPTIMIZED', sortOrder: 'DECREASING' }, + date_new_old: { sortBy: 'SORT_INDEX', sortOrder: 'DECREASING' }, + date_old_new: { sortBy: 'SORT_INDEX', sortOrder: 'INCREASING' }, + price_low_high: { sortBy: 'PRICE', sortOrder: 'INCREASING' }, + price_high_low: { sortBy: 'PRICE', sortOrder: 'DECREASING' }, + distance: { sortBy: 'LOCATION', sortOrder: 'INCREASING' }, +}; + +export const DEFAULT_SETTINGS: CleanplaatsSettings = { + removeTopAds: true, + removeDagtoppers: true, + removePromotedListings: true, + removeOpvalStickers: true, + removeReservedListings: false, + removeFavoriteRelatedAds: false, + sellerAgeWarningEnabled: false, + sellerAgeWarningThresholdValue: 3, + sellerAgeWarningThresholdUnit: 'days', + darkMode: false, + blacklistedSellers: [], + blacklistedTerms: [], + resultsPerPage: 30, + defaultSortMode: 'standard', + sortPreferenceSource: 'cleanplaats', +}; + +export const DEFAULT_PANEL_STATE: CleanplaatsPanelState = { + isCollapsed: false, + hasShownWelcomeToast: false, + lastSeenVersion: '', + activeView: 'filters', +}; + +export const MARKTPLAATS_SORT_LABEL_TO_MODE: Record = { + standaard: 'standard', + 'datum (nieuw-oud)': 'date_new_old', + 'datum (oud-nieuw)': 'date_old_new', + 'prijs (laag-hoog)': 'price_low_high', + 'prijs (hoog-laag)': 'price_high_low', + afstand: 'distance', +}; + +export const BLACKLISTED_TITLE_SELECTORS = [ + '.hz-StructuredListing-title', + '.hz-Listing-title', + '.hz-Listing-group--title-description', + '.hz-StructuredListing-body', + '[class*="ListingTitle_hz-Listing-title"]', + '[class*="ListingTitle_hz-StructuredListing-title"]', +].join(', '); diff --git a/src/shared/constants/sort.ts b/src/shared/constants/sort.ts new file mode 100644 index 0000000..802643c --- /dev/null +++ b/src/shared/constants/sort.ts @@ -0,0 +1,24 @@ +import type { CleanplaatsSortMode } from '@/shared/types/state'; + +export type SortTransform = { + sortBy: string; + sortOrder: 'DECREASING' | 'INCREASING'; +}; + +export const SORT_MODES: Record = { + standard: null, + date_new_old: { sortBy: 'SORT_INDEX', sortOrder: 'DECREASING' }, + date_old_new: { sortBy: 'SORT_INDEX', sortOrder: 'INCREASING' }, + price_low_high: { sortBy: 'PRICE', sortOrder: 'INCREASING' }, + price_high_low: { sortBy: 'PRICE', sortOrder: 'DECREASING' }, + distance: { sortBy: 'LOCATION', sortOrder: 'INCREASING' }, +}; + +export const SORT_LABEL_TO_MODE: Record = { + standaard: 'standard', + 'datum (nieuw-oud)': 'date_new_old', + 'datum (oud-nieuw)': 'date_old_new', + 'prijs (laag-hoog)': 'price_low_high', + 'prijs (hoog-laag)': 'price_high_low', + afstand: 'distance', +}; diff --git a/src/shared/constants/storage.ts b/src/shared/constants/storage.ts new file mode 100644 index 0000000..42a1a35 --- /dev/null +++ b/src/shared/constants/storage.ts @@ -0,0 +1,9 @@ +export const STORAGE_KEYS = { + settings: 'cleanplaatsSettings', + panelState: 'panelState', + firstRun: 'firstRun', +} as const; + +export const LOCAL_STORAGE_KEYS = { + darkMode: 'cleanplaats:darkMode', +} as const; diff --git a/src/shared/constants/update-notes.ts b/src/shared/constants/update-notes.ts new file mode 100644 index 0000000..1ae3f6c --- /dev/null +++ b/src/shared/constants/update-notes.ts @@ -0,0 +1,36 @@ +import type { UpdateNote } from '@/shared/types/state'; + +export const CLEANPLAATS_UPDATE_NOTES: Record = { + '2.0.7': { + intro: + 'Cleanplaats 2.0.7 voegt een extra veiligheidswaarschuwing toe op advertentiepagina’s en maakt het verbergen van verkopers duidelijker en handiger.', + highlights: [ + 'Je kunt nu een waarschuwing krijgen bij nieuwe verkoperaccounts. Deze instelling vind je onder het tabje "Voorkeuren" in het paneel, waar je zelf kiest vanaf hoeveel dagen, weken, maanden of jaren je zo’n melding wilt zien.', + 'Op advertentiepagina’s staat nu ook een knop onder de verkopernaam om in één keer alle advertenties van die verkoper te verbergen.', + 'De knop om een verkoper te verbergen is nu ook netjes vertaald op 2ememain.', + ], + note: 'Zie je een verkoper die je niet vertrouwt? Dan kun je die nu direct vanaf de advertentiepagina verbergen.', + }, + '2.0.6': { + intro: + 'Cleanplaats 2.0.6 herstelt een paar dingen op Favorieten en lost een vervelende fout op die sommige filters uit beeld haalde.', + highlights: [ + 'De filters voor categorie en afstand zijn weer terug waar ze horen.', + 'Gerelateerde advertenties in Favorieten worden niet meer standaard verborgen. Via de nieuwe knop "Voorkeuren" kun je dit nu zelf aan of uit zetten.', + 'Niet-beschikbare advertenties in Favorieten zien er in dark mode nu weer duidelijk anders uit dan actieve advertenties.', + ], + note: 'Excuses voor de bug waardoor categorie en afstand ineens konden verdwijnen. Bedankt aan iedereen die dit zo snel heeft gemeld via Reddit en GitHub issues. Jullie hulp en betrokkenheid maken Cleanplaats tot het succes dat het is.', + }, + '2.0.5': { + intro: + 'Cleanplaats 2.0.5 werkt Marktplaats verder bij met vooral meer dark mode-ondersteuning en een rustigere interface op meerdere pagina’s.', + highlights: [ + 'Dark mode is verder uitgebreid op onder meer "Mijn advertenties", account- en plaats advertentie-pagina’s, tabelweergaven en onderdelen rond eigen advertenties.', + 'Ook losse interface-elementen zoals "Deal gesloten?", voorstel- en leveringsmenu’s nemen nu beter het donkere thema over.', + 'Storende banners en promotieblokken zijn op meerdere plekken verborgen, waaronder "gerelateerde advertenties" in Favorieten.', + 'Een visuele flicker bij het laden in dark mode is aangepakt, waardoor pagina’s rustiger en consistenter openen.', + "Marktplaats banner voor 'koop je auto bij autobedrijven' weggehaald", + ], + note: "Zie je nog een onderdeel of licht onderdeel dat door de dark mode heen glipt in veel gebruikte pagina's? Meld het via GitHub issues in het paneel.", + }, +}; diff --git a/src/shared/messages/runtime.ts b/src/shared/messages/runtime.ts new file mode 100644 index 0000000..3b4149e --- /dev/null +++ b/src/shared/messages/runtime.ts @@ -0,0 +1,30 @@ +export const RUNTIME_MESSAGE_ACTIONS = { + keepAlive: 'keepAlive', + forceRefresh: 'forceRefresh', +} as const; + +export const KEEP_ALIVE_ACTION = RUNTIME_MESSAGE_ACTIONS.keepAlive; +export const FORCE_REFRESH_ACTION = RUNTIME_MESSAGE_ACTIONS.forceRefresh; + +export type RuntimeMessageAction = + (typeof RUNTIME_MESSAGE_ACTIONS)[keyof typeof RUNTIME_MESSAGE_ACTIONS]; + +export type RuntimeMessage = + | { + action: typeof RUNTIME_MESSAGE_ACTIONS.keepAlive; + } + | { + action: typeof RUNTIME_MESSAGE_ACTIONS.forceRefresh; + }; + +export type RuntimeMessageResponse = + | { + status: 'acknowledged'; + timestamp: number; + } + | { + status: 'refreshed'; + } + | { + status: 'ignored'; + }; diff --git a/src/shared/storage/repository.ts b/src/shared/storage/repository.ts new file mode 100644 index 0000000..162f45a --- /dev/null +++ b/src/shared/storage/repository.ts @@ -0,0 +1,144 @@ +import type { CleanplaatsPanelState, CleanplaatsSettings } from '@/shared/types/state'; +import { + LOCAL_STORAGE_KEYS, + STORAGE_KEYS, +} from '@/shared/constants/storage'; +import { + DEFAULT_PANEL_STATE, + DEFAULT_SETTINGS, +} from '@/shared/constants/settings'; +import { + clonePanelState, + cloneSettings, + normalizePanelState, + normalizeSettings, + normalizeStoredBoolean, + readBooleanString, +} from '@/shared/utils/settings-normalization'; +import { + parseStoredJson, + stringifyStoredJson, +} from '@/shared/utils/serialization'; + +const browserApi = browser; + +type LocalStorageLike = Pick; + +export type LoadedState = { + settings: CleanplaatsSettings; + panelState: CleanplaatsPanelState; + firstRun: boolean; +}; + +const readDarkModePreference = (storageLike?: LocalStorageLike): boolean | undefined => { + if (!storageLike) return undefined; + + try { + return readBooleanString(storageLike.getItem(LOCAL_STORAGE_KEYS.darkMode)); + } catch (error) { + console.warn('Cleanplaats: Failed reading dark mode from localStorage', error); + return undefined; + } +}; + +const persistDarkModePreference = (enabled: boolean, storageLike?: LocalStorageLike): void => { + if (!storageLike) return; + + try { + storageLike.setItem(LOCAL_STORAGE_KEYS.darkMode, enabled ? 'true' : 'false'); + } catch (error) { + console.warn('Cleanplaats: Failed writing dark mode to localStorage', error); + } +}; + +const getStorageItems = async (keys: string[]): Promise> => + browserApi.storage.local.get(keys) as Promise>; + +const setStorageItems = async (items: Record): Promise => { + await browserApi.storage.local.set(items); +}; + +export class SettingsRepository { + async load(storageLike: LocalStorageLike | undefined = window.localStorage): Promise { + const items = await getStorageItems([ + STORAGE_KEYS.settings, + STORAGE_KEYS.panelState, + STORAGE_KEYS.firstRun, + ]); + + const rawSettings = parseStoredJson>(items[STORAGE_KEYS.settings]); + const rawPanelState = parseStoredJson>(items[STORAGE_KEYS.panelState]); + + const settings = normalizeSettings(rawSettings); + const panelState = normalizePanelState(rawPanelState); + + const darkModeFromLocalStorage = readDarkModePreference(storageLike); + if (typeof darkModeFromLocalStorage === 'boolean') { + settings.darkMode = darkModeFromLocalStorage; + } + + const firstRun = normalizeStoredBoolean(items[STORAGE_KEYS.firstRun], true); + + return { + settings, + panelState, + firstRun, + }; + } + + async saveSettings( + settings: CleanplaatsSettings, + panelState: CleanplaatsPanelState, + storageLike: LocalStorageLike | undefined = window.localStorage, + ): Promise { + const normalizedSettings = normalizeSettings(settings); + const normalizedPanelState = normalizePanelState(panelState); + + persistDarkModePreference(Boolean(normalizedSettings.darkMode), storageLike); + + await setStorageItems({ + [STORAGE_KEYS.settings]: stringifyStoredJson(normalizedSettings), + [STORAGE_KEYS.panelState]: stringifyStoredJson(normalizedPanelState), + }); + } + + async markFirstRunCompleted(): Promise { + await setStorageItems({ + [STORAGE_KEYS.firstRun]: false, + }); + } + + async getRawSettingsValue(): Promise { + const items = await getStorageItems([STORAGE_KEYS.settings]); + const value = items[STORAGE_KEYS.settings]; + return typeof value === 'string' ? value : undefined; + } + + async getRawPanelStateValue(): Promise { + const items = await getStorageItems([STORAGE_KEYS.panelState]); + const value = items[STORAGE_KEYS.panelState]; + return typeof value === 'string' ? value : undefined; + } + + cloneDefaults(): LoadedState { + return { + settings: cloneSettings(DEFAULT_SETTINGS), + panelState: clonePanelState(DEFAULT_PANEL_STATE), + firstRun: true, + }; + } + + parseStorageSettingsValue(rawValue: unknown): CleanplaatsSettings { + const parsed = parseStoredJson>(rawValue); + return normalizeSettings(parsed); + } + + parseStoragePanelStateValue(rawValue: unknown): CleanplaatsPanelState { + const parsed = parseStoredJson>(rawValue); + return normalizePanelState(parsed); + } + + parseStorageFirstRunValue(rawValue: unknown): boolean { + return normalizeStoredBoolean(rawValue, true); + } +} diff --git a/src/shared/types/state.ts b/src/shared/types/state.ts new file mode 100644 index 0000000..cbea089 --- /dev/null +++ b/src/shared/types/state.ts @@ -0,0 +1,177 @@ +export type SortMode = + | 'standard' + | 'date_new_old' + | 'date_old_new' + | 'price_low_high' + | 'price_high_low' + | 'distance'; + +export type CleanplaatsSortMode = SortMode; + +export type SortModeConfig = { + sortBy: string; + sortOrder: 'DECREASING' | 'INCREASING'; +}; + +export type SortPreferenceSource = 'cleanplaats' | 'marketplace'; + +export type SellerAgeThresholdUnit = 'days' | 'weeks' | 'months' | 'years'; + +export interface CleanplaatsSettings { + removeTopAds: boolean; + removeDagtoppers: boolean; + removePromotedListings: boolean; + removeOpvalStickers: boolean; + removeReservedListings: boolean; + removeFavoriteRelatedAds: boolean; + sellerAgeWarningEnabled: boolean; + sellerAgeWarningThresholdValue: number; + sellerAgeWarningThresholdUnit: SellerAgeThresholdUnit; + darkMode: boolean; + blacklistedSellers: string[]; + blacklistedTerms: string[]; + resultsPerPage: 30 | 50 | 100; + defaultSortMode: CleanplaatsSortMode; + sortPreferenceSource: SortPreferenceSource; +} + +export interface CleanplaatsStats { + topAdsRemoved: number; + dagtoppersRemoved: number; + promotedListingsRemoved: number; + opvalStickersRemoved: number; + otherAdsRemoved: number; + totalRemoved: number; +} + +export interface CleanplaatsObservers { + mutation: MutationObserver | null; + ads: MutationObserver | null; + webchat: MutationObserver | null; + sellerAge: MutationObserver | null; +} + +export interface CleanplaatsRuntimeState { + lastSellerAgeWarningKey: string; + sellerAgeCheckTimer: number; +} + +export interface CleanplaatsFeatureFlags { + showStats: boolean; + autoCollapse: boolean; + firstRun: boolean; +} + +export interface CleanplaatsPanelState { + isCollapsed: boolean; + hasShownWelcomeToast: boolean; + lastSeenVersion: string; + activeView: 'filters' | 'preferences'; +} + +export interface CleanplaatsState { + settings: CleanplaatsSettings; + stats: CleanplaatsStats; + observers: CleanplaatsObservers; + runtime: CleanplaatsRuntimeState; + featureFlags: CleanplaatsFeatureFlags; + panelState: CleanplaatsPanelState; +} + +export interface ReviewCtaConfig { + linkLabel: string; + url: string; +} + +export interface CleanplaatsLocaleText { + feedbackLabel: string; + feedbackText: string; + feedbackAriaLabel: string; + reviewAriaLabel: (linkLabel: string) => string; + supportTitle: string; + supportButton: string; + optionsTitle: string; + topAdLabel: string; + topAdTooltip: string; + topAdTooltipTwh: string; + dagtoppersLabel: string; + dagtoppersTooltip: string; + promotedListingsLabel: string; + promotedListingsTooltip: string; + stickersLabel: string; + stickersTooltip: string; + reservedLabel: string; + reservedTooltip: string; + favoriteRelatedAdsLabel: string; + favoriteRelatedAdsTooltip: string; + sellerAgeWarningLabel: string; + sellerAgeWarningTooltip: string; + sellerAgeWarningThresholdLabel: string; + sellerAgeWarningThresholdValueAriaLabel: string; + sellerAgeWarningThresholdUnitAriaLabel: string; + sellerAgeWarningThresholdUnits: Record; + sellerAgeWarningToastTitle: string; + sellerAgeWarningToastMessage: ( + sellerName: string, + sellerAgeText: string, + thresholdLabel: string, + ) => string; + preferencesLabel: string; + backLabel: string; + preferencesIntro: string; + darkModeLabel: string; + darkModeTooltip: string; + resultsPerPageLabel: string; + defaultSortLabel: string; + sortOptions: Record; + statsTitle: string; + statsTop: string; + statsDagtoppers: string; + statsBusiness: string; + statsStickers: string; + statsOther: string; + statsTotal: string; + manageTerms: string; + manageSellers: string; + termsModalTitle: string; + termsEmpty: string; + hiddenButton: string; + unhideButton: string; + termInputPlaceholder: string; + termInputHelp: string; + addButton: string; + closeButton: string; + sellersModalTitle: string; + sellersEmpty: string; + sellerInputPlaceholder: string; + sellerInputHelp: string; + hideSellerButton: string; + hiddenSellerButton: string; + hideSellerButtonAriaLabel: string; + blacklistToastHint: string; + blacklistToastHiddenSuffix: string; + blacklistToastHiddenPluralSuffix: string; + blacklistToastShownSuffix: string; + blacklistToastShownHint: string; + termToastHidden: (term: string) => string; + termToastShown: (term: string) => string; +} + +export type LocaleText = CleanplaatsLocaleText; + +export interface SellerAgeInfo { + sellerName: string; + sellerAgeText: string; + sellerAgeDays: number; +} + +export interface UpdateNote { + intro: string; + highlights: string[]; + note: string; +} + +export type UpdateNotes = Record; + +export type RuntimeResponseStatus = 'acknowledged' | 'refreshed' | 'ignored'; + diff --git a/src/shared/utils/selectors.ts b/src/shared/utils/selectors.ts new file mode 100644 index 0000000..7de1e09 --- /dev/null +++ b/src/shared/utils/selectors.ts @@ -0,0 +1,9 @@ +import { BLACKLISTED_TITLE_SELECTORS } from '@/shared/constants/settings'; + +export function getListingTitleElement(container: Element): Element | null { + return container.querySelector(BLACKLISTED_TITLE_SELECTORS); +} + +export function getListingTitleText(container: Element): string { + return getListingTitleElement(container)?.textContent?.trim().toLowerCase() ?? ''; +} diff --git a/src/shared/utils/seller-age.ts b/src/shared/utils/seller-age.ts new file mode 100644 index 0000000..d9218c1 --- /dev/null +++ b/src/shared/utils/seller-age.ts @@ -0,0 +1,56 @@ +import type { SellerAgeThresholdUnit } from '@/shared/types/state'; + +export const normalizeSellerAgeText = (text: string): string => + text.trim().toLowerCase().replace(/\s+/g, ' '); + +const SELLER_AGE_REGEX = + /(\d+)\s+(dag|dagen|day|days|jour|jours|week|weken|semaine|semaines|maand|maanden|month|months|mois|jaar|jaren|year|years|an|ans)\b/; + +export const parseSellerAgeToDays = (input: string): number | null => { + const normalized = normalizeSellerAgeText(input); + const match = normalized.match(SELLER_AGE_REGEX); + if (!match) return null; + + const amountRaw = match[1]; + const unit = match[2] ?? ''; + if (!amountRaw || !unit) return null; + + const amount = Number.parseInt(amountRaw, 10); + if (!Number.isFinite(amount) || amount < 0) return null; + + if (['dag', 'dagen', 'day', 'days', 'jour', 'jours'].includes(unit)) { + return amount; + } + + if (['week', 'weken', 'semaine', 'semaines'].includes(unit)) { + return amount * 7; + } + + if (['maand', 'maanden', 'month', 'months', 'mois'].includes(unit)) { + return amount * 30; + } + + if (['jaar', 'jaren', 'year', 'years', 'an', 'ans'].includes(unit)) { + return amount * 365; + } + + return null; +}; + +export const thresholdToDays = ( + value: number, + unit: SellerAgeThresholdUnit, +): number => { + const normalizedValue = Math.max(1, Number.isFinite(value) ? Math.trunc(value) : 1); + switch (unit) { + case 'days': + return normalizedValue; + case 'weeks': + return normalizedValue * 7; + case 'years': + return normalizedValue * 365; + case 'months': + default: + return normalizedValue * 30; + } +}; diff --git a/src/shared/utils/serialization.ts b/src/shared/utils/serialization.ts new file mode 100644 index 0000000..34f22c8 --- /dev/null +++ b/src/shared/utils/serialization.ts @@ -0,0 +1,45 @@ +export function parseJsonRecord(input: unknown): Record { + if (input == null) return {}; + + if (typeof input === 'string') { + try { + const parsed = JSON.parse(input); + return typeof parsed === 'object' && parsed !== null + ? (parsed as Record) + : {}; + } catch { + return {}; + } + } + + if (typeof input === 'object') { + return input as Record; + } + + return {}; +} + +export function parseStoredJson(input: unknown): T | undefined { + if (input == null) { + return undefined; + } + + if (typeof input === 'string') { + try { + const parsed = JSON.parse(input) as T; + return parsed; + } catch { + return undefined; + } + } + + if (typeof input === 'object') { + return input as T; + } + + return undefined; +} + +export function stringifyStoredJson(value: unknown): string { + return JSON.stringify(value); +} diff --git a/src/shared/utils/settings-normalization.ts b/src/shared/utils/settings-normalization.ts new file mode 100644 index 0000000..800ae5e --- /dev/null +++ b/src/shared/utils/settings-normalization.ts @@ -0,0 +1,150 @@ +import { DEFAULT_PANEL_STATE, DEFAULT_SETTINGS } from '@/shared/constants/settings'; +import type { + CleanplaatsPanelState, + CleanplaatsSettings, + CleanplaatsSortMode, + SellerAgeThresholdUnit, + SortPreferenceSource, +} from '@/shared/types/state'; + +const VALID_SORT_MODES = new Set([ + 'standard', + 'date_new_old', + 'date_old_new', + 'price_low_high', + 'price_high_low', + 'distance', +]); + +const VALID_SORT_SOURCES = new Set(['cleanplaats', 'marketplace']); +const VALID_THRESHOLD_UNITS = new Set([ + 'days', + 'weeks', + 'months', + 'years', +]); + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value + .filter((entry): entry is string => typeof entry === 'string') + .map((entry) => entry.trim()) + .filter(Boolean); +} + +function toBoolean(value: unknown, fallback: boolean): boolean { + return typeof value === 'boolean' ? value : fallback; +} + +function toInteger(value: unknown, fallback: number, min: number, max: number): number { + const parsed = Number.parseInt(String(value), 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.min(max, Math.max(min, parsed)); +} + +function toResultsPerPage(value: unknown, fallback: CleanplaatsSettings['resultsPerPage']): CleanplaatsSettings['resultsPerPage'] { + const parsed = Number.parseInt(String(value), 10); + if (parsed === 30 || parsed === 50 || parsed === 100) { + return parsed; + } + return fallback; +} + +export function readBooleanString(value: string | null | undefined): boolean | undefined { + if (value === 'true') return true; + if (value === 'false') return false; + return undefined; +} + +export function normalizeSettings(raw: Partial | null | undefined): CleanplaatsSettings { + if (!raw || typeof raw !== 'object') { + return { ...DEFAULT_SETTINGS }; + } + + const defaultSortMode = VALID_SORT_MODES.has(raw.defaultSortMode as CleanplaatsSortMode) + ? (raw.defaultSortMode as CleanplaatsSortMode) + : DEFAULT_SETTINGS.defaultSortMode; + + const sortPreferenceSource = VALID_SORT_SOURCES.has(raw.sortPreferenceSource as SortPreferenceSource) + ? (raw.sortPreferenceSource as SortPreferenceSource) + : DEFAULT_SETTINGS.sortPreferenceSource; + + const thresholdUnit = VALID_THRESHOLD_UNITS.has(raw.sellerAgeWarningThresholdUnit as SellerAgeThresholdUnit) + ? (raw.sellerAgeWarningThresholdUnit as SellerAgeThresholdUnit) + : DEFAULT_SETTINGS.sellerAgeWarningThresholdUnit; + + return { + ...DEFAULT_SETTINGS, + removeTopAds: toBoolean(raw.removeTopAds, DEFAULT_SETTINGS.removeTopAds), + removeDagtoppers: toBoolean(raw.removeDagtoppers, DEFAULT_SETTINGS.removeDagtoppers), + removePromotedListings: toBoolean(raw.removePromotedListings, DEFAULT_SETTINGS.removePromotedListings), + removeOpvalStickers: toBoolean(raw.removeOpvalStickers, DEFAULT_SETTINGS.removeOpvalStickers), + removeReservedListings: toBoolean(raw.removeReservedListings, DEFAULT_SETTINGS.removeReservedListings), + removeFavoriteRelatedAds: toBoolean(raw.removeFavoriteRelatedAds, DEFAULT_SETTINGS.removeFavoriteRelatedAds), + sellerAgeWarningEnabled: toBoolean( + raw.sellerAgeWarningEnabled, + DEFAULT_SETTINGS.sellerAgeWarningEnabled, + ), + sellerAgeWarningThresholdValue: toInteger( + raw.sellerAgeWarningThresholdValue, + DEFAULT_SETTINGS.sellerAgeWarningThresholdValue, + 1, + 99, + ), + sellerAgeWarningThresholdUnit: thresholdUnit, + darkMode: toBoolean(raw.darkMode, DEFAULT_SETTINGS.darkMode), + blacklistedSellers: asStringArray(raw.blacklistedSellers), + blacklistedTerms: asStringArray(raw.blacklistedTerms), + resultsPerPage: toResultsPerPage(raw.resultsPerPage, DEFAULT_SETTINGS.resultsPerPage), + defaultSortMode, + sortPreferenceSource, + }; +} + +export function normalizePanelState( + raw: Partial | null | undefined, +): CleanplaatsPanelState { + if (!raw || typeof raw !== 'object') { + return { ...DEFAULT_PANEL_STATE }; + } + + const activeView = + raw.activeView === 'preferences' || raw.activeView === 'filters' + ? raw.activeView + : DEFAULT_PANEL_STATE.activeView; + + return { + ...DEFAULT_PANEL_STATE, + isCollapsed: toBoolean(raw.isCollapsed, DEFAULT_PANEL_STATE.isCollapsed), + hasShownWelcomeToast: toBoolean( + raw.hasShownWelcomeToast, + DEFAULT_PANEL_STATE.hasShownWelcomeToast, + ), + lastSeenVersion: + typeof raw.lastSeenVersion === 'string' + ? raw.lastSeenVersion + : DEFAULT_PANEL_STATE.lastSeenVersion, + activeView, + }; +} + +export function cloneSettings(input: CleanplaatsSettings): CleanplaatsSettings { + return { + ...input, + blacklistedSellers: [...input.blacklistedSellers], + blacklistedTerms: [...input.blacklistedTerms], + }; +} + +export function clonePanelState(input: CleanplaatsPanelState): CleanplaatsPanelState { + return { + ...input, + }; +} + +export function normalizeStoredBoolean(value: unknown, fallback: boolean): boolean { + if (typeof value === 'boolean') { + return value; + } + return fallback; +} diff --git a/content.css b/src/styles/content.css similarity index 99% rename from content.css rename to src/styles/content.css index 62e70e7..eac8bc2 100644 --- a/content.css +++ b/src/styles/content.css @@ -26,6 +26,7 @@ .cleanplaats-panel:not(.collapsed) { min-width: 250px; padding: 0; + background-image: none !important; } /* Collapsed state */ diff --git a/dark-mode.css b/src/styles/dark-mode.css similarity index 100% rename from dark-mode.css rename to src/styles/dark-mode.css diff --git a/src/types/assets.d.ts b/src/types/assets.d.ts new file mode 100644 index 0000000..1c5760d --- /dev/null +++ b/src/types/assets.d.ts @@ -0,0 +1,8 @@ +declare module '*.css'; + +/** Allow bundled asset paths (icons, etc.) with `browser.runtime.getURL`. */ +declare module 'wxt/browser' { + export interface WxtRuntime { + getURL(path: string): string; + } +} diff --git a/tests/background/hash-url.test.ts b/tests/background/hash-url.test.ts new file mode 100644 index 0000000..4f2271c --- /dev/null +++ b/tests/background/hash-url.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; +import { + buildHashOptions, + getModifiedUrlIfNeeded, + parseHashOptions, +} from '../../src/background/services/hash-url'; + +describe('background/hash-url', () => { + it('parses hash options into a key/value map', () => { + const parsed = parseHashOptions('#limit:50|sortBy:PRICE|sortOrder:INCREASING'); + expect(parsed).toEqual({ + limit: '50', + sortBy: 'PRICE', + sortOrder: 'INCREASING', + }); + }); + + it('builds hash options from map entries', () => { + const hash = buildHashOptions({ + limit: '100', + sortBy: 'PRICE', + sortOrder: 'DECREASING', + }); + + expect(hash).toBe('#limit:100|sortBy:PRICE|sortOrder:DECREASING'); + }); + + it('adds missing limit parameter', () => { + const result = getModifiedUrlIfNeeded({ + urlString: 'https://www.marktplaats.nl/l/auto-s/#', + resultsPerPage: '50', + defaultSortMode: 'standard', + sortPreferenceSource: 'cleanplaats', + }); + + expect(result).toBeTruthy(); + const parsed = parseHashOptions(new URL(result!).hash); + expect(parsed.limit).toBe('50'); + }); + + it('adds configured sort when cleanplaats controls sort mode', () => { + const result = getModifiedUrlIfNeeded({ + urlString: 'https://www.marktplaats.nl/l/auto-s/#limit:50', + resultsPerPage: '50', + defaultSortMode: 'price_low_high', + sortPreferenceSource: 'cleanplaats', + }); + + expect(result).toBeTruthy(); + const parsed = parseHashOptions(new URL(result!).hash); + expect(parsed.sortBy).toBe('PRICE'); + expect(parsed.sortOrder).toBe('INCREASING'); + }); + + it('removes explicit sort when default sort mode is standard', () => { + const result = getModifiedUrlIfNeeded({ + urlString: + 'https://www.marktplaats.nl/l/auto-s/#limit:30|sortBy:PRICE|sortOrder:INCREASING', + resultsPerPage: '30', + defaultSortMode: 'standard', + sortPreferenceSource: 'cleanplaats', + }); + + expect(result).toBeTruthy(); + const parsed = parseHashOptions(new URL(result!).hash); + expect(parsed.limit).toBe('30'); + expect(parsed.sortBy).toBeUndefined(); + expect(parsed.sortOrder).toBeUndefined(); + }); + + it('returns null when no rewrite is needed', () => { + const result = getModifiedUrlIfNeeded({ + urlString: 'https://www.marktplaats.nl/l/auto-s/#limit:30', + resultsPerPage: '30', + defaultSortMode: 'standard', + sortPreferenceSource: 'cleanplaats', + }); + + expect(result).toBeNull(); + }); +}); diff --git a/tests/content/sort-utils.test.ts b/tests/content/sort-utils.test.ts new file mode 100644 index 0000000..97db53d --- /dev/null +++ b/tests/content/sort-utils.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { + getSortModeFromLabel, + normalizeSortLabel, +} from '@/content/utils/sort'; + +describe('content sort utils', () => { + it('normalizes sort labels', () => { + expect(normalizeSortLabel(' Datum (nieuw-oud) ')).toBe('datum (nieuw-oud)'); + }); + + it('maps known labels to sort mode', () => { + expect(getSortModeFromLabel('Standaard')).toBe('standard'); + expect(getSortModeFromLabel('Datum (nieuw-oud)')).toBe('date_new_old'); + expect(getSortModeFromLabel('Datum (oud-nieuw)')).toBe('date_old_new'); + expect(getSortModeFromLabel('Prijs (laag-hoog)')).toBe('price_low_high'); + expect(getSortModeFromLabel('Prijs (hoog-laag)')).toBe('price_high_low'); + expect(getSortModeFromLabel('Afstand')).toBe('distance'); + }); + + it('returns null for unknown labels', () => { + expect(getSortModeFromLabel('Onbekend')).toBeNull(); + }); +}); diff --git a/tests/shared/seller-age.test.ts b/tests/shared/seller-age.test.ts new file mode 100644 index 0000000..5491a02 --- /dev/null +++ b/tests/shared/seller-age.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { parseSellerAgeToDays, thresholdToDays } from '@/shared/utils/seller-age'; + +describe('seller age parsing', () => { + it('parses dutch, french and english units', () => { + expect(parseSellerAgeToDays('3 dagen op Marktplaats')).toBe(3); + expect(parseSellerAgeToDays('2 semaines')).toBe(14); + expect(parseSellerAgeToDays('1 year active')).toBe(365); + }); + + it('returns null for unparseable text', () => { + expect(parseSellerAgeToDays('nieuw account')).toBeNull(); + expect(parseSellerAgeToDays('')).toBeNull(); + }); + + it('converts threshold to days', () => { + expect(thresholdToDays(2, 'days')).toBe(2); + expect(thresholdToDays(2, 'weeks')).toBe(14); + expect(thresholdToDays(2, 'months')).toBe(60); + expect(thresholdToDays(2, 'years')).toBe(730); + }); +}); diff --git a/tests/shared/storage-serialization.test.ts b/tests/shared/storage-serialization.test.ts new file mode 100644 index 0000000..1678699 --- /dev/null +++ b/tests/shared/storage-serialization.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { parseStoredJson, stringifyStoredJson } from '@/shared/utils/serialization'; + +describe('parseStoredJson', () => { + it('parses JSON string values', () => { + const parsed = parseStoredJson<{ darkMode: boolean }>('{"darkMode":true}'); + expect(parsed).toEqual({ darkMode: true }); + }); + + it('returns object input unchanged as typed value', () => { + const parsed = parseStoredJson<{ resultsPerPage: number }>({ resultsPerPage: 50 }); + expect(parsed).toEqual({ resultsPerPage: 50 }); + }); + + it('returns undefined for invalid JSON strings', () => { + const parsed = parseStoredJson<{ foo: string }>('{invalid'); + expect(parsed).toBeUndefined(); + }); + + it('returns undefined for nullish values', () => { + expect(parseStoredJson(null)).toBeUndefined(); + expect(parseStoredJson(undefined)).toBeUndefined(); + }); +}); + +describe('stringifyStoredJson', () => { + it('serializes values to JSON strings', () => { + expect(stringifyStoredJson({ removeTopAds: true })).toBe('{"removeTopAds":true}'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0cdad0c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./.wxt/tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true + }, + "include": [ + "entrypoints", + "src", + ".wxt/wxt.d.ts", + "tests", + "wxt.config.ts" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..9054307 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['tests/**/*.test.ts'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@@': path.resolve(__dirname, '.'), + '~': path.resolve(__dirname, './src'), + '~~': path.resolve(__dirname, '.'), + }, + }, +}); diff --git a/wxt.config.ts b/wxt.config.ts new file mode 100644 index 0000000..21878ac --- /dev/null +++ b/wxt.config.ts @@ -0,0 +1,55 @@ +import { defineConfig } from 'wxt'; + +const HOST_MATCH_PATTERNS = [ + '*://*.marktplaats.nl/*', + '*://*.2dehands.be/*', + '*://*.2ememain.be/*', +]; + +export default defineConfig({ + modules: ['@wxt-dev/module-react'], + srcDir: 'src', + manifestVersion: 3, + manifest: { + name: 'Cleanplaats - Marktplaats zonder spam', + version: '2.0.7', + description: 'Zelf in de hand wat je wel én niet wil zien op Marktplaats door te filteren', + permissions: [ + 'storage', + 'scripting', + 'tabs', + 'webNavigation', + 'declarativeNetRequest', + 'alarms', + ], + host_permissions: HOST_MATCH_PATTERNS, + action: { + default_title: 'Cleanplaats', + default_icon: { + '16': 'icons/icon16.png', + '48': 'icons/icon48.png', + '128': 'icons/icon128.png', + }, + }, + icons: { + '16': 'icons/icon16.png', + '48': 'icons/icon48.png', + '128': 'icons/icon128.png', + }, + web_accessible_resources: [ + { + resources: ['icons/*'], + matches: HOST_MATCH_PATTERNS, + }, + ], + browser_specific_settings: { + gecko: { + id: 'cleanplaats@cleanplaats.dev', + strict_min_version: '121.0', + }, + gecko_android: { + strict_min_version: '121.0', + }, + }, + }, +});