// ==UserScript== // @name Web Search Navigator // @version 0.5.2 // @description Keyboard shortcuts for Google search, YouTube, Startpage, Brave Search, Google Scholar, Github, and Amazon. // @namespace web-search-navigator // @author Converter Script // @match *://www.google.com/search* // @match *://www.google.ad/search* // @match *://www.google.ae/search* // @match *://www.google.com.af/search* // @match *://www.google.com.ag/search* // @match *://www.google.com.ai/search* // @match *://www.google.al/search* // @match *://www.google.am/search* // @match *://www.google.co.ao/search* // @match *://www.google.com.ar/search* // @match *://www.google.as/search* // @match *://www.google.at/search* // @match *://www.google.com.au/search* // @match *://www.google.az/search* // @match *://www.google.ba/search* // @match *://www.google.com.bd/search* // @match *://www.google.be/search* // @match *://www.google.bf/search* // @match *://www.google.bg/search* // @match *://www.google.com.bh/search* // @match *://www.google.bi/search* // @match *://www.google.bj/search* // @match *://www.google.com.bn/search* // @match *://www.google.com.bo/search* // @match *://www.google.com.br/search* // @match *://www.google.bs/search* // @match *://www.google.bt/search* // @match *://www.google.co.bw/search* // @match *://www.google.by/search* // @match *://www.google.com.bz/search* // @match *://www.google.ca/search* // @match *://www.google.cd/search* // @match *://www.google.cf/search* // @match *://www.google.cg/search* // @match *://www.google.ch/search* // @match *://www.google.ci/search* // @match *://www.google.co.ck/search* // @match *://www.google.cl/search* // @match *://www.google.cm/search* // @match *://www.google.cn/search* // @match *://www.google.com.co/search* // @match *://www.google.co.cr/search* // @match *://www.google.com.cu/search* // @match *://www.google.cv/search* // @match *://www.google.com.cy/search* // @match *://www.google.cz/search* // @match *://www.google.de/search* // @match *://www.google.dj/search* // @match *://www.google.dk/search* // @match *://www.google.dm/search* // @match *://www.google.com.do/search* // @match *://www.google.dz/search* // @match *://www.google.com.ec/search* // @match *://www.google.ee/search* // @match *://www.google.com.eg/search* // @match *://www.google.es/search* // @match *://www.google.com.et/search* // @match *://www.google.fi/search* // @match *://www.google.com.fj/search* // @match *://www.google.fm/search* // @match *://www.google.fr/search* // @match *://www.google.ga/search* // @match *://www.google.ge/search* // @match *://www.google.gg/search* // @match *://www.google.com.gh/search* // @match *://www.google.com.gi/search* // @match *://www.google.gl/search* // @match *://www.google.gm/search* // @match *://www.google.gp/search* // @match *://www.google.gr/search* // @match *://www.google.com.gt/search* // @match *://www.google.gy/search* // @match *://www.google.com.hk/search* // @match *://www.google.hn/search* // @match *://www.google.hr/search* // @match *://www.google.ht/search* // @match *://www.google.hu/search* // @match *://www.google.co.id/search* // @match *://www.google.ie/search* // @match *://www.google.co.il/search* // @match *://www.google.im/search* // @match *://www.google.co.in/search* // @match *://www.google.iq/search* // @match *://www.google.is/search* // @match *://www.google.it/search* // @match *://www.google.je/search* // @match *://www.google.com.jm/search* // @match *://www.google.jo/search* // @match *://www.google.co.jp/search* // @match *://www.google.co.ke/search* // @match *://www.google.com.kh/search* // @match *://www.google.ki/search* // @match *://www.google.kg/search* // @match *://www.google.co.kr/search* // @match *://www.google.com.kw/search* // @match *://www.google.kz/search* // @match *://www.google.la/search* // @match *://www.google.com.lb/search* // @match *://www.google.li/search* // @match *://www.google.lk/search* // @match *://www.google.co.ls/search* // @match *://www.google.lt/search* // @match *://www.google.lu/search* // @match *://www.google.lv/search* // @match *://www.google.com.ly/search* // @match *://www.google.co.ma/search* // @match *://www.google.md/search* // @match *://www.google.me/search* // @match *://www.google.mg/search* // @match *://www.google.mk/search* // @match *://www.google.ml/search* // @match *://www.google.com.mm/search* // @match *://www.google.mn/search* // @match *://www.google.ms/search* // @match *://www.google.com.mt/search* // @match *://www.google.mu/search* // @match *://www.google.mv/search* // @match *://www.google.mw/search* // @match *://www.google.com.mx/search* // @match *://www.google.com.my/search* // @match *://www.google.co.mz/search* // @match *://www.google.com.na/search* // @match *://www.google.com.nf/search* // @match *://www.google.com.ng/search* // @match *://www.google.com.ni/search* // @match *://www.google.ne/search* // @match *://www.google.nl/search* // @match *://www.google.no/search* // @match *://www.google.com.np/search* // @match *://www.google.nr/search* // @match *://www.google.nu/search* // @match *://www.google.co.nz/search* // @match *://www.google.com.om/search* // @match *://www.google.com.pa/search* // @match *://www.google.com.pe/search* // @match *://www.google.com.pg/search* // @match *://www.google.com.ph/search* // @match *://www.google.com.pk/search* // @match *://www.google.pl/search* // @match *://www.google.pn/search* // @match *://www.google.com.pr/search* // @match *://www.google.ps/search* // @match *://www.google.pt/search* // @match *://www.google.com.py/search* // @match *://www.google.com.qa/search* // @match *://www.google.ro/search* // @match *://www.google.ru/search* // @match *://www.google.rw/search* // @match *://www.google.com.sa/search* // @match *://www.google.com.sb/search* // @match *://www.google.sc/search* // @match *://www.google.se/search* // @match *://www.google.com.sg/search* // @match *://www.google.sh/search* // @match *://www.google.si/search* // @match *://www.google.sk/search* // @match *://www.google.com.sl/search* // @match *://www.google.sn/search* // @match *://www.google.so/search* // @match *://www.google.sm/search* // @match *://www.google.sr/search* // @match *://www.google.st/search* // @match *://www.google.com.sv/search* // @match *://www.google.td/search* // @match *://www.google.tg/search* // @match *://www.google.co.th/search* // @match *://www.google.com.tj/search* // @match *://www.google.tk/search* // @match *://www.google.tl/search* // @match *://www.google.tm/search* // @match *://www.google.tn/search* // @match *://www.google.to/search* // @match *://www.google.com.tr/search* // @match *://www.google.tt/search* // @match *://www.google.com.tw/search* // @match *://www.google.co.tz/search* // @match *://www.google.com.ua/search* // @match *://www.google.co.ug/search* // @match *://www.google.co.uk/search* // @match *://www.google.com.uy/search* // @match *://www.google.co.uz/search* // @match *://www.google.com.vc/search* // @match *://www.google.co.ve/search* // @match *://www.google.vg/search* // @match *://www.google.co.vi/search* // @match *://www.google.com.vn/search* // @match *://www.google.vu/search* // @match *://www.google.ws/search* // @match *://www.google.rs/search* // @match *://www.google.co.za/search* // @match *://www.google.co.zm/search* // @match *://www.google.co.zw/search* // @match *://www.google.cat/search* // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_openInTab // @icon data:image/png;base64,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 // @run-at document-end // ==/UserScript== console.log("Script start:",performance.now());const e=!0,t=e=>e,o="passthrough";let s,c={createHTML:t,createScript:t,createScriptURL:t},i=!1;const r=()=>{try{void 0!==window.isSecureContext&&window.isSecureContext&&window.trustedTypes&&window.trustedTypes.createPolicy&&(i=!0,trustedTypes.defaultPolicy?(l("TT Default Policy exists"),c=window.trustedTypes.createPolicy("default",c),s=trustedTypes.defaultPolicy,l(`Created custom passthrough policy, in case the default policy is too restrictive: Use Policy '${o}' in var 'TTP':`,c)):s=c=window.trustedTypes.createPolicy("default",c),l("Trusted-Type Policies: TTP:",c,"TTP_default:",s))}catch(e){l(e)}},l=(...e)=>{console.log(...e)};r(); (function() { // #region Logging const SCRIPT_NAME = "Web Search Navigator"; const _log = (...args) => {}; const _warn = (...args) => console.warn(`[${typeof SCRIPT_NAME === 'string' ? SCRIPT_NAME : '[USERSCRIPT_CONVERTED]'}]`, ...args); const _error = (...args) => { let e = args[0]; console.error(`[${typeof SCRIPT_NAME === 'string' ? SCRIPT_NAME : '[USERSCRIPT_CONVERTED]'}]`, ...args); } // #endregion // #region Unified Polyfill // #region Messaging implementation function createEventBus( scopeId, type = "page", // "page" or "iframe" { allowedOrigin = "*", children = [], parentWindow = null } = {} ) { if (!scopeId) throw new Error("createEventBus requires a scopeId"); const handlers = {}; function handleIncoming(ev) { if (allowedOrigin !== "*" && ev.origin !== allowedOrigin) return; const msg = ev.data; if (!msg || msg.__eventBus !== true || msg.scopeId !== scopeId) return; const { event, payload } = msg; // PAGE: if it's an INIT from an iframe, adopt it if (type === "page" && event === "__INIT__") { const win = ev.source; if (win && !children.includes(win)) { children.push(win); } return; } (handlers[event] || []).forEach((fn) => fn(payload, { origin: ev.origin, source: ev.source }) ); } window.addEventListener("message", handleIncoming); function emitTo(win, event, payload) { const envelope = { __eventBus: true, scopeId, event, payload, }; win.postMessage(envelope, allowedOrigin); } // IFRAME: announce to page on startup if (type === "iframe") { setTimeout(() => { const pw = parentWindow || window.parent; if (pw && pw.postMessage) { emitTo(pw, "__INIT__", null); } }, 0); } return { on(event, fn) { handlers[event] = handlers[event] || []; handlers[event].push(fn); }, off(event, fn) { if (!handlers[event]) return; handlers[event] = handlers[event].filter((h) => h !== fn); }, /** * Emits an event. * @param {string} event - The event name. * @param {any} payload - The event payload. * @param {object} [options] - Emission options. * @param {Window} [options.to] - A specific window to target. If provided, message is ONLY sent to the target. */ emit(event, payload, { to } = {}) { // If a specific target window is provided, send only to it and DO NOT dispatch locally. // This prevents a port from receiving its own messages. if (to) { if (to && typeof to.postMessage === "function") { emitTo(to, event, payload); } return; // Exit after targeted send. } // For broadcast messages (no 'to' target), dispatch locally first. (handlers[event] || []).forEach((fn) => fn(payload, { origin: location.origin, source: window }) ); // Then propagate the broadcast to other windows. if (type === "page") { children.forEach((win) => emitTo(win, event, payload)); } else { const pw = parentWindow || window.parent; if (pw && pw.postMessage) { emitTo(pw, event, payload); } } }, }; } function createRuntime(type = "background", bus) { let nextId = 1; const pending = {}; const msgListeners = []; let nextPortId = 1; const ports = {}; const onConnectListeners = []; function parseArgs(args) { let target, message, options, callback; const arr = [...args]; if (arr.length === 0) { throw new Error("sendMessage requires at least one argument"); } if (arr.length === 1) { return { message: arr[0] }; } // last object could be options if ( arr.length && typeof arr[arr.length - 1] === "object" && !Array.isArray(arr[arr.length - 1]) ) { options = arr.pop(); } // last function is callback if (arr.length && typeof arr[arr.length - 1] === "function") { callback = arr.pop(); } if ( arr.length === 2 && (typeof arr[0] === "string" || typeof arr[0] === "number") ) { [target, message] = arr; } else { [message] = arr; } return { target, message, options, callback }; } if (type === "background") { bus.on("__REQUEST__", ({ id, message }, { source }) => { let responded = false, isAsync = false; function sendResponse(resp) { if (responded) return; responded = true; // Target the response directly back to the window that sent the request. bus.emit("__RESPONSE__", { id, response: resp }, { to: source }); } const results = msgListeners .map((fn) => { try { // msg, sender, sendResponse const ret = fn(message, { id, tab: { id: source } }, sendResponse); if (ret === true || (ret && typeof ret.then === "function")) { isAsync = true; return ret; } return ret; } catch (e) { _error(e); } }) .filter((r) => r !== undefined); const promises = results.filter((r) => r && typeof r.then === "function"); if (!isAsync && promises.length === 0) { const out = results.length === 1 ? results[0] : results; sendResponse(out); } else if (promises.length) { Promise.all(promises).then((vals) => { if (!responded) { const out = vals.length === 1 ? vals[0] : vals; sendResponse(out); } }); } }); } if (type !== "background") { bus.on("__RESPONSE__", ({ id, response }) => { const entry = pending[id]; if (!entry) return; entry.resolve(response); if (entry.callback) entry.callback(response); delete pending[id]; }); } function sendMessage(...args) { // Background should be able to send message to itself // if (type === "background") { // throw new Error("Background cannot sendMessage to itself"); // } const { target, message, callback } = parseArgs(args); const id = nextId++; const promise = new Promise((resolve) => { pending[id] = { resolve, callback }; bus.emit("__REQUEST__", { id, message }); }); return promise; } bus.on("__PORT_CONNECT__", ({ portId, name }, { source }) => { if (type !== "background") return; const backgroundPort = makePort("background", portId, name, source); ports[portId] = backgroundPort; onConnectListeners.forEach((fn) => fn(backgroundPort)); // send back a CONNECT_ACK so the client can // start listening on its end: bus.emit("__PORT_CONNECT_ACK__", { portId, name }, { to: source }); }); // Clients handle the ACK and finalize their Port object by learning the remote window. bus.on("__PORT_CONNECT_ACK__", ({ portId, name }, { source }) => { if (type === "background") return; // ignore const p = ports[portId]; if (!p) return; // Call the port's internal finalize method to complete the handshake if (p._finalize) { p._finalize(source); } }); // Any port message travels via "__PORT_MESSAGE__" bus.on("__PORT_MESSAGE__", (envelope, { source }) => { const { portId } = envelope; const p = ports[portId]; if (!p) return; p._receive(envelope, source); }); // Any port disconnect: bus.on("__PORT_DISCONNECT__", ({ portId }) => { const p = ports[portId]; if (!p) return; p._disconnect(); delete ports[portId]; }); // Refactored makePort to correctly manage internal state and the connection handshake. function makePort(side, portId, name, remoteWindow) { let onMessageHandlers = []; let onDisconnectHandlers = []; let buffer = []; // Unique instance ID for this port instance const instanceId = Math.random().toString(36).slice(2) + Date.now(); // These state variables are part of the closure and are updated by _finalize let _ready = side === "background"; function _drainBuffer() { buffer.forEach((m) => _post(m)); buffer = []; } function _post(msg) { // Always use the 'to' parameter for port messages, making them directional. // Include senderInstanceId bus.emit( "__PORT_MESSAGE__", { portId, msg, senderInstanceId: instanceId }, { to: remoteWindow } ); } function postMessage(msg) { if (!_ready) { buffer.push(msg); } else { _post(msg); } } function _receive(envelope, source) { // envelope: { msg, senderInstanceId } if (envelope.senderInstanceId === instanceId) return; // Don't dispatch to self onMessageHandlers.forEach((fn) => fn(envelope.msg, { id: portId, tab: { id: source } }) ); } function disconnect() { // Also use the 'to' parameter for disconnect messages bus.emit("__PORT_DISCONNECT__", { portId }, { to: remoteWindow }); _disconnect(); delete ports[portId]; } function _disconnect() { onDisconnectHandlers.forEach((fn) => fn()); onMessageHandlers = []; onDisconnectHandlers = []; } // This function is called on the client port when the ACK is received from background. // It updates the port's state, completing the connection. function _finalize(win) { remoteWindow = win; // <-- This is the crucial part: learn the destination _ready = true; _drainBuffer(); } return { name, sender: { id: portId, }, onMessage: { addListener(fn) { onMessageHandlers.push(fn); }, removeListener(fn) { onMessageHandlers = onMessageHandlers.filter((x) => x !== fn); }, }, onDisconnect: { addListener(fn) { onDisconnectHandlers.push(fn); }, removeListener(fn) { onDisconnectHandlers = onDisconnectHandlers.filter((x) => x !== fn); }, }, postMessage, disconnect, // Internal methods used by the runtime _receive, _disconnect, _finalize, // Expose the finalizer for the ACK handler }; } function connect(connectInfo = {}) { if (type === "background") { throw new Error("Background must use onConnect, not connect()"); } const name = connectInfo.name || ""; const portId = nextPortId++; // create the client side port // remoteWindow is initially null; it will be set by _finalize upon ACK. const clientPort = makePort("client", portId, name, null); ports[portId] = clientPort; // fire the connect event across the bus bus.emit("__PORT_CONNECT__", { portId, name }); return clientPort; } function onConnect(fn) { if (type !== "background") { throw new Error("connect event only fires in background"); } onConnectListeners.push(fn); } return { // rpc: sendMessage, onMessage: { addListener(fn) { msgListeners.push(fn); }, removeListener(fn) { const i = msgListeners.indexOf(fn); if (i >= 0) msgListeners.splice(i, 1); }, }, // port API: connect, onConnect: { addListener(fn) { onConnect(fn); }, removeListener(fn) { const i = onConnectListeners.indexOf(fn); if (i >= 0) onConnectListeners.splice(i, 1); }, }, }; } // #region Abstraction layer Handle postmesage for (function () { const pendingRequests = new Map(); // requestId -> { resolve, reject, timeout } let nextRequestId = 1; window.addEventListener("message", async (event) => { const { type, requestId, method, args } = event.data; if (type === "abstraction-request") { try { let result; switch (method) { case "_storageSet": result = await _storageSet(args[0]); break; case "_storageGet": result = await _storageGet(args[0]); break; case "_storageRemove": result = await _storageRemove(args[0]); break; case "_storageClear": result = await _storageClear(); break; case "_cookieList": result = await _cookieList(args[0]); break; case "_cookieSet": result = await _cookieSet(args[0]); break; case "_cookieDelete": result = await _cookieDelete(args[0]); break; case "_fetch": result = await _fetch(args[0], args[1]); break; case "_registerMenuCommand": result = _registerMenuCommand(args[0], args[1]); break; case "_openTab": result = _openTab(args[0], args[1]); break; case "_initStorage": result = await _initStorage(); break; default: throw new Error(`Unknown abstraction method: ${method}`); } event.source.postMessage({ type: "abstraction-response", requestId, success: true, result, }); } catch (error) { event.source.postMessage({ type: "abstraction-response", requestId, success: false, error: { message: error.message, stack: error.stack, }, }); } } }); _log("[PostMessage Handler] Abstraction layer message handler initialized"); })(); // #endregion // #region Abstraction Layer Userscript Target async function _storageSet(items) { try { for (const key in items) { if (items.hasOwnProperty(key)) { await GM_setValue(key, items[key]); } } return Promise.resolve(); } catch (e) { _error("GM_setValue error:", e); return Promise.reject(e); } } async function _storageGet(keys) { if (!keys) { keys = null; } if ( Array.isArray(keys) && (keys.length === 0 || [null, undefined].includes(keys[0])) ) { keys = null; } try { const results = {}; let keyList = []; let defaults = {}; let requestedKeys = []; if (keys === null) { keyList = await GM_listValues(); requestedKeys = [...keyList]; } else if (typeof keys === "string") { keyList = [keys]; requestedKeys = [keys]; } else if (Array.isArray(keys)) { keyList = keys; requestedKeys = [...keys]; } else if (typeof keys === "object" && keys !== null) { keyList = Object.keys(keys); requestedKeys = [...keyList]; defaults = keys; } else { _error("_storageGet error: Invalid keys format", keys); return Promise.reject(new Error("Invalid keys format for get")); } for (const key of keyList) { const defaultValue = defaults.hasOwnProperty(key) ? defaults[key] : undefined; const storedValue = await GM_getValue(key, defaultValue); results[key] = storedValue; } const finalResult = {}; for (const key of requestedKeys) { if (results.hasOwnProperty(key)) { finalResult[key] = results[key]; } else if (defaults.hasOwnProperty(key)) { finalResult[key] = defaults[key]; } } return Promise.resolve(finalResult); } catch (e) { _error("GM_getValue/GM_listValues error:", e); return Promise.reject(e); } } async function _storageRemove(keysToRemove) { try { let keyList = []; if (typeof keysToRemove === "string") { keyList = [keysToRemove]; } else if (Array.isArray(keysToRemove)) { keyList = keysToRemove; } else { _error("_storageRemove error: Invalid keys format", keysToRemove); return Promise.reject(new Error("Invalid keys format for remove")); } for (const key of keyList) { await GM_deleteValue(key); } return Promise.resolve(); } catch (e) { _error("GM_deleteValue error:", e); return Promise.reject(e); } } async function _storageClear() { try { const keys = await GM_listValues(); await Promise.all(keys.map((key) => GM_deleteValue(key))); return Promise.resolve(); } catch (e) { _error("GM_listValues/GM_deleteValue error during clear:", e); return Promise.reject(e); } } async function _cookieList(details) { return new Promise((resolve, reject) => { if (typeof GM_cookie === "undefined" || !GM_cookie.list) { return reject(new Error("GM_cookie.list is not available.")); } GM_cookie.list(details, (cookies, error) => { if (error) { return reject(new Error(error)); } resolve(cookies); }); }); } async function _cookieSet(details) { return new Promise((resolve, reject) => { if (typeof GM_cookie === "undefined" || !GM_cookie.set) { return reject(new Error("GM_cookie.set is not available.")); } GM_cookie.set(details, (error) => { if (error) { return reject(new Error(error)); } resolve(); }); }); } async function _cookieDelete(details) { return new Promise((resolve, reject) => { if (typeof GM_cookie === "undefined" || !GM_cookie.delete) { return reject(new Error("GM_cookie.delete is not available.")); } GM_cookie.delete(details, (error) => { if (error) { return reject(new Error(error)); } resolve(); }); }); } async function _fetch(url, options = {}) { return new Promise((resolve, reject) => { try { GM_xmlhttpRequest({ method: options.method || "GET", url: url, headers: options.headers || {}, data: options.body, responseType: options.responseType, timeout: options.timeout || 0, binary: options.responseType === "blob" || options.responseType === "arraybuffer", onload: function (response) { const responseHeaders = {}; if (response.responseHeaders) { response.responseHeaders .trim() .split("\\r\\n") .forEach((header) => { const parts = header.match(/^([^:]+):\s*(.*)$/); if (parts && parts.length === 3) { responseHeaders[parts[1].toLowerCase()] = parts[2]; } }); } const mockResponse = { ok: response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText || (response.status >= 200 && response.status < 300 ? "OK" : ""), url: response.finalUrl || url, headers: new Headers(responseHeaders), text: () => Promise.resolve(response.responseText), json: () => { try { return Promise.resolve(JSON.parse(response.responseText)); } catch (e) { return Promise.reject(new SyntaxError("Could not parse JSON")); } }, blob: () => { if (response.response instanceof Blob) { return Promise.resolve(response.response); } return Promise.reject( new Error("Requires responseType:'blob' in GM_xmlhttpRequest") ); }, arrayBuffer: () => { if (response.response instanceof ArrayBuffer) { return Promise.resolve(response.response); } return Promise.reject( new Error( "Requires responseType:'arraybuffer' in GM_xmlhttpRequest" ) ); }, clone: function () { const cloned = { ...this }; cloned.text = () => Promise.resolve(response.responseText); cloned.json = () => this.json(); cloned.blob = () => this.blob(); cloned.arrayBuffer = () => this.arrayBuffer(); return cloned; }, }; if (mockResponse.ok) { resolve(mockResponse); } else { const error = new Error(`HTTP error! status: ${response.status}`); error.response = mockResponse; reject(error); } }, onerror: function (response) { reject( new Error( `GM_xmlhttpRequest network error: ${ response.statusText || "Unknown Error" }` ) ); }, onabort: function () { reject(new Error("GM_xmlhttpRequest aborted")); }, ontimeout: function () { reject(new Error("GM_xmlhttpRequest timed out")); }, }); } catch (e) { _error("_fetch (GM_xmlhttpRequest) error:", e); reject(e); } }); } function _registerMenuCommand(name, func) { if (typeof GM_registerMenuCommand === "function") { try { GM_registerMenuCommand(name, func); } catch (e) { _error("GM_registerMenuCommand failed:", e); } } else { _warn("GM_registerMenuCommand not available."); } } function _openTab(url, active) { if (typeof GM_openInTab === "function") { try { GM_openInTab(url, { loadInBackground: !active }); } catch (e) { _error("GM_openInTab failed:", e); } } else { _warn("GM_openInTab not available, using window.open as fallback."); try { window.open(url); } catch (e) { _error("window.open fallback failed:", e); } } } async function _initStorage() { return Promise.resolve(); } const EXTENSION_ASSETS_MAP = { "options_page.html": "\n\n\n\n Options for Web Search Navigator\n \n\n\n\n
\n

General settings

\n
\n \n
\n
\n \n
\n
\n
\n

Google specific settings

\n
\n \n
\n
\n \n
\n
\n \n
\n
\n
\n

Keybindings

\n
\n

Help

\n
\n All keybindings should be specified in\n Mousetrap format. Examples:\n
    \n
  • \n a\n
  • \n
  • \n z y\n
  • \n
  • \n ctrl+a\n
  • \n
  • \n command+a\n
  • \n
  • \n a, ctrl+b, z y, command+c - multiple shortcuts that will be treated equivalently
  • \n
\n Special keys names: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown,\n del, delete, and f1 through f19. In order to disable a keybinding, delete its keybinding in the textbox.\n\n Note that not all search engines support all the keybindings.\n
\n
\n
\n

Common actions

\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n

Results filtering

\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n

Google and Startpage

\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n

EXPERIMENTAL: Alternative search engines

\n
\n

Help

\n There is experimental support for using this extension in the websites below.\n Note that some features are still buggy in certain websites.\n You can enable or disable the extension of these websites at any time by clicking on the checkboxes.\n When you enable a website, the browser will prompt you for additional permissions which are needed to be able to run this extension on that website.\n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n
\n

Appearance

\n
\n \n
\n
\n

EXPERIMENTAL: Custom CSS

\n You can set custom CSS rules to change how the focused search results are highlighted. The textarea below contains the default CSS rules.\n If you want to reset the CSS to the defaults, set the textarea content to an empty string and save.\n
\n

Edit CSS rules

\n \n
\n
\n
\n
\n
\n

Advanced

\n
\n
\n This option can be used as a workaround for some websites.\n
\n
\n \n
\n
\n
\n \n
\n
\n

Custom Gitlab URL regex

\n
\n Define private Gitlab URL regex. Default is ^https://(www\\.)?.*git.*\\. \n
\n
\n \n
\n
\n
\n
\n\n
\n
\n \n \n
\n\n \n \n \n\n\n\n" }; // #endregion // #endregion // #region Polyfill Implementation function buildPolyfill({ isBackground = false, isOtherPage = false } = {}) { // Generate a unique context ID for this polyfill instance const contextType = isBackground ? "background" : isOtherPage ? "options" : "content"; const contextId = `${contextType}_${Math.random() .toString(36) .substring(2, 15)}`; const IS_IFRAME = "false" === "true"; const BUS = (function () { if (globalThis.__BUS) { return globalThis.__BUS; } globalThis.__BUS = createEventBus( "web-search-navigator", IS_IFRAME ? "iframe" : "page", ); return globalThis.__BUS; })(); const RUNTIME = createRuntime(isBackground ? "background" : "tab", BUS); const createNoopListeners = () => ({ addListener: (callback) => { _log("addListener", callback); }, removeListener: (callback) => { _log("removeListener", callback); }, }); // TODO: Stub const storageChangeListeners = new Set(); function broadcastStorageChange(changes, areaName) { storageChangeListeners.forEach((listener) => { listener(changes, areaName); }); } let REQ_PERMS = []; // #region Chrome polyfill let chrome = { extension: { isAllowedIncognitoAccess: () => Promise.resolve(true), sendMessage: (...args) => _messagingHandler.sendMessage(...args), }, permissions: { // TODO: Remove origin permission means exclude from origin in startup (when checking for content scripts) request: (permissions, callback) => { _log("permissions.request", permissions, callback); if (Array.isArray(permissions)) { REQ_PERMS = [...REQ_PERMS, ...permissions]; } if (typeof callback === "function") { callback(permissions); } return Promise.resolve(permissions); }, contains: (permissions, callback) => { if (typeof callback === "function") { callback(true); } return Promise.resolve(true); }, getAll: () => { return Promise.resolve({ permissions: EXTENSION_PERMISSIONS, origins: ORIGIN_PERMISSIONS, }); }, onAdded: createNoopListeners(), onRemoved: createNoopListeners(), }, i18n: { getUILanguage: () => { return USED_LOCALE || "en"; }, getMessage: (key, substitutions = []) => { if (typeof substitutions === "string") { substitutions = [substitutions]; } if (typeof LOCALE_KEYS !== "undefined" && LOCALE_KEYS[key]) { return LOCALE_KEYS[key].message?.replace( /\$(\d+)/g, (match, p1) => substitutions[p1 - 1] || match, ); } return key; }, }, alarms: { onAlarm: createNoopListeners(), create: () => { _log("alarms.create", arguments); }, get: () => { _log("alarms.get", arguments); }, }, runtime: { ...RUNTIME, onInstalled: createNoopListeners(), onStartup: createNoopListeners(), // TODO: Postmessage to parent to open options page or call openOptionsPage openOptionsPage: () => { // const url = chrome.runtime.getURL(OPTIONS_PAGE_PATH); // console.log("openOptionsPage", _openTab, url, EXTENSION_ASSETS_MAP); // _openTab(url); if (typeof openOptionsPage === "function") { openOptionsPage(); } else if (window.parent) { window.parent.postMessage({ type: "openOptionsPage" }, "*"); } else { _warn("openOptionsPage not available."); } }, getManifest: () => { // The manifest object will be injected into the scope where buildPolyfill is called if (typeof INJECTED_MANIFEST !== "undefined") { return JSON.parse(JSON.stringify(INJECTED_MANIFEST)); // Return deep copy } _warn("INJECTED_MANIFEST not found for chrome.runtime.getManifest"); return { name: "Unknown", version: "0.0", manifest_version: 2 }; }, getURL: (path) => { if (!path) return ""; if (path.startsWith("/")) { path = path.substring(1); } if (typeof _createAssetUrl === "function") { return _createAssetUrl(path); } _warn( `chrome.runtime.getURL fallback for '${path}'. Assets may not be available.`, ); // Attempt a relative path resolution (highly context-dependent and likely wrong) try { if (window.location.protocol.startsWith("http")) { return new URL(path, window.location.href).toString(); } } catch (e) { /* ignore error, fallback */ } return path; }, id: "polyfilled-extension-" + Math.random().toString(36).substring(2, 15), lastError: null, setUninstallURL: () => {}, setUpdateURL: () => {}, getPlatformInfo: async () => { const platform = { os: "unknown", arch: "unknown", nacl_arch: "unknown", }; if (typeof navigator !== "undefined") { const userAgent = navigator.userAgent.toLowerCase(); if (userAgent.includes("mac")) platform.os = "mac"; else if (userAgent.includes("win")) platform.os = "win"; else if (userAgent.includes("linux")) platform.os = "linux"; else if (userAgent.includes("android")) platform.os = "android"; else if (userAgent.includes("ios")) platform.os = "ios"; if (userAgent.includes("x86_64") || userAgent.includes("amd64")) { platform.arch = "x86-64"; } else if (userAgent.includes("i386") || userAgent.includes("i686")) { platform.arch = "x86-32"; } else if (userAgent.includes("arm")) { platform.arch = "arm"; } } return platform; }, getBrowserInfo: async () => { const info = { name: "unknown", version: "unknown", buildID: "unknown", }; if (typeof navigator !== "undefined") { const userAgent = navigator.userAgent; if (userAgent.includes("Chrome")) { info.name = "Chrome"; const match = userAgent.match(/Chrome\/([0-9.]+)/); if (match) info.version = match[1]; } else if (userAgent.includes("Firefox")) { info.name = "Firefox"; const match = userAgent.match(/Firefox\/([0-9.]+)/); if (match) info.version = match[1]; } else if (userAgent.includes("Safari")) { info.name = "Safari"; const match = userAgent.match(/Version\/([0-9.]+)/); if (match) info.version = match[1]; } } return info; }, }, storage: { local: { get: function (keys, callback) { if (typeof _storageGet !== "function") throw new Error("_storageGet not defined"); const promise = _storageGet(keys); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.get callback:", e); } }) .catch((error) => { _error("Storage.get error:", error); callback({}); }); return; } return promise; }, set: function (items, callback) { if (typeof _storageSet !== "function") throw new Error("_storageSet not defined"); const promise = _storageSet(items).then((result) => { broadcastStorageChange(items, "local"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.set callback:", e); } }) .catch((error) => { _error("Storage.set error:", error); callback(); }); return; } return promise; }, remove: function (keys, callback) { if (typeof _storageRemove !== "function") throw new Error("_storageRemove not defined"); const promise = _storageRemove(keys).then((result) => { const changes = {}; const keyList = Array.isArray(keys) ? keys : [keys]; keyList.forEach((key) => { changes[key] = { oldValue: undefined, newValue: undefined }; }); broadcastStorageChange(changes, "local"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.remove callback:", e); } }) .catch((error) => { _error("Storage.remove error:", error); callback(); }); return; } return promise; }, clear: function (callback) { if (typeof _storageClear !== "function") throw new Error("_storageClear not defined"); const promise = _storageClear().then((result) => { broadcastStorageChange({}, "local"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.clear callback:", e); } }) .catch((error) => { _error("Storage.clear error:", error); callback(); }); return; } return promise; }, onChanged: { addListener: (callback) => { storageChangeListeners.add(callback); }, removeListener: (callback) => { storageChangeListeners.delete(callback); }, }, }, sync: { get: function (keys, callback) { _warn("chrome.storage.sync polyfill maps to local"); return chrome.storage.local.get(keys, callback); }, set: function (items, callback) { _warn("chrome.storage.sync polyfill maps to local"); const promise = chrome.storage.local.set(items).then((result) => { broadcastStorageChange(items, "sync"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.sync.set callback:", e); } }) .catch((error) => { _error("Storage.sync.set error:", error); callback(); }); return; } return promise; }, remove: function (keys, callback) { _warn("chrome.storage.sync polyfill maps to local"); const promise = chrome.storage.local.remove(keys).then((result) => { const changes = {}; const keyList = Array.isArray(keys) ? keys : [keys]; keyList.forEach((key) => { changes[key] = { oldValue: undefined, newValue: undefined }; }); broadcastStorageChange(changes, "sync"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.sync.remove callback:", e); } }) .catch((error) => { _error("Storage.sync.remove error:", error); callback(); }); return; } return promise; }, clear: function (callback) { _warn("chrome.storage.sync polyfill maps to local"); const promise = chrome.storage.local.clear().then((result) => { broadcastStorageChange({}, "sync"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.sync.clear callback:", e); } }) .catch((error) => { _error("Storage.sync.clear error:", error); callback(); }); return; } return promise; }, onChanged: { addListener: (callback) => { storageChangeListeners.add(callback); }, removeListener: (callback) => { storageChangeListeners.delete(callback); }, }, }, onChanged: { addListener: (callback) => { storageChangeListeners.add(callback); }, removeListener: (callback) => { storageChangeListeners.delete(callback); }, }, managed: { get: function (keys, callback) { _warn("chrome.storage.managed polyfill is read-only empty."); const promise = Promise.resolve({}); if (typeof callback === "function") { promise.then((result) => { try { callback(result); } catch (e) { _error("Error in storage.managed.get callback:", e); } }); return; } return promise; }, }, }, cookies: (function () { const cookieChangeListeners = new Set(); function broadcastCookieChange(changeInfo) { cookieChangeListeners.forEach((listener) => { try { listener(changeInfo); } catch (e) { _error("Error in cookies.onChanged listener:", e); } }); } function handlePromiseCallback(promise, callback) { if (typeof callback === "function") { promise .then((result) => callback(result)) .catch((error) => { // chrome.runtime.lastError = { message: error.message }; // TODO: Implement lastError _error(error); callback(); // Call with undefined on error }); return; } return promise; } return { get: function (details, callback) { if (typeof _cookieList !== "function") { return handlePromiseCallback( Promise.reject(new Error("_cookieList not defined")), callback, ); } const promise = _cookieList({ url: details.url, name: details.name, storeId: details.storeId, partitionKey: details.partitionKey, }).then((cookies) => { if (!cookies || cookies.length === 0) { return null; } // Sort by path length (longest first), then creation time (earliest first, if available) cookies.sort((a, b) => { const pathLenDiff = (b.path || "").length - (a.path || "").length; if (pathLenDiff !== 0) return pathLenDiff; return (a.creationTime || 0) - (b.creationTime || 0); }); return cookies[0]; }); return handlePromiseCallback(promise, callback); }, getAll: function (details, callback) { if (typeof _cookieList !== "function") { return handlePromiseCallback( Promise.reject(new Error("_cookieList not defined")), callback, ); } if (details.partitionKey) { _warn( "cookies.getAll: partitionKey is not fully supported in this environment.", ); } const promise = _cookieList(details); return handlePromiseCallback(promise, callback); }, set: function (details, callback) { const promise = (async () => { if ( typeof _cookieSet !== "function" || typeof _cookieList !== "function" ) { throw new Error("_cookieSet or _cookieList not defined"); } if (details.partitionKey) { _warn( "cookies.set: partitionKey is not fully supported in this environment.", ); } const getDetails = { url: details.url, name: details.name, storeId: details.storeId, }; const oldCookies = await _cookieList(getDetails); const oldCookie = oldCookies && oldCookies[0]; if (oldCookie) { broadcastCookieChange({ cause: "overwrite", cookie: oldCookie, removed: true, }); } await _cookieSet(details); const newCookies = await _cookieList(getDetails); const newCookie = newCookies && newCookies[0]; if (newCookie) { broadcastCookieChange({ cause: "explicit", cookie: newCookie, removed: false, }); } return newCookie || null; })(); return handlePromiseCallback(promise, callback); }, remove: function (details, callback) { const promise = (async () => { if ( typeof _cookieDelete !== "function" || typeof _cookieList !== "function" ) { throw new Error("_cookieDelete or _cookieList not defined"); } const oldCookies = await _cookieList(details); const oldCookie = oldCookies && oldCookies[0]; if (!oldCookie) return null; // Nothing to remove await _cookieDelete(details); broadcastCookieChange({ cause: "explicit", cookie: oldCookie, removed: true, }); return { url: details.url, name: details.name, storeId: details.storeId || "0", partitionKey: details.partitionKey, }; })(); return handlePromiseCallback(promise, callback); }, getAllCookieStores: function (callback) { const promise = Promise.resolve([ { id: "0", tabIds: [1] }, // Mock store for the current context ]); return handlePromiseCallback(promise, callback); }, getPartitionKey: function (details, callback) { _warn( "chrome.cookies.getPartitionKey is not supported in this environment.", ); const promise = Promise.resolve({ partitionKey: {} }); // Return empty partition key return handlePromiseCallback(promise, callback); }, onChanged: { addListener: (callback) => { if (typeof callback === "function") { cookieChangeListeners.add(callback); } }, removeListener: (callback) => { cookieChangeListeners.delete(callback); }, }, }; })(), tabs: { query: async (queryInfo) => { _warn("chrome.tabs.query polyfill only returns current tab info."); const dummyId = Math.floor(Math.random() * 1000) + 1; return [ { id: dummyId, url: CURRENT_LOCATION, active: true, windowId: 1, status: "complete", }, ]; }, create: async ({ url, active = true }) => { _log(`[Polyfill tabs.create] URL: ${url}`); if (typeof _openTab !== "function") throw new Error("_openTab not defined"); _openTab(url, active); const dummyId = Math.floor(Math.random() * 1000) + 1001; return Promise.resolve({ id: dummyId, url: url, active, windowId: 1, }); }, sendMessage: async (tabId, message) => { _warn( `chrome.tabs.sendMessage polyfill (to tab ${tabId}) redirects to runtime.sendMessage (current context).`, ); return chrome.runtime.sendMessage(message); }, onActivated: createNoopListeners(), onUpdated: createNoopListeners(), onRemoved: createNoopListeners(), onReplaced: createNoopListeners(), onCreated: createNoopListeners(), onMoved: createNoopListeners(), onDetached: createNoopListeners(), onAttached: createNoopListeners(), }, windows: { onFocusChanged: createNoopListeners(), onCreated: createNoopListeners(), onRemoved: createNoopListeners(), onFocused: createNoopListeners(), onFocus: createNoopListeners(), onBlur: createNoopListeners(), onFocused: createNoopListeners(), }, notifications: { create: async (notificationId, options) => { try { let id = notificationId; let notificationOptions = options; if (typeof notificationId === "object" && notificationId !== null) { notificationOptions = notificationId; id = "notification_" + Math.random().toString(36).substring(2, 15); } else if (typeof notificationId === "string" && options) { id = notificationId; notificationOptions = options; } else { throw new Error("Invalid parameters for notifications.create"); } if (!notificationOptions || typeof notificationOptions !== "object") { throw new Error("Notification options must be an object"); } const { title, message, iconUrl, type = "basic", } = notificationOptions; if (!title || !message) { throw new Error("Notification must have title and message"); } if ("Notification" in window) { if (Notification.permission === "granted") { const notification = new Notification(title, { body: message, icon: iconUrl, tag: id, }); _log(`[Notifications] Created notification: ${id}`); return id; } else if (Notification.permission === "default") { const permission = await Notification.requestPermission(); if (permission === "granted") { const notification = new Notification(title, { body: message, icon: iconUrl, tag: id, }); _log( `[Notifications] Created notification after permission: ${id}`, ); return id; } else { _warn("[Notifications] Permission denied for notifications"); return id; } } else { _warn("[Notifications] Notifications are blocked"); return id; } } else { _warn( "[Notifications] Native notifications not supported, using console fallback", ); _log(`[Notification] ${title}: ${message}`); return id; } } catch (error) { _error("[Notifications] Error creating notification:", error.message); throw error; } }, clear: async (notificationId) => { _log(`[Notifications] Clear notification: ${notificationId}`); // For native notifications, there's no direct way to clear by ID // This is a limitation of the Web Notifications API return true; }, getAll: async () => { _warn("[Notifications] getAll not fully supported in polyfill"); return {}; }, getPermissionLevel: async () => { if ("Notification" in window) { const permission = Notification.permission; return { level: permission === "granted" ? "granted" : "denied" }; } return { level: "denied" }; }, }, contextMenus: { create: (createProperties, callback) => { try { if (!createProperties || typeof createProperties !== "object") { throw new Error("Context menu create properties must be an object"); } const { id, title, contexts = ["page"], onclick } = createProperties; const menuId = id || `menu_${Math.random().toString(36).substring(2, 15)}`; if (!title || typeof title !== "string") { throw new Error("Context menu must have a title"); } // Store menu items for potential use if (!window._polyfillContextMenus) { window._polyfillContextMenus = new Map(); } window._polyfillContextMenus.set(menuId, { id: menuId, title, contexts, onclick, enabled: createProperties.enabled !== false, }); _log( `[ContextMenus] Created context menu item: ${title} (${menuId})`, ); // Try to register a menu command as fallback if (typeof _registerMenuCommand === "function") { try { _registerMenuCommand( title, onclick || (() => { _log(`Context menu clicked: ${title}`); }), ); } catch (e) { _warn( "[ContextMenus] Failed to register as menu command:", e.message, ); } } if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } return menuId; } catch (error) { _error("[ContextMenus] Error creating context menu:", error.message); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } throw error; } }, update: (id, updateProperties, callback) => { try { if ( !window._polyfillContextMenus || !window._polyfillContextMenus.has(id) ) { throw new Error(`Context menu item not found: ${id}`); } const menuItem = window._polyfillContextMenus.get(id); Object.assign(menuItem, updateProperties); _log(`[ContextMenus] Updated context menu item: ${id}`); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } catch (error) { _error("[ContextMenus] Error updating context menu:", error.message); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } }, remove: (menuItemId, callback) => { try { if ( window._polyfillContextMenus && window._polyfillContextMenus.has(menuItemId) ) { window._polyfillContextMenus.delete(menuItemId); _log(`[ContextMenus] Removed context menu item: ${menuItemId}`); } else { _warn( `[ContextMenus] Context menu item not found for removal: ${menuItemId}`, ); } if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } catch (error) { _error("[ContextMenus] Error removing context menu:", error.message); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } }, removeAll: (callback) => { try { if (window._polyfillContextMenus) { const count = window._polyfillContextMenus.size; window._polyfillContextMenus.clear(); _log(`[ContextMenus] Removed all ${count} context menu items`); } if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } catch (error) { _error( "[ContextMenus] Error removing all context menus:", error.message, ); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } }, onClicked: { addListener: (callback) => { if (!window._polyfillContextMenuListeners) { window._polyfillContextMenuListeners = new Set(); } window._polyfillContextMenuListeners.add(callback); _log("[ContextMenus] Added click listener"); }, removeListener: (callback) => { if (window._polyfillContextMenuListeners) { window._polyfillContextMenuListeners.delete(callback); _log("[ContextMenus] Removed click listener"); } }, }, }, }; const tc = (fn) => { try { fn(); } catch (e) {} }; const loggingProxyHandler = (_key) => ({ get(target, key, receiver) { tc(() => _log(`[${contextType}] [CHROME - ${_key}] Getting ${key}`)); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { tc(() => _log(`[${contextType}] [CHROME - ${_key}] Setting ${key} to ${value}`), ); return Reflect.set(target, key, value, receiver); }, has(target, key) { tc(() => _log(`[${contextType}] [CHROME - ${_key}] Checking if ${key} exists`), ); return Reflect.has(target, key); }, }); chrome = Object.fromEntries( Object.entries(chrome).map(([key, value]) => [ key, new Proxy(value, loggingProxyHandler(key)), ]), ); // Alias browser to chrome for common Firefox pattern const browser = new Proxy(chrome, loggingProxyHandler); const oldGlobalThis = globalThis; const oldWindow = window; const oldSelf = self; const oldGlobal = globalThis; const __globalsStorage = {}; const TO_MODIFY = [oldGlobalThis, oldWindow, oldSelf, oldGlobal]; const set = (k, v) => { __globalsStorage[k] = v; TO_MODIFY.forEach((target) => { target[k] = v; }); }; const proxyHandler = { get(target, key, receiver) { const fns = [ () => __globalsStorage[key], () => Reflect.get(target, key, target), () => target[key], ]; const out = fns .map((f) => { try { let out = f(); return out; } catch (e) { return undefined; } }) .find((f) => f !== undefined); if (typeof out === "function") { return out.bind(target); } return out; }, set(target, key, value, receiver) { try { tc(() => _log(`[${contextType}] Setting ${key} to ${value}`)); set(key, value); return Reflect.set(target, key, value, receiver); } catch (e) { _error("Error setting", key, value, e); try { target[key] = value; return true; } catch (e) { _error("Error setting", key, value, e); } return false; } }, has(target, key) { try { return key in __globalsStorage || key in target; } catch (e) { _error("Error has", key, e); try { return key in __globalsStorage || key in target; } catch (e) { _error("Error has", key, e); } return false; } }, getOwnPropertyDescriptor(target, key) { try { if (key in __globalsStorage) { return { configurable: true, enumerable: true, writable: true, value: __globalsStorage[key], }; } // fall back to the real globalThis const desc = Reflect.getOwnPropertyDescriptor(target, key); // ensure it's configurable so the with‑scope binding logic can override it if (desc && !desc.configurable) { desc.configurable = true; } return desc; } catch (e) { _error("Error getOwnPropertyDescriptor", key, e); return { configurable: true, enumerable: true, writable: true, value: undefined, }; } }, defineProperty(target, key, descriptor) { try { // Normalize descriptor to avoid mixed accessor & data attributes const hasAccessor = "get" in descriptor || "set" in descriptor; if (hasAccessor) { // Build a clean descriptor without value/writable when accessors present const normalized = { configurable: "configurable" in descriptor ? descriptor.configurable : true, enumerable: "enumerable" in descriptor ? descriptor.enumerable : false, }; if ("get" in descriptor) normalized.get = descriptor.get; if ("set" in descriptor) normalized.set = descriptor.set; // Store accessor references for inspection but avoid breaking invariants set(key, { get: descriptor.get, set: descriptor.set, }); return Reflect.defineProperty(target, key, normalized); } // Data descriptor path set(key, descriptor.value); return Reflect.defineProperty(target, key, descriptor); } catch (e) { _error("Error defineProperty", key, descriptor, e); return false; } }, }; // Create proxies once proxyHandler is defined const proxyWindow = new Proxy(oldWindow, proxyHandler); const proxyGlobalThis = new Proxy(oldGlobalThis, proxyHandler); const proxyGlobal = new Proxy(oldGlobal, proxyHandler); const proxySelf = new Proxy(oldSelf, proxyHandler); // Seed storage with core globals so lookups succeed inside `with` blocks Object.assign(__globalsStorage, { chrome, browser, window: proxyWindow, globalThis: proxyGlobalThis, global: proxyGlobal, self: proxySelf, document: oldWindow.document, }); const __globals = { chrome, browser, window: proxyWindow, globalThis: proxyGlobalThis, global: proxyGlobal, self: proxySelf, __globals: __globalsStorage, }; __globals.contextId = contextId; __globals.contextType = contextType; __globals.module = undefined; __globals.amd = undefined; __globals.define = undefined; __globals.importScripts = (...args) => { _log("importScripts", args); }; return __globals; } if (typeof window !== 'undefined') { window.buildPolyfill = buildPolyfill; } // #endregion // #endregion // #endregion // #region Background Script Environment const START_BACKGROUND_SCRIPT = (function(){ const backgroundPolyfill = buildPolyfill({ isBackground: true }); const scriptName = "Web Search Navigator"; const debug = "[Web Search Navigator]"; _log(debug + ' Executing background scripts...'); function executeBackgroundScripts(){ with(backgroundPolyfill){ // BG: browser-polyfill.js !function(e,r){if("function"==typeof define&&define.amd)define("webextension-polyfill",["module"],r);else if("undefined"!=typeof exports)r(module);else{var s={exports:{}};r(s),e.browser=s.exports}}("undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:this,(function(e){"use strict";if(!globalThis.chrome?.runtime?.id)throw new Error("This script should only be loaded in a browser extension.");if(void 0===globalThis.browser||Object.getPrototypeOf(globalThis.browser)!==Object.prototype){const r="The message port closed before a response was received.",s=e=>{const s={alarms:{clear:{minArgs:0,maxArgs:1},clearAll:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getAll:{minArgs:0,maxArgs:0}},bookmarks:{create:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},getChildren:{minArgs:1,maxArgs:1},getRecent:{minArgs:1,maxArgs:1},getSubTree:{minArgs:1,maxArgs:1},getTree:{minArgs:0,maxArgs:0},move:{minArgs:2,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeTree:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}},browserAction:{disable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},enable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},getBadgeBackgroundColor:{minArgs:1,maxArgs:1},getBadgeText:{minArgs:1,maxArgs:1},getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},openPopup:{minArgs:0,maxArgs:0},setBadgeBackgroundColor:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setBadgeText:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},browsingData:{remove:{minArgs:2,maxArgs:2},removeCache:{minArgs:1,maxArgs:1},removeCookies:{minArgs:1,maxArgs:1},removeDownloads:{minArgs:1,maxArgs:1},removeFormData:{minArgs:1,maxArgs:1},removeHistory:{minArgs:1,maxArgs:1},removeLocalStorage:{minArgs:1,maxArgs:1},removePasswords:{minArgs:1,maxArgs:1},removePluginData:{minArgs:1,maxArgs:1},settings:{minArgs:0,maxArgs:0}},commands:{getAll:{minArgs:0,maxArgs:0}},contextMenus:{remove:{minArgs:1,maxArgs:1},removeAll:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},cookies:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:1,maxArgs:1},getAllCookieStores:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},devtools:{inspectedWindow:{eval:{minArgs:1,maxArgs:2,singleCallbackArg:!1}},panels:{create:{minArgs:3,maxArgs:3,singleCallbackArg:!0},elements:{createSidebarPane:{minArgs:1,maxArgs:1}}}},downloads:{cancel:{minArgs:1,maxArgs:1},download:{minArgs:1,maxArgs:1},erase:{minArgs:1,maxArgs:1},getFileIcon:{minArgs:1,maxArgs:2},open:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},pause:{minArgs:1,maxArgs:1},removeFile:{minArgs:1,maxArgs:1},resume:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},extension:{isAllowedFileSchemeAccess:{minArgs:0,maxArgs:0},isAllowedIncognitoAccess:{minArgs:0,maxArgs:0}},history:{addUrl:{minArgs:1,maxArgs:1},deleteAll:{minArgs:0,maxArgs:0},deleteRange:{minArgs:1,maxArgs:1},deleteUrl:{minArgs:1,maxArgs:1},getVisits:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1}},i18n:{detectLanguage:{minArgs:1,maxArgs:1},getAcceptLanguages:{minArgs:0,maxArgs:0}},identity:{launchWebAuthFlow:{minArgs:1,maxArgs:1}},idle:{queryState:{minArgs:1,maxArgs:1}},management:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},getSelf:{minArgs:0,maxArgs:0},setEnabled:{minArgs:2,maxArgs:2},uninstallSelf:{minArgs:0,maxArgs:1}},notifications:{clear:{minArgs:1,maxArgs:1},create:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:0},getPermissionLevel:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},pageAction:{getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},hide:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},permissions:{contains:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},request:{minArgs:1,maxArgs:1}},runtime:{getBackgroundPage:{minArgs:0,maxArgs:0},getPlatformInfo:{minArgs:0,maxArgs:0},openOptionsPage:{minArgs:0,maxArgs:0},requestUpdateCheck:{minArgs:0,maxArgs:0},sendMessage:{minArgs:1,maxArgs:3},sendNativeMessage:{minArgs:2,maxArgs:2},setUninstallURL:{minArgs:1,maxArgs:1}},sessions:{getDevices:{minArgs:0,maxArgs:1},getRecentlyClosed:{minArgs:0,maxArgs:1},restore:{minArgs:0,maxArgs:1}},storage:{local:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},managed:{get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1}},sync:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}}},tabs:{captureVisibleTab:{minArgs:0,maxArgs:2},create:{minArgs:1,maxArgs:1},detectLanguage:{minArgs:0,maxArgs:1},discard:{minArgs:0,maxArgs:1},duplicate:{minArgs:1,maxArgs:1},executeScript:{minArgs:1,maxArgs:2},get:{minArgs:1,maxArgs:1},getCurrent:{minArgs:0,maxArgs:0},getZoom:{minArgs:0,maxArgs:1},getZoomSettings:{minArgs:0,maxArgs:1},goBack:{minArgs:0,maxArgs:1},goForward:{minArgs:0,maxArgs:1},highlight:{minArgs:1,maxArgs:1},insertCSS:{minArgs:1,maxArgs:2},move:{minArgs:2,maxArgs:2},query:{minArgs:1,maxArgs:1},reload:{minArgs:0,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeCSS:{minArgs:1,maxArgs:2},sendMessage:{minArgs:2,maxArgs:3},setZoom:{minArgs:1,maxArgs:2},setZoomSettings:{minArgs:1,maxArgs:2},update:{minArgs:1,maxArgs:2}},topSites:{get:{minArgs:0,maxArgs:0}},webNavigation:{getAllFrames:{minArgs:1,maxArgs:1},getFrame:{minArgs:1,maxArgs:1}},webRequest:{handlerBehaviorChanged:{minArgs:0,maxArgs:0}},windows:{create:{minArgs:0,maxArgs:1},get:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:1},getCurrent:{minArgs:0,maxArgs:1},getLastFocused:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}}};if(0===Object.keys(s).length)throw new Error("api-metadata.json has not been included in browser-polyfill");class g extends WeakMap{constructor(e,r){super(r),this.createItem=e}get(e){return this.has(e)||this.set(e,this.createItem(e)),super.get(e)}}const a=(r,s)=>(...g)=>{e.runtime.lastError?r.reject(new Error(e.runtime.lastError.message)):s.singleCallbackArg||g.length<=1&&!1!==s.singleCallbackArg?r.resolve(g[0]):r.resolve(g)},m=e=>1==e?"argument":"arguments",n=(e,r,s)=>new Proxy(r,{apply:(r,g,a)=>s.call(g,e,...a)});let t=Function.call.bind(Object.prototype.hasOwnProperty);const A=(e,r={},s={})=>{let g=Object.create(null),i={has:(r,s)=>s in e||s in g,get(i,o,l){if(o in g)return g[o];if(!(o in e))return;let x=e[o];if("function"==typeof x)if("function"==typeof r[o])x=n(e,e[o],r[o]);else if(t(s,o)){let r=((e,r)=>function(s,...g){if(g.lengthr.maxArgs)throw new Error(`Expected at most ${r.maxArgs} ${m(r.maxArgs)} for ${e}(), got ${g.length}`);return new Promise(((m,n)=>{if(r.fallbackToNoCallback)try{s[e](...g,a({resolve:m,reject:n},r))}catch(a){console.warn(`${e} API method doesn't seem to support the callback parameter, falling back to call it without a callback: `,a),s[e](...g),r.fallbackToNoCallback=!1,r.noCallback=!0,m()}else r.noCallback?(s[e](...g),m()):s[e](...g,a({resolve:m,reject:n},r))}))})(o,s[o]);x=n(e,e[o],r)}else x=x.bind(e);else if("object"==typeof x&&null!==x&&(t(r,o)||t(s,o)))x=A(x,r[o],s[o]);else{if(!t(s,"*"))return Object.defineProperty(g,o,{configurable:!0,enumerable:!0,get:()=>e[o],set(r){e[o]=r}}),x;x=A(x,r[o],s["*"])}return g[o]=x,x},set:(r,s,a,m)=>(s in g?g[s]=a:e[s]=a,!0),defineProperty:(e,r,s)=>Reflect.defineProperty(g,r,s),deleteProperty:(e,r)=>Reflect.deleteProperty(g,r)},o=Object.create(e);return new Proxy(o,i)},i=e=>({addListener(r,s,...g){r.addListener(e.get(s),...g)},hasListener:(r,s)=>r.hasListener(e.get(s)),removeListener(r,s){r.removeListener(e.get(s))}}),o=new g((e=>"function"!=typeof e?e:function(r){const s=A(r,{},{getContent:{minArgs:0,maxArgs:0}});e(s)})),l=new g((e=>"function"!=typeof e?e:function(r,s,g){let a,m,n=!1,t=new Promise((e=>{a=function(r){n=!0,e(r)}}));try{m=e(r,s,a)}catch(e){m=Promise.reject(e)}const A=!0!==m&&((i=m)&&"object"==typeof i&&"function"==typeof i.then);var i;if(!0!==m&&!A&&!n)return!1;const o=e=>{e.then((e=>{g(e)}),(e=>{let r;r=e&&(e instanceof Error||"string"==typeof e.message)?e.message:"An unexpected error occurred",g({__mozWebExtensionPolyfillReject__:!0,message:r})})).catch((e=>{console.error("Failed to send onMessage rejected reply",e)}))};return o(A?m:t),!0})),x=({reject:s,resolve:g},a)=>{e.runtime.lastError?e.runtime.lastError.message===r?g():s(new Error(e.runtime.lastError.message)):a&&a.__mozWebExtensionPolyfillReject__?s(new Error(a.message)):g(a)},c=(e,r,s,...g)=>{if(g.lengthr.maxArgs)throw new Error(`Expected at most ${r.maxArgs} ${m(r.maxArgs)} for ${e}(), got ${g.length}`);return new Promise(((e,r)=>{const a=x.bind(null,{resolve:e,reject:r});g.push(a),s.sendMessage(...g)}))},d={devtools:{network:{onRequestFinished:i(o)}},runtime:{onMessage:i(l),onMessageExternal:i(l),sendMessage:c.bind(null,"sendMessage",{minArgs:1,maxArgs:3})},tabs:{sendMessage:c.bind(null,"sendMessage",{minArgs:2,maxArgs:3})}},u={clear:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}};return s.privacy={network:{"*":u},services:{"*":u},websites:{"*":u}},A(e,d,s)};e.exports=s(chrome)}else e.exports=globalThis.browser})); // BG: webext-dynamic-content-scripts.js !function(){"use strict";const t=globalThis.chrome&&new function t(e){return new Proxy(e,{get:(e,n)=>"function"!=typeof e[n]?new t(e[n]):(...t)=>new Promise(((r,o)=>{e[n](...t,(t=>{chrome.runtime.lastError?o(new Error(chrome.runtime.lastError.message)):r(t)}))}))})}(globalThis.chrome),e="object"==typeof chrome&&"scripting"in chrome;function n(t){return void 0===t?void 0:[t]}function r({tabId:r,frameId:o,files:s,allFrames:i,matchAboutBlank:a,runAt:c}){for(let l of s)"string"==typeof l&&(l={file:l}),e?chrome.scripting.insertCSS({target:{tabId:r,frameIds:n(o),allFrames:i},files:"file"in l?[l.file]:void 0,css:"code"in l?l.code:void 0}):t.tabs.insertCSS(r,{...l,matchAboutBlank:a,allFrames:i,frameId:o,runAt:null!=c?c:"document_start"})}async function o({tabId:r,frameId:o,files:s,allFrames:i,matchAboutBlank:a,runAt:c}){let l;for(let u of s)if("string"==typeof u&&(u={file:u}),e){if("code"in u)throw new Error("chrome.scripting does not support injecting strings of `code`");chrome.scripting.executeScript({target:{tabId:r,frameIds:n(o),allFrames:i},files:[u.file]})}else"code"in u&&await l,l=t.tabs.executeScript(r,{...u,matchAboutBlank:a,allFrames:i,frameId:o,runAt:c})}async function s(t,e){var n,s,i,a,c,l;const{frameId:u,tabId:m}="object"==typeof t?t:{tabId:t,frameId:0};for(const t of(f=e,Array.isArray(f)?f:[f]))r({tabId:m,frameId:u,files:null!==(n=t.css)&&void 0!==n?n:[],matchAboutBlank:null!==(s=t.matchAboutBlank)&&void 0!==s?s:t.match_about_blank,runAt:null!==(i=t.runAt)&&void 0!==i?i:t.run_at}),o({tabId:m,frameId:u,files:null!==(a=t.js)&&void 0!==a?a:[],matchAboutBlank:null!==(c=t.matchAboutBlank)&&void 0!==c?c:t.match_about_blank,runAt:null!==(l=t.runAt)&&void 0!==l?l:t.run_at});var f;await Promise.all([])}const i=globalThis.chrome&&new function t(e){return new Proxy(e,{get:(e,n)=>"function"!=typeof e[n]?new t(e[n]):(...t)=>new Promise(((r,o)=>{e[n](...t,(t=>{chrome.runtime.lastError?o(new Error(chrome.runtime.lastError.message)):r(t)}))}))})}(globalThis.chrome),a=/^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^file:\/\/\/.*$|^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/,c="object"==typeof navigator&&navigator.userAgent.includes("Firefox/"),l=c?/^(https?|wss?):[/][/][^/]+([/].*)?$/:/^https?:[/][/][^/]+([/].*)?$/,u=/^(https?|file|ftp):[/]+/;function m(...t){return 0===t.length?/$./:t.includes("")?u:t.includes("*://*/*")?l:new RegExp(t.map((t=>function(t){if(!a.test(t))throw new Error(t+" is an invalid pattern, it must match "+String(a));let[,e,n,r]=t.split(/(^[^:]+:[/][/])([^/]+)?/);return e=e.replace("*",c?"(https?|wss?)":"https?").replace(/[/]/g,"[/]"),n=(null!=n?n:"").replace(/^[*][.]/,"([^/]+.)*").replace(/^[*]$/,"[^/]+").replace(/[.]/g,"[.]").replace(/[*]$/g,"[^.]+"),r=r.replace(/[/]/g,"[/]").replace(/[.]/g,"[.]").replace(/[*]/g,".*"),"^"+e+n+"("+r+")?$"}(t))).join("|"))}const f="object"==typeof chrome&&"webNavigation"in chrome;async function d(n,r){return async function(n,r,...o){const{frameId:s,tabId:i}=function(t){return"object"==typeof t?t:{tabId:t,frameId:0}}(n);if(e){const[t]=await chrome.scripting.executeScript({target:{tabId:i,frameIds:[s]},func:r,args:o});return null==t?void 0:t.result}const[a]=await t.tabs.executeScript(i,{code:`(${r.toString()})(...${JSON.stringify(o)})`,frameId:s});return a}(n,(t=>{const e=document[t];return document[t]=!0,e}),JSON.stringify(r))}function p(){return function(t){var e,n,r;const o={origins:[],permissions:[]},s=new Set([...null!==(e=t.permissions)&&void 0!==e?e:[],...(null!==(n=t.content_scripts)&&void 0!==n?n:[]).flatMap((t=>{var e;return null!==(e=t.matches)&&void 0!==e?e:[]}))]);t.devtools_page&&!(null===(r=t.optional_permissions)||void 0===r?void 0:r.includes("devtools"))&&s.add("devtools");for(const t of s)t.includes("://")?o.origins.push(t):o.permissions.push(t);return o}(chrome.runtime.getManifest())}const h=/:[/][/][*.]*([^/]+)/;function g(t){return t.split(h)[1]}async function b(t){return new Promise((e=>{chrome.permissions.getAll((n=>{const r=p();e(function(t,e,{strictOrigins:n=!0}={}){var r,o;const s={origins:[],permissions:[]};for(const o of null!==(r=e.origins)&&void 0!==r?r:[])if(!t.origins.includes(o)){if(!n){const e=g(o);if(t.origins.some((t=>g(t)===e)))continue}s.origins.push(o)}for(const n of null!==(o=e.permissions)&&void 0!==o?o:[])t.permissions.includes(n)||s.permissions.push(n);return s}(r,n,t))}))}))}var v,w,y;const I=new Map,A=null!==(y=null===(w=null===(v=null===globalThis||void 0===globalThis?void 0:globalThis.browser)||void 0===v?void 0:v.contentScripts)||void 0===w?void 0:w.register)&&void 0!==y?y:async function(t,e){const{js:n=[],css:s=[],matchAboutBlank:a,matches:c,excludeMatches:l,runAt:u}=t;let{allFrames:p}=t;f?p=!1:p&&console.warn("`allFrames: true` requires the `webNavigation` permission to work correctly: https://github.com/fregante/content-scripts-register-polyfill#permissions");const h=m(...c),g=m(...null!=l?l:[]),b=async(t,e,c=0)=>{h.test(t)&&!g.test(t)&&await async function(t){return i.permissions.contains({origins:[new URL(t).origin+"/*"]})}(t)&&!await d({tabId:e,frameId:c},{js:n,css:s})&&(r({tabId:e,frameId:c,files:s,matchAboutBlank:a,runAt:u}),await o({tabId:e,frameId:c,files:n,matchAboutBlank:a,runAt:u}))},v=async(t,{status:e},{url:n})=>{e&&n&&b(n,t)},w=async({tabId:t,frameId:e,url:n})=>{b(n,t,e)};f?chrome.webNavigation.onCommitted.addListener(w):chrome.tabs.onUpdated.addListener(v);const y={async unregister(){f?chrome.webNavigation.onCommitted.removeListener(w):chrome.tabs.onUpdated.removeListener(v)}};return"function"==typeof e&&e(y),y};function k(t){return{file:new URL(t,location.origin).pathname}}async function S({origins:t}){const e=chrome.runtime.getManifest().content_scripts;if(!e)throw new Error("webext-dynamic-content-scripts tried to register scripts on th new host permissions, but no content scripts were found in the manifest.");for(const n of t||[])for(const t of e){const e=A({js:(t.js||[]).map((t=>k(t))),css:(t.css||[]).map((t=>k(t))),allFrames:t.all_frames,matches:[n],excludeMatches:t.matches,runAt:t.run_at});I.set(n,e)}var n,r;r=e,0!==(n=t||[]).length&&chrome.tabs.query({url:n},(t=>{for(const e of t)e.id&&s(e.id,r)}))}(async()=>{S(await b({strictOrigins:!1}))})(),chrome.permissions.onAdded.addListener((t=>{t.origins&&t.origins.length>0&&S(t)})),chrome.permissions.onRemoved.addListener((async({origins:t})=>{if(t&&0!==t.length)for(const[e,n]of I)t.includes(e)&&(await n).unregister()}))}(); // BG: background.js browser.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === "tabsCreate") { browser.tabs .create({ url: request.options.url, active: request.options.active, openerTabId: sender.tab.id, }) .then((tab) => { if (!browser.tabs.group) { return; } return browser.tabs.group({ tabIds: tab.id, groupId: sender.tab.groupId, }); }); return true; } return false; }); } } executeBackgroundScripts.call(backgroundPolyfill); _log(debug + ' Background scripts execution complete.'); }); setTimeout(() => { // Wait for things to be defined START_BACKGROUND_SCRIPT(); }, 10); _log("START_BACKGROUND_SCRIPT", START_BACKGROUND_SCRIPT); // End background script environment // #endregion // #region Orchestration Logic // Other globals currently defined at this spot: SCRIPT_NAME, _log, _warn, _error const INJECTED_MANIFEST = {"manifest_version":2,"name":"Web Search Navigator","version":"0.5.2","description":"Keyboard shortcuts for Google search, YouTube, Startpage, Brave Search, Google Scholar, Github, and Amazon.","permissions":["storage"],"optional_permissions":["https://*/*","https://search.brave.com/*","https://startpage.com/*","https://www.startpage.com/*","https://www.youtube.com/*","https://github.com/*","https://gitlab.com/*","https://www.github.com/*","https://www.amazon.com/*","https://www.amazon.cn/*","https://www.amazon.in/*","https://www.amazon.co.jp/*","https://www.amazon.co.uk/*","https://www.amazon.ca/*","https://www.amazon.fr/*","https://www.amazon.de/*","https://www.amazon.it/*","https://www.amazon.es/*","https://www.amazon.com.au/*","https://www.amazon.com.mx/*","https://www.amazon.com.br/*","https://www.amazon.nl/*","https://scholar.google.ad/*","https://scholar.google.ae/*","https://scholar.google.al/*","https://scholar.google.am/*","https://scholar.google.as/*","https://scholar.google.at/*","https://scholar.google.az/*","https://scholar.google.ba/*","https://scholar.google.be/*","https://scholar.google.bf/*","https://scholar.google.bg/*","https://scholar.google.bi/*","https://scholar.google.bj/*","https://scholar.google.bs/*","https://scholar.google.bt/*","https://scholar.google.by/*","https://scholar.google.ca/*","https://scholar.google.cat/*","https://scholar.google.cd/*","https://scholar.google.cf/*","https://scholar.google.cg/*","https://scholar.google.ch/*","https://scholar.google.ci/*","https://scholar.google.cl/*","https://scholar.google.cm/*","https://scholar.google.cn/*","https://scholar.google.co.ao/*","https://scholar.google.co.bw/*","https://scholar.google.co.ck/*","https://scholar.google.co.cr/*","https://scholar.google.co.id/*","https://scholar.google.co.il/*","https://scholar.google.co.in/*","https://scholar.google.co.jp/*","https://scholar.google.co.ke/*","https://scholar.google.co.kr/*","https://scholar.google.co.ls/*","https://scholar.google.co.ma/*","https://scholar.google.co.mz/*","https://scholar.google.co.nz/*","https://scholar.google.co.th/*","https://scholar.google.co.tz/*","https://scholar.google.co.ug/*","https://scholar.google.co.uk/*","https://scholar.google.co.uz/*","https://scholar.google.co.ve/*","https://scholar.google.co.vi/*","https://scholar.google.co.za/*","https://scholar.google.co.zm/*","https://scholar.google.co.zw/*","https://scholar.google.com.af/*","https://scholar.google.com.ag/*","https://scholar.google.com.ai/*","https://scholar.google.com.ar/*","https://scholar.google.com.au/*","https://scholar.google.com.bd/*","https://scholar.google.com.bh/*","https://scholar.google.com.bn/*","https://scholar.google.com.bo/*","https://scholar.google.com.br/*","https://scholar.google.com.bz/*","https://scholar.google.com.co/*","https://scholar.google.com.cu/*","https://scholar.google.com.cy/*","https://scholar.google.com.do/*","https://scholar.google.com.ec/*","https://scholar.google.com.eg/*","https://scholar.google.com.et/*","https://scholar.google.com.fj/*","https://scholar.google.com.gh/*","https://scholar.google.com.gi/*","https://scholar.google.com.gt/*","https://scholar.google.com.hk/*","https://scholar.google.com.jm/*","https://scholar.google.com.kh/*","https://scholar.google.com.kw/*","https://scholar.google.com.lb/*","https://scholar.google.com.ly/*","https://scholar.google.com.mm/*","https://scholar.google.com.mt/*","https://scholar.google.com.mx/*","https://scholar.google.com.my/*","https://scholar.google.com.na/*","https://scholar.google.com.nf/*","https://scholar.google.com.ng/*","https://scholar.google.com.ni/*","https://scholar.google.com.np/*","https://scholar.google.com.om/*","https://scholar.google.com.pa/*","https://scholar.google.com.pe/*","https://scholar.google.com.pg/*","https://scholar.google.com.ph/*","https://scholar.google.com.pk/*","https://scholar.google.com.pr/*","https://scholar.google.com.py/*","https://scholar.google.com.qa/*","https://scholar.google.com.sa/*","https://scholar.google.com.sb/*","https://scholar.google.com.sg/*","https://scholar.google.com.sl/*","https://scholar.google.com.sv/*","https://scholar.google.com.tj/*","https://scholar.google.com.tr/*","https://scholar.google.com.tw/*","https://scholar.google.com.ua/*","https://scholar.google.com.uy/*","https://scholar.google.com.vc/*","https://scholar.google.com.vn/*","https://scholar.google.com/*","https://scholar.google.cv/*","https://scholar.google.cz/*","https://scholar.google.de/*","https://scholar.google.dj/*","https://scholar.google.dk/*","https://scholar.google.dm/*","https://scholar.google.dz/*","https://scholar.google.ee/*","https://scholar.google.es/*","https://scholar.google.fi/*","https://scholar.google.fm/*","https://scholar.google.fr/*","https://scholar.google.ga/*","https://scholar.google.ge/*","https://scholar.google.gg/*","https://scholar.google.gl/*","https://scholar.google.gm/*","https://scholar.google.gp/*","https://scholar.google.gr/*","https://scholar.google.gy/*","https://scholar.google.hn/*","https://scholar.google.hr/*","https://scholar.google.ht/*","https://scholar.google.hu/*","https://scholar.google.ie/*","https://scholar.google.im/*","https://scholar.google.iq/*","https://scholar.google.is/*","https://scholar.google.it/*","https://scholar.google.je/*","https://scholar.google.jo/*","https://scholar.google.kg/*","https://scholar.google.ki/*","https://scholar.google.kz/*","https://scholar.google.la/*","https://scholar.google.li/*","https://scholar.google.lk/*","https://scholar.google.lt/*","https://scholar.google.lu/*","https://scholar.google.lv/*","https://scholar.google.md/*","https://scholar.google.me/*","https://scholar.google.mg/*","https://scholar.google.mk/*","https://scholar.google.ml/*","https://scholar.google.mn/*","https://scholar.google.ms/*","https://scholar.google.mu/*","https://scholar.google.mv/*","https://scholar.google.mw/*","https://scholar.google.ne/*","https://scholar.google.nl/*","https://scholar.google.no/*","https://scholar.google.nr/*","https://scholar.google.nu/*","https://scholar.google.pl/*","https://scholar.google.pn/*","https://scholar.google.ps/*","https://scholar.google.pt/*","https://scholar.google.ro/*","https://scholar.google.rs/*","https://scholar.google.ru/*","https://scholar.google.rw/*","https://scholar.google.sc/*","https://scholar.google.se/*","https://scholar.google.sh/*","https://scholar.google.si/*","https://scholar.google.sk/*","https://scholar.google.sm/*","https://scholar.google.sn/*","https://scholar.google.so/*","https://scholar.google.sr/*","https://scholar.google.st/*","https://scholar.google.td/*","https://scholar.google.tg/*","https://scholar.google.tk/*","https://scholar.google.tl/*","https://scholar.google.tm/*","https://scholar.google.tn/*","https://scholar.google.to/*","https://scholar.google.tt/*","https://scholar.google.vg/*","https://scholar.google.vu/*","https://scholar.google.ws/*"],"content_scripts":[{"js":["browser-polyfill.js","mousetrap.js","mousetrap-global-bind.js","options.js","search_engines.js","main.js"],"run_at":"document_end","matches":["*://www.google.com/search*","*://www.google.ad/search*","*://www.google.ae/search*","*://www.google.com.af/search*","*://www.google.com.ag/search*","*://www.google.com.ai/search*","*://www.google.al/search*","*://www.google.am/search*","*://www.google.co.ao/search*","*://www.google.com.ar/search*","*://www.google.as/search*","*://www.google.at/search*","*://www.google.com.au/search*","*://www.google.az/search*","*://www.google.ba/search*","*://www.google.com.bd/search*","*://www.google.be/search*","*://www.google.bf/search*","*://www.google.bg/search*","*://www.google.com.bh/search*","*://www.google.bi/search*","*://www.google.bj/search*","*://www.google.com.bn/search*","*://www.google.com.bo/search*","*://www.google.com.br/search*","*://www.google.bs/search*","*://www.google.bt/search*","*://www.google.co.bw/search*","*://www.google.by/search*","*://www.google.com.bz/search*","*://www.google.ca/search*","*://www.google.cd/search*","*://www.google.cf/search*","*://www.google.cg/search*","*://www.google.ch/search*","*://www.google.ci/search*","*://www.google.co.ck/search*","*://www.google.cl/search*","*://www.google.cm/search*","*://www.google.cn/search*","*://www.google.com.co/search*","*://www.google.co.cr/search*","*://www.google.com.cu/search*","*://www.google.cv/search*","*://www.google.com.cy/search*","*://www.google.cz/search*","*://www.google.de/search*","*://www.google.dj/search*","*://www.google.dk/search*","*://www.google.dm/search*","*://www.google.com.do/search*","*://www.google.dz/search*","*://www.google.com.ec/search*","*://www.google.ee/search*","*://www.google.com.eg/search*","*://www.google.es/search*","*://www.google.com.et/search*","*://www.google.fi/search*","*://www.google.com.fj/search*","*://www.google.fm/search*","*://www.google.fr/search*","*://www.google.ga/search*","*://www.google.ge/search*","*://www.google.gg/search*","*://www.google.com.gh/search*","*://www.google.com.gi/search*","*://www.google.gl/search*","*://www.google.gm/search*","*://www.google.gp/search*","*://www.google.gr/search*","*://www.google.com.gt/search*","*://www.google.gy/search*","*://www.google.com.hk/search*","*://www.google.hn/search*","*://www.google.hr/search*","*://www.google.ht/search*","*://www.google.hu/search*","*://www.google.co.id/search*","*://www.google.ie/search*","*://www.google.co.il/search*","*://www.google.im/search*","*://www.google.co.in/search*","*://www.google.iq/search*","*://www.google.is/search*","*://www.google.it/search*","*://www.google.je/search*","*://www.google.com.jm/search*","*://www.google.jo/search*","*://www.google.co.jp/search*","*://www.google.co.ke/search*","*://www.google.com.kh/search*","*://www.google.ki/search*","*://www.google.kg/search*","*://www.google.co.kr/search*","*://www.google.com.kw/search*","*://www.google.kz/search*","*://www.google.la/search*","*://www.google.com.lb/search*","*://www.google.li/search*","*://www.google.lk/search*","*://www.google.co.ls/search*","*://www.google.lt/search*","*://www.google.lu/search*","*://www.google.lv/search*","*://www.google.com.ly/search*","*://www.google.co.ma/search*","*://www.google.md/search*","*://www.google.me/search*","*://www.google.mg/search*","*://www.google.mk/search*","*://www.google.ml/search*","*://www.google.com.mm/search*","*://www.google.mn/search*","*://www.google.ms/search*","*://www.google.com.mt/search*","*://www.google.mu/search*","*://www.google.mv/search*","*://www.google.mw/search*","*://www.google.com.mx/search*","*://www.google.com.my/search*","*://www.google.co.mz/search*","*://www.google.com.na/search*","*://www.google.com.nf/search*","*://www.google.com.ng/search*","*://www.google.com.ni/search*","*://www.google.ne/search*","*://www.google.nl/search*","*://www.google.no/search*","*://www.google.com.np/search*","*://www.google.nr/search*","*://www.google.nu/search*","*://www.google.co.nz/search*","*://www.google.com.om/search*","*://www.google.com.pa/search*","*://www.google.com.pe/search*","*://www.google.com.pg/search*","*://www.google.com.ph/search*","*://www.google.com.pk/search*","*://www.google.pl/search*","*://www.google.pn/search*","*://www.google.com.pr/search*","*://www.google.ps/search*","*://www.google.pt/search*","*://www.google.com.py/search*","*://www.google.com.qa/search*","*://www.google.ro/search*","*://www.google.ru/search*","*://www.google.rw/search*","*://www.google.com.sa/search*","*://www.google.com.sb/search*","*://www.google.sc/search*","*://www.google.se/search*","*://www.google.com.sg/search*","*://www.google.sh/search*","*://www.google.si/search*","*://www.google.sk/search*","*://www.google.com.sl/search*","*://www.google.sn/search*","*://www.google.so/search*","*://www.google.sm/search*","*://www.google.sr/search*","*://www.google.st/search*","*://www.google.com.sv/search*","*://www.google.td/search*","*://www.google.tg/search*","*://www.google.co.th/search*","*://www.google.com.tj/search*","*://www.google.tk/search*","*://www.google.tl/search*","*://www.google.tm/search*","*://www.google.tn/search*","*://www.google.to/search*","*://www.google.com.tr/search*","*://www.google.tt/search*","*://www.google.com.tw/search*","*://www.google.co.tz/search*","*://www.google.com.ua/search*","*://www.google.co.ug/search*","*://www.google.co.uk/search*","*://www.google.com.uy/search*","*://www.google.co.uz/search*","*://www.google.com.vc/search*","*://www.google.co.ve/search*","*://www.google.vg/search*","*://www.google.co.vi/search*","*://www.google.com.vn/search*","*://www.google.vu/search*","*://www.google.ws/search*","*://www.google.rs/search*","*://www.google.co.za/search*","*://www.google.co.zm/search*","*://www.google.co.zw/search*","*://www.google.cat/search*"],"css":[]}],"options_ui":{"page":"options_page.html","chrome_style":true},"browser_action":{},"page_action":{},"action":{},"icons":{"16":"icon16.png","48":"icon48.png","128":"icon128.png"},"web_accessible_resources":[],"background":{"scripts":["browser-polyfill.js","webext-dynamic-content-scripts.js","background.js"],"persistent":false},"_id":"web-search-navigator"}; const CONTENT_SCRIPT_CONFIGS_FOR_MATCHING = [ { "matches": [ "*://www.google.com/search*", "*://www.google.ad/search*", "*://www.google.ae/search*", "*://www.google.com.af/search*", "*://www.google.com.ag/search*", "*://www.google.com.ai/search*", "*://www.google.al/search*", "*://www.google.am/search*", "*://www.google.co.ao/search*", "*://www.google.com.ar/search*", "*://www.google.as/search*", "*://www.google.at/search*", "*://www.google.com.au/search*", "*://www.google.az/search*", "*://www.google.ba/search*", "*://www.google.com.bd/search*", "*://www.google.be/search*", "*://www.google.bf/search*", "*://www.google.bg/search*", "*://www.google.com.bh/search*", "*://www.google.bi/search*", "*://www.google.bj/search*", "*://www.google.com.bn/search*", "*://www.google.com.bo/search*", "*://www.google.com.br/search*", "*://www.google.bs/search*", "*://www.google.bt/search*", "*://www.google.co.bw/search*", "*://www.google.by/search*", "*://www.google.com.bz/search*", "*://www.google.ca/search*", "*://www.google.cd/search*", "*://www.google.cf/search*", "*://www.google.cg/search*", "*://www.google.ch/search*", "*://www.google.ci/search*", "*://www.google.co.ck/search*", "*://www.google.cl/search*", "*://www.google.cm/search*", "*://www.google.cn/search*", "*://www.google.com.co/search*", "*://www.google.co.cr/search*", "*://www.google.com.cu/search*", "*://www.google.cv/search*", "*://www.google.com.cy/search*", "*://www.google.cz/search*", "*://www.google.de/search*", "*://www.google.dj/search*", "*://www.google.dk/search*", "*://www.google.dm/search*", "*://www.google.com.do/search*", "*://www.google.dz/search*", "*://www.google.com.ec/search*", "*://www.google.ee/search*", "*://www.google.com.eg/search*", "*://www.google.es/search*", "*://www.google.com.et/search*", "*://www.google.fi/search*", "*://www.google.com.fj/search*", "*://www.google.fm/search*", "*://www.google.fr/search*", "*://www.google.ga/search*", "*://www.google.ge/search*", "*://www.google.gg/search*", "*://www.google.com.gh/search*", "*://www.google.com.gi/search*", "*://www.google.gl/search*", "*://www.google.gm/search*", "*://www.google.gp/search*", "*://www.google.gr/search*", "*://www.google.com.gt/search*", "*://www.google.gy/search*", "*://www.google.com.hk/search*", "*://www.google.hn/search*", "*://www.google.hr/search*", "*://www.google.ht/search*", "*://www.google.hu/search*", "*://www.google.co.id/search*", "*://www.google.ie/search*", "*://www.google.co.il/search*", "*://www.google.im/search*", "*://www.google.co.in/search*", "*://www.google.iq/search*", "*://www.google.is/search*", "*://www.google.it/search*", "*://www.google.je/search*", "*://www.google.com.jm/search*", "*://www.google.jo/search*", "*://www.google.co.jp/search*", "*://www.google.co.ke/search*", "*://www.google.com.kh/search*", "*://www.google.ki/search*", "*://www.google.kg/search*", "*://www.google.co.kr/search*", "*://www.google.com.kw/search*", "*://www.google.kz/search*", "*://www.google.la/search*", "*://www.google.com.lb/search*", "*://www.google.li/search*", "*://www.google.lk/search*", "*://www.google.co.ls/search*", "*://www.google.lt/search*", "*://www.google.lu/search*", "*://www.google.lv/search*", "*://www.google.com.ly/search*", "*://www.google.co.ma/search*", "*://www.google.md/search*", "*://www.google.me/search*", "*://www.google.mg/search*", "*://www.google.mk/search*", "*://www.google.ml/search*", "*://www.google.com.mm/search*", "*://www.google.mn/search*", "*://www.google.ms/search*", "*://www.google.com.mt/search*", "*://www.google.mu/search*", "*://www.google.mv/search*", "*://www.google.mw/search*", "*://www.google.com.mx/search*", "*://www.google.com.my/search*", "*://www.google.co.mz/search*", "*://www.google.com.na/search*", "*://www.google.com.nf/search*", "*://www.google.com.ng/search*", "*://www.google.com.ni/search*", "*://www.google.ne/search*", "*://www.google.nl/search*", "*://www.google.no/search*", "*://www.google.com.np/search*", "*://www.google.nr/search*", "*://www.google.nu/search*", "*://www.google.co.nz/search*", "*://www.google.com.om/search*", "*://www.google.com.pa/search*", "*://www.google.com.pe/search*", "*://www.google.com.pg/search*", "*://www.google.com.ph/search*", "*://www.google.com.pk/search*", "*://www.google.pl/search*", "*://www.google.pn/search*", "*://www.google.com.pr/search*", "*://www.google.ps/search*", "*://www.google.pt/search*", "*://www.google.com.py/search*", "*://www.google.com.qa/search*", "*://www.google.ro/search*", "*://www.google.ru/search*", "*://www.google.rw/search*", "*://www.google.com.sa/search*", "*://www.google.com.sb/search*", "*://www.google.sc/search*", "*://www.google.se/search*", "*://www.google.com.sg/search*", "*://www.google.sh/search*", "*://www.google.si/search*", "*://www.google.sk/search*", "*://www.google.com.sl/search*", "*://www.google.sn/search*", "*://www.google.so/search*", "*://www.google.sm/search*", "*://www.google.sr/search*", "*://www.google.st/search*", "*://www.google.com.sv/search*", "*://www.google.td/search*", "*://www.google.tg/search*", "*://www.google.co.th/search*", "*://www.google.com.tj/search*", "*://www.google.tk/search*", "*://www.google.tl/search*", "*://www.google.tm/search*", "*://www.google.tn/search*", "*://www.google.to/search*", "*://www.google.com.tr/search*", "*://www.google.tt/search*", "*://www.google.com.tw/search*", "*://www.google.co.tz/search*", "*://www.google.com.ua/search*", "*://www.google.co.ug/search*", "*://www.google.co.uk/search*", "*://www.google.com.uy/search*", "*://www.google.co.uz/search*", "*://www.google.com.vc/search*", "*://www.google.co.ve/search*", "*://www.google.vg/search*", "*://www.google.co.vi/search*", "*://www.google.com.vn/search*", "*://www.google.vu/search*", "*://www.google.ws/search*", "*://www.google.rs/search*", "*://www.google.co.za/search*", "*://www.google.co.zm/search*", "*://www.google.co.zw/search*", "*://www.google.cat/search*" ] } ]; const OPTIONS_PAGE_PATH = "options_page.html"; const POPUP_PAGE_PATH = null; const EXTENSION_ICON = "data:image/png;base64,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"; const extensionCssData = {}; const LOCALE_KEYS = {}; const USED_LOCALE = "en"; const CURRENT_LOCATION = window.location.href; const convertMatchPatternToRegExp = function convertMatchPatternToRegExp(pattern) { if (pattern === "") return new RegExp(".*"); try { const singleEscapedPattern = convertMatchPatternToRegExpString(pattern).replace(/\\\\/g, "\\"); return new RegExp(singleEscapedPattern); } catch (error) { debug("Error converting match pattern to RegExp: %s, Error: %s", pattern, error.message); return new RegExp("$."); } }; const convertMatchPatternToRegExpString = function convertMatchPatternToRegExpString(pattern) { function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\\\$&"); } if (typeof pattern !== "string" || !pattern) return "$."; const schemeMatch = pattern.match(/^(\*|https?|file|ftp):\/\//); if (!schemeMatch) return "$."; const scheme = schemeMatch[1]; pattern = pattern.substring(schemeMatch[0].length); const schemeRegex = scheme === "*" ? "https?|file|ftp" : scheme, hostMatch = pattern.match(/^([^\/]+)/); if (!hostMatch) return "$."; const host = hostMatch[1]; pattern = pattern.substring(host.length); let hostRegex; if (host === "*") hostRegex = "[^/]+"; else if (host.startsWith("*.")) hostRegex = "(?:[^\\/]+\\.)?" + escapeRegex(host.substring(2)); else hostRegex = escapeRegex(host); let pathRegex = pattern; if (!pathRegex.startsWith("/")) pathRegex = "/" + pathRegex; pathRegex = pathRegex.split("*").map(escapeRegex).join(".*"); if (pathRegex === "/.*") pathRegex = "(?:/.*)?"; else pathRegex = pathRegex + "(?:[?#]|$)"; return `^${schemeRegex}:\\/\\/${hostRegex}${pathRegex}`; }; const ALL_PERMISSIONS = [ ...(INJECTED_MANIFEST.permissions || []), ...(INJECTED_MANIFEST.optional_permissions || []), ...(INJECTED_MANIFEST.host_permissions || []), ...(INJECTED_MANIFEST.content_scripts ?.map((cs) => cs.matches || []) ?.flat() || []), ]; const isOrigin = (perm) => { if ( perm.startsWith("*://") || perm.startsWith("http://") || perm.startsWith("https://") ) { return true; } return false; }; const ORIGIN_PERMISSIONS = ALL_PERMISSIONS.filter(isOrigin); const EXTENSION_PERMISSIONS = ALL_PERMISSIONS.filter((perm) => !isOrigin(perm)); function _testBlobCSP() { try { const code = `console.log("Blob CSP test");`; const blob = new Blob([code], { type: "application/javascript" }); const blobUrl = URL.createObjectURL(blob); const script = document.createElement("script"); script.src = blobUrl; let blocked = false; script.onerror = () => { blocked = true; }; document.head.appendChild(script); return new Promise((resolve) => { setTimeout(() => { resolve(!blocked); document.head.removeChild(script); URL.revokeObjectURL(blobUrl); }, 100); }); } catch (e) { return Promise.resolve(false); } } let CAN_USE_BLOB_CSP = false; const waitForDOMEnd = () => { if (document.readyState === "loading") { return new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }) ); } return Promise.resolve(); }; waitForDOMEnd().then(() => { _testBlobCSP().then((result) => { CAN_USE_BLOB_CSP = result; }); }); function _base64ToBlob(base64, mimeType = "application/octet-stream") { const binary = atob(base64); const len = binary.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i); return new Blob([bytes], { type: mimeType }); } function _getMimeTypeFromPath(p) { const ext = (p.split(".").pop() || "").toLowerCase(); const map = { html: "text/html", htm: "text/html", js: "text/javascript", css: "text/css", json: "application/json", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", webp: "image/webp", ico: "image/x-icon", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", eot: "application/vnd.ms-fontobject", }; return map[ext] || "application/octet-stream"; } function _isTextAsset(ext) { return ["html", "htm", "js", "css", "json", "svg", "txt", "xml"].includes( ext ); } function _createAssetUrl(path = "") { if (path.startsWith("/")) path = path.slice(1); const assetData = EXTENSION_ASSETS_MAP[path]; if (typeof assetData === "undefined") { _warn("[runtime.getURL] Asset not found for", path); return path; } const mime = _getMimeTypeFromPath(path); const ext = (path.split(".").pop() || "").toLowerCase(); if (CAN_USE_BLOB_CSP) { let blob; if (_isTextAsset(ext)) { blob = new Blob([assetData], { type: mime }); } else { blob = _base64ToBlob(assetData, mime); } return URL.createObjectURL(blob); } else { if (_isTextAsset(ext)) { return `data:${mime};base64,${btoa(assetData)}`; } else { return `data:${mime};base64,${assetData}`; } } } function _matchGlobPattern(pattern, path) { if (!pattern || !path) return false; pattern = pattern.replace(/\\/g, "/"); path = path.replace(/\\/g, "/"); if (pattern === path) return true; let regexPattern = pattern .replace(/[.+?^${}()|[\]\\]/g, "\\$&") // Escape regex chars .replace(/\*\*/g, "__DOUBLESTAR__") // Temporarily replace ** .replace(/\*/g, "[^/]*") // * matches any chars except / .replace(/__DOUBLESTAR__/g, ".*"); // ** matches any chars including / regexPattern = "^" + regexPattern + "$"; try { const regex = new RegExp(regexPattern); return regex.test(path); } catch (e) { _error(`Invalid glob pattern: ${pattern}`, e); return false; } } function _isWebAccessibleResource(resourcePath, webAccessibleResources) { if ( !Array.isArray(webAccessibleResources) || webAccessibleResources.length === 0 ) { return false; } // Normalize the resource path const normalizedPath = resourcePath.replace(/\\/g, "/").replace(/^\/+/, ""); for (const webAccessibleResource of webAccessibleResources) { let patterns = []; // Handle both manifest v2 and v3 formats if (typeof webAccessibleResource === "string") { // Manifest v2 format: array of strings patterns = [webAccessibleResource]; } else if ( webAccessibleResource && Array.isArray(webAccessibleResource.resources) ) { // Manifest v3 format: objects with resources array patterns = webAccessibleResource.resources; } // Check if the path matches any pattern for (const pattern of patterns) { if (_matchGlobPattern(pattern, normalizedPath)) { return true; } } } return false; } window._matchGlobPattern = _matchGlobPattern; window._isWebAccessibleResource = _isWebAccessibleResource; // This function contains all the CSS injection and JS execution, // ordered by run_at timing internally using await. // #region Script Execution Logic async function executeAllScripts(globalThis, extensionCssData) { const {chrome, browser, global, window, self} = globalThis; const scriptName = "Web Search Navigator"; _log(`Starting execution phases...`); // #region Document Start if (typeof document !== 'undefined') { _log(`Executing document-start phase...`); const scriptPaths = []; _log(` Executing JS (start): ${scriptPaths}`); try { // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically) with (globalThis){; ;} } catch(e) { _error(` Error executing scripts ${scriptPaths}`, e); } } else { _log(`Skipping document-start phase (no document).`); } // #endregion // #region Wait for Document End DOMContentLoaded --- if (typeof document !== 'undefined' && document.readyState === 'loading') { _log(`Waiting for DOMContentLoaded...`); await new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, { once: true })); _log(`DOMContentLoaded fired.`); } else if (typeof document !== 'undefined') { _log(`DOMContentLoaded already passed or not applicable.`); } // #endregion // #region Document End if (typeof document !== 'undefined') { _log(`Executing document-end phase...`); const scriptPaths = ["mousetrap.js","mousetrap-global-bind.js","options.js","search_engines.js","main.js"]; _log(` Executing JS (end): ${scriptPaths}`); try { // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically) with (globalThis){; // START: mousetrap.js !function(e,t,n){if(e){for(var r,o={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},a={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},i={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},c={option:"alt",command:"meta",return:"enter",escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},u=1;u<20;++u)o[111+u]="f"+u;for(u=0;u<=9;++u)o[u+96]=u.toString();y.prototype.bind=function(e,t,n){var r=this;return e=e instanceof Array?e:[e],r._bindMultiple.call(r,e,t,n),r},y.prototype.unbind=function(e,t){return this.bind.call(this,e,(function(){}),t)},y.prototype.trigger=function(e,t){var n=this;return n._directMap[e+":"+t]&&n._directMap[e+":"+t]({},e),n},y.prototype.reset=function(){var e=this;return e._callbacks={},e._directMap={},e},y.prototype.stopCallback=function(e,t){if((" "+t.className+" ").indexOf(" mousetrap ")>-1)return!1;if(d(t,this.target))return!1;if("composedPath"in e&&"function"==typeof e.composedPath){var n=e.composedPath()[0];n!==e.target&&(t=n)}return"INPUT"==t.tagName||"SELECT"==t.tagName||"TEXTAREA"==t.tagName||t.isContentEditable},y.prototype.handleKey=function(){var e=this;return e._handleKey.apply(e,arguments)},y.addKeycodes=function(e){for(var t in e)e.hasOwnProperty(t)&&(o[t]=e[t]);r=null},y.init=function(){var e=y(t);for(var n in e)"_"!==n.charAt(0)&&(y[n]=function(t){return function(){return e[t].apply(e,arguments)}}(n))},y.init(),e.Mousetrap=y,"undefined"!=typeof module&&module.exports&&(module.exports=y),"function"==typeof define&&define.amd&&define((function(){return y}))}function l(e,t,n){e.addEventListener?e.addEventListener(t,n,!1):e.attachEvent("on"+t,n)}function s(e){if("keypress"==e.type){var t=String.fromCharCode(e.which);return e.shiftKey||(t=t.toLowerCase()),t}return o[e.which]?o[e.which]:a[e.which]?a[e.which]:String.fromCharCode(e.which).toLowerCase()}function f(e){return"shift"==e||"ctrl"==e||"alt"==e||"meta"==e}function p(e,t,n){return n||(n=function(){if(!r)for(var e in r={},o)e>95&&e<112||o.hasOwnProperty(e)&&(r[o[e]]=e);return r}()[e]?"keydown":"keypress"),"keypress"==n&&t.length&&(n="keydown"),n}function h(e,t){var n,r,o,a=[];for(n=function(e){return"+"===e?["+"]:(e=e.replace(/\+{2}/g,"+plus")).split("+")}(e),o=0;o1?k(e,c,t,r):(i=h(e,r),n._callbacks[i.key]=n._callbacks[i.key]||[],p(i.key,i.modifiers,{type:i.action},o,e,a),n._callbacks[i.key][o?"unshift":"push"]({callback:t,modifiers:i.modifiers,action:i.action,seq:o,level:a,combo:e}))}n._handleKey=function(e,t,n){var r,o=p(e,t,n),a={},l=0,s=!1;for(r=0;r { // Alternative: kb.split(/, */); return kb.split(',').map((t) => t.trim()); }; // eslint-disable-next-line no-unused-vars const keybindingArrayToString = (kb) => { return kb.join(', '); }; /** * @param {StorageArea} storage The storage area to which this section will * write. * @param {Object} defaultValues The default options. * @constructor */ class BrowserStorage { constructor(storage, defaultValues) { this.storage = storage; this.values = {}; this.defaultValues = defaultValues; } load() { // this.storage.get(null) returns all the data stored: // https://developer.chrome.com/extensions/storage#method-StorageArea-get return this.storage.get(null).then((values) => { this.values = values; // Prior to versions 0.4.* the keybindings were stored as strings, so we // migrate them to arrays if needed. let migrated = false; for (const [key, value] of Object.entries(this.values)) { if (!(key in DEFAULT_KEYBINDINGS) || Array.isArray(value)) { continue; } migrated = true; this.values[key] = keybindingStringToArray(value); } if (migrated) { return this.save(); } }); } save() { return this.storage.set(this.values); } get(key) { const value = this.values[key]; if (value != null) { return value; } return this.defaultValues[key]; } set(key, value) { this.values[key] = value; } clear() { return this.storage.clear().then(() => { this.values = {}; }); } getAll() { // Merge options from storage with defaults. return {...this.defaultValues, ...this.values}; } } const createSyncedOptions = () => { return new BrowserStorage(browser.storage.sync, DEFAULT_OPTIONS); }; // eslint-disable-next-line no-unused-vars class ExtensionOptions { constructor() { this.sync = createSyncedOptions(); this.local = new BrowserStorage(browser.storage.local, { lastQueryUrl: null, lastFocusedIndex: 0, }); } load() { return Promise.all([this.local.load(), this.sync.load()]); } } // END: options.js // START: search_engines.js /** * This file contains search engine specific logic via search engine objects. * * A search engine object must provide the following: * - {regex} urlPattern * - {CSS selector} searchBoxSelector * - {SearchResult[]} getSearchResults() * * Optional functions/properties: * - {Array} tabs * Default: {} * - {int} getTopMargin: used if top results are not entirely visible * Default: 0 * - {int} getBottomMargin: used if bottom results are not entirely visible. * Relevant for some search engines, since Firefox and Chrome show a tooltip * with the URL of focused links at the bottom and can hide some of the * search result at the bottom. * Default: getDefaultBottomMargin() * - {Function} onChangedResults: function for registering a callback on * changed search results. The callback gets a single boolean parameter that * is set to true if the only change is newly appended results. * Default: null (meaning there's no support for such events) * - {None} changeTools(period) * * Every SearchResult must provide the element and highlightClass properties and * optionally the following: * - {Callback} anchorSelector: callback for getting the anchor * Default: the element itself * - {Callback} highlightedElementSelector: callback for getting the * highlighted element * Default: the element itself * - {Callback} containerSelector: callback for getting the container that * needs to be visible when an element is selected. * Default: the element itself */ class SearchResult { // We must declare the private class fields. #element; #anchorSelector; #highlightedElementSelector; #containerSelector; /** * @param {Element} element * @param {function|null} anchorSelector * @param {string} highlightClass * @param {function|null} highlightedElementSelector * @param {function|null} containerSelector */ constructor( element, anchorSelector, highlightClass, highlightedElementSelector, containerSelector, ) { this.#element = element; this.#anchorSelector = anchorSelector; this.highlightClass = highlightClass; this.#highlightedElementSelector = highlightedElementSelector; this.#containerSelector = containerSelector; } get anchor() { if (!this.#anchorSelector) { return this.#element; } return this.#anchorSelector(this.#element); } get container() { if (!this.#containerSelector) { return this.#element; } return this.#containerSelector(this.#element); } get highlightedElement() { if (!this.#highlightedElementSelector) { return this.#element; } return this.#highlightedElementSelector(this.#element); } } // eslint-disable-next-line /** * @param {Array} includedSearchResults An array of * tuples. Each tuple contains collection of the search results optionally * accompanied with their container selector. * @constructor */ const getSortedSearchResults = ( includedSearchResults, excludedNodeList = [], ) => { const excludedResultsSet = new Set(); for (const node of excludedNodeList) { excludedResultsSet.add(node); } const searchResults = []; for (const results of includedSearchResults) { for (const node of results.nodes) { const searchResult = new SearchResult( node, results.anchorSelector, results.highlightClass, results.highlightedElementSelector, results.containerSelector, ); const anchor = searchResult.anchor; // Use offsetParent to exclude hidden elements, see: // https://stackoverflow.com/a/21696585/1014208 if ( anchor != null && !excludedResultsSet.has(anchor) && anchor.offsetParent !== null ) { // Prevent adding the same node multiple times. excludedResultsSet.add(anchor); searchResults.push(searchResult); } } } // Sort searchResults by their document position. searchResults.sort((a, b) => { const position = a.anchor.compareDocumentPosition(b.anchor); if (position & Node.DOCUMENT_POSITION_FOLLOWING) { return -1; } else if (position & Node.DOCUMENT_POSITION_PRECEDING) { return 1; } else { return 0; } }); return searchResults; }; const getFixedSearchBoxTopMargin = (searchBoxContainer, element) => { // When scrolling down, the search box can have a fixed position and can hide // search results, so we try to compensate for it. if (!searchBoxContainer || searchBoxContainer.contains(element)) { return 0; } return searchBoxContainer.getBoundingClientRect().height; }; // https://stackoverflow.com/a/7000222/2870889 // eslint-disable-next-line no-unused-vars const isFirefox = () => { return navigator.userAgent.toLowerCase().indexOf('firefox') >= 0; }; // eslint-disable-next-line no-unused-vars const getDefaultBottomMargin = (element) => { return 28; }; const selectorElementGetter = (selector) => { return () => { return document.querySelector(selector); }; }; const nParent = (element, n) => { while (n > 0 && element) { element = element.parentElement; n--; } return element; }; const debounce = (callback, delayMs) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { return callback(...args); }, delayMs); }; }; class GoogleSearch { constructor(options) { this.options = options; } get urlPattern() { return /^https:\/\/(www\.)?google\./; } get searchBoxSelector() { // Must match search engine search box // NOTE: we used '#searchform input[name=q]' before 2020-06-05 but that // doesn't work in the images search tab. Another option is to use // 'input[role="combobox"]' but this doesn't work when there's also a // dictionary search box. // return '#searchform input[name=q]', return 'form[role=search] [name=q]'; } getTopMargin(element) { return getFixedSearchBoxTopMargin( document.querySelector('#searchform.minidiv'), element, ); } getBottomMargin(element) { return isFirefox() ? 0 : getDefaultBottomMargin(); } onChangedResults(callback) { if (GoogleSearch.#isImagesTab()) { return GoogleSearch.#onImageSearchResults(callback); } if (this.options.googleIncludeMemex) { return GoogleSearch.#onMemexResults(callback); } // https://github.com/infokiller/web-search-navigator/issues/464 const container = document.querySelector('#rcnt'); if (!container) { return; } const observer = new MutationObserver( debounce((mutationsList, observer) => { callback(true); }, 50), ); observer.observe(container, { attributes: false, childList: true, subtree: true, }); } static #isImagesTab() { const searchParams = new URLSearchParams(window.location.search); return searchParams.get('tbm') === 'isch'; } static #getImagesTabResults() { const includedElements = [ // Image links { nodes: document.querySelectorAll('.islrc a[data-nav="1"]'), highlightClass: 'wsn-google-focused-image', }, // Show more results button { nodes: document.querySelectorAll('#islmp [type="button"]'), highlightClass: 'wsn-google-focused-image', }, ]; return getSortedSearchResults(includedElements, []); } static #regularResults() { return [ { nodes: document.querySelectorAll('#search .r > a:first-of-type'), highlightClass: 'wsn-google-focused-link', containerSelector: (n) => n.parentElement.parentElement, }, { nodes: document.querySelectorAll('#search .r g-link > a:first-of-type'), highlightClass: 'wsn-google-focused-link', containerSelector: (n) => n.parentElement.parentElement, }, // More results button in continous loading // https://imgur.com/a/X9zyJ24 { nodes: document.querySelectorAll( '#botstuff a[href^="/search"][href*="start="] h3', ), highlightClass: 'wsn-google-focused-link', anchorSelector: (n) => n.closest('a'), }, // Continuously loaded results are *sometimes* in the #botstuff container // https://imgur.com/a/s6ow0La { nodes: document.querySelectorAll('#botstuff a h3'), highlightClass: 'wsn-google-focused-link', containerSelector: (n) => nParent(n, 5), highlightedElementSelector: (n) => nParent(n, 5), anchorSelector: (n) => n.closest('a'), }, // Sometimes featured snippets are not contained in #search (possibly when // there are large images?): https://imgur.com/a/VluRKIQ { nodes: document.querySelectorAll('.xpdopen .g a'), highlightClass: 'wsn-google-focused-link', highlightedElementSelector: (n) => n.querySelector('h3'), }, // Large YouTube video as top result: https://imgur.com/a/JIe62QV { nodes: document.querySelectorAll('h3 a[href*="youtube.com"]'), highlightClass: 'wsn-google-focused-link', highlightedElementSelector: (n) => n.closest('h3'), }, // Sub-results: https://imgur.com/a/CJePYJM { nodes: document.querySelectorAll('#search h3 a:first-of-type'), highlightClass: 'wsn-google-focused-link', highlightedElementSelector: (n) => n.closest('h3'), containerSelector: (n) => n.closest('tr'), }, // Shopping results: https://imgur.com/a/wccM2iq { nodes: document.querySelectorAll('#rso a h4'), anchorSelector: (n) => n.closest('a'), highlightClass: 'wsn-google-focused-card', highlightedElementSelector: (n) => n.closest('.sh-dgr__content'), }, // News tab: https://imgur.com/a/MR9q31f { nodes: document.querySelectorAll('#search g-card a'), highlightClass: 'wsn-google-focused-link', }, // Jobs heading for the jobs cards section. Clicking on it takes you // to Google's job search. // As of 2023-05-28, the Google's jobs search URLs seem to contain two // query string params which seem relevant: // #endregion // #endregion // #endregion // #region htivrt jobs // The first one matches the jobs heading, but also buttons in the // jobs UI such as filtering by WFH/in-office. Therefore, we use the // second one for specific jobs, but the first one to detect the jobs // heading (otherwise it would be matched later in vaccines). // eslint-disable-next-line max-len // const jobsSelector = '#search a:is([href*="ibp=htl;jobs"], [href*="htivrt=jobs"])'; // NOTE: this must be added to the included elements before: // #endregion // #region books and featured snippets // TODO: add screenshot { nodes: document.querySelectorAll( // eslint-disable-next-line max-len '#search a:is([href*="ibp=htl;jobs"],[href*="htivrt=jobs"]) [role=heading][aria-level="2"]', ), anchorSelector: (n) => n.closest('a'), // highlightedElementSelector: (n) => n.closest('li'), highlightClass: 'wsn-google-focused-job-card', }, // Same as above, but for specific job results. // TODO: add screenshot // Jobs cards { // nodes: document.querySelectorAll('#search a[href*="htivrt=jobs"]'), // eslint-disable-next-line max-len nodes: document.querySelectorAll('#search li a[href*="htivrt=jobs"]'), highlightedElementSelector: (n) => n.closest('li'), highlightClass: 'wsn-google-focused-job-card', }, // Books tab: https://imgur.com/a/QSBIOb6 // NOTE: This is required for matching "features snippets" in the general // search tab, and also matches other results. { nodes: document.querySelectorAll('#search [data-hveid] a h3'), anchorSelector: (n) => n.closest('a'), containerSelector: (n) => n.closest('[data-hveid]'), highlightedElementSelector: (n) => n.closest('[data-hveid]'), highlightClass: 'wsn-google-focused-link', }, // Next/previous results page { nodes: document.querySelectorAll('#pnprev, #pnnext'), highlightClass: 'wsn-google-card-item', }, ]; } static #cardResults() { const nearestChildOrSiblingOrParentAnchor = (element) => { const childAnchor = element.querySelector('a'); if (childAnchor && childAnchor.href) { return childAnchor; } const siblingAnchor = element.parentElement.querySelector('a'); if (siblingAnchor && siblingAnchor.href) { return siblingAnchor; } return element.closest('a'); }; const nearestCardContainer = (element) => { return element.closest('g-inner-card'); }; return [ // Twitter: https://imgur.com/a/fdI75JG { nodes: document.querySelectorAll( '#search [data-init-vis=true] [role=heading]', ), anchorSelector: nearestChildOrSiblingOrParentAnchor, highlightedElementSelector: nearestCardContainer, highlightClass: 'wsn-google-focused-card', }, // Vertical "Top stories" results { nodes: document.querySelectorAll('#search [role=text] [role=heading]'), anchorSelector: nearestChildOrSiblingOrParentAnchor, highlightClass: 'wsn-google-focused-link', }, // Vertical video results: https://imgur.com/a/GyKhwrx // Vertical video results: https://imgur.com/a/8fbPnvT { nodes: document.querySelectorAll( '#search video-voyager a [role=heading]', ), anchorSelector: nearestChildOrSiblingOrParentAnchor, containerSelector: nearestChildOrSiblingOrParentAnchor, highlightedElementSelector: nearestChildOrSiblingOrParentAnchor, highlightClass: 'wsn-google-focused-link', }, // Horizontal video results: https://imgur.com/a/gRGJ7l9 // People also search for: https://imgur.com/a/QpCHKt0 { nodes: document.querySelectorAll( '#search g-scrolling-carousel g-inner-card a [role=heading]', ), anchorSelector: nearestChildOrSiblingOrParentAnchor, containerSelector: nearestCardContainer, highlightedElementSelector: nearestCardContainer, highlightClass: 'wsn-google-card-item', }, // Vaccines: https://imgur.com/a/325qJzE { nodes: document.querySelectorAll( '#search a.a-no-hover-decoration [role=heading]', ), anchorSelector: nearestChildOrSiblingOrParentAnchor, containerSelector: nearestChildOrSiblingOrParentAnchor, highlightedElementSelector: nearestChildOrSiblingOrParentAnchor, highlightClass: 'wsn-google-focused-link', }, // Things to do in X: https://imgur.com/a/ibXwiuT { nodes: document.querySelectorAll('td a [role=heading]'), anchorSelector: nearestChildOrSiblingOrParentAnchor, containerSelector: (n) => n.closest('td'), highlightedElementSelector: (n) => n.closest('td'), highlightClass: 'wsn-google-card-item', }, // Vertical Maps/Places: https://imgur.com/a/JXrxBCj // Vertical recipes: https://imgur.com/a/3r7klHk // Top stories grid: https://imgur.com/a/mY93YRF // TODO: fix the small movements in recipes item selection. { nodes: document.querySelectorAll('a [role=heading]'), anchorSelector: nearestChildOrSiblingOrParentAnchor, containerSelector: nearestChildOrSiblingOrParentAnchor, highlightedElementSelector: nearestChildOrSiblingOrParentAnchor, highlightClass: 'wsn-google-card-item', }, ]; } static #placesResults() { const nodes = document.querySelectorAll('.vk_c a'); // The first node is usually the map image which needs to be styled // differently. let map; let links = nodes; if (nodes[0] != null && nodes[0].querySelector('img')) { map = nodes[0]; links = Array.from(nodes).slice(1); } const results = []; if (map != null) { results.push({ nodes: [map], highlightedElementSelector: (n) => n.parentElement, highlightClass: 'wsn-google-focused-map', }); } results.push({ nodes: links, highlightClass: 'wsn-google-focused-link', }); return results; } static #memexResults() { return [ { nodes: document.querySelectorAll( '#memexResults ._3d3zwUrsb4CVi1Li4H6CBw a', ), highlightClass: 'wsn-google-focused-memex-result', }, ]; } getSearchResults() { if (GoogleSearch.#isImagesTab()) { return GoogleSearch.#getImagesTabResults(); } const includedElements = GoogleSearch.#regularResults(); if (this.options.googleIncludeCards) { includedElements.push(...GoogleSearch.#cardResults()); } if (this.options.googleIncludePlaces) { includedElements.push(...GoogleSearch.#placesResults()); } if (this.options.googleIncludeMemex) { includedElements.push(...GoogleSearch.#memexResults()); } const excludedElements = document.querySelectorAll( [ // People also ask. Each one of the used selectors should be // sufficient, but we use both to be more robust to upstream DOM // changes. '.related-question-pair a', '#search .kp-blk:not(.c2xzTb) .r > a:first-of-type', // Right hand sidebar. We exclude it because it is after all the // results in the document order (as determined by // Node.DOCUMENT_POSITION_FOLLOWING used in getSortedSearchResults), // and it's confusing. '#rhs a', ].join(', '), ); return getSortedSearchResults(includedElements, excludedElements); } static #onImageSearchResults(callback) { const container = document.querySelector('.islrc'); if (!container) { return; } const observer = new MutationObserver( debounce((mutationsList, observer) => { callback(true); }, 50), ); observer.observe(container, { attributes: false, childList: true, subtree: false, }); } static #onMemexResults(callback) { const container = document.querySelector('#rhs'); if (!container) { return; } const observer = new MutationObserver( debounce((mutationsList, observer) => { if (document.querySelector('#memexResults') != null) { callback(true); } }, 50), ); observer.observe(container, { attributes: false, childList: true, subtree: true, }); } static #imageSearchTabs() { const visibleTabs = document.querySelectorAll('.T47uwc > a'); // NOTE: The order of the tabs after the first two is dependent on the // query. For example: // #endregion // #region california maps news videos return { navigateSearchTab: visibleTabs[0], navigateMapsTab: selectorElementGetter( '.T47uwc > a[href*="maps.google."]', ), navigateVideosTab: selectorElementGetter('.T47uwc > a[href*="&tbm=vid"]'), navigateNewsTab: selectorElementGetter('.T47uwc > a[href*="&tbm=nws"]'), navigateShoppingTab: selectorElementGetter( 'a[role="menuitem"][href*="&tbm=shop"]', ), navigateBooksTab: selectorElementGetter( 'a[role="menuitem"][href*="&tbm=bks"]', ), navigateFlightsTab: selectorElementGetter( 'a[role="menuitem"][href*="&tbm=flm"]', ), navigateFinancialTab: selectorElementGetter( 'a[role="menuitem"][href*="/finance?"]', ), // TODO: Disable image search's default keybindings to avoid confusing the // user, because the default keybindings can cause an indenepdent // navigation of the image results with another outline. The code below // doesn't work because the key event is captured by the image search // code, since Moustrap is bound on document, instead of a more specific // container. The following does work, but the code needs some changes to // support binding on a specific container per search engine: // // Mousetrap(document.querySelector('.islrc')).bind ... // Mousetrap(document.querySelector('#Sva75c')).bind ... // // navigatePreviousResultPage: null, // navigateNextResultPage: null, }; } // Array storing tuples of tabs navigation keybindings and their corresponding // CSS selector get previousPageButton() { if (GoogleSearch.#isImagesTab()) { return null; } return selectorElementGetter('#pnprev'); } get nextPageButton() { if (GoogleSearch.#isImagesTab()) { return null; } return selectorElementGetter('#pnnext'); } get tabs() { if (GoogleSearch.#isImagesTab()) { return GoogleSearch.#imageSearchTabs(); } return { navigateSearchTab: selectorElementGetter( // eslint-disable-next-line max-len 'a[href*="/search?q="]:not([href*="&tbm="]):not([href*="maps.google."])', ), navigateImagesTab: selectorElementGetter('a[href*="&tbm=isch"]'), navigateVideosTab: selectorElementGetter('a[href*="&tbm=vid"]'), navigateMapsTab: selectorElementGetter('a[href*="maps.google."]'), navigateNewsTab: selectorElementGetter('a[href*="&tbm=nws"]'), navigateShoppingTab: selectorElementGetter('a[href*="&tbm=shop"]'), navigateBooksTab: selectorElementGetter('a[href*="&tbm=bks"]'), navigateFlightsTab: selectorElementGetter('a[href*="&tbm=flm"]'), navigateFinancialTab: selectorElementGetter('[href*="/finance?"]'), }; } /** * Filter the results based on special properties * @param {*} period, filter identifier. Accepted filter are : * 'a' : all results * 'h' : last hour * 'd' : last day * 'w' : last week * 'm' : last month * 'y' : last year * 'v' : verbatim search * null : toggle sort */ // TODO: Refactor this function to get enums after migrating to typescript. changeTools(period) { const searchParams = new URLSearchParams(window.location.search); // Use the last value of the tbs param in case there are multiple ones, // since the last one overrides the previous ones. const allTbsValues = searchParams.getAll('tbs'); const lastTbsValue = allTbsValues[allTbsValues.length - 1] || ''; const match = /(qdr:.|li:1)(,sbd:.)?/.exec(lastTbsValue); const currentPeriod = (match && match[1]) || ''; const currentSort = (match && match[2]) || ''; if (period === 'a') { searchParams.delete('tbs'); } else if (period) { let newTbs = ''; if (period === 'v') { if (currentPeriod === 'li:1') { newTbs = ''; } else { newTbs = 'li:1'; } } else { newTbs = `qdr:${period}`; } searchParams.set('tbs', `${newTbs}${currentSort}`); // Can't apply sort when not using period. } else if (currentPeriod) { searchParams.set( 'tbs', `${currentPeriod}` + (currentSort ? '' : ',sbd:1'), ); } const newSearchString = '?' + searchParams.toString(); if (newSearchString !== window.location.search) { window.location.search = newSearchString; } return false; } changeImageSize(size) { const sizeOptions = { LARGE: {value: 0, name: 'Large', code: 'l'}, MEDIUM: {value: 1, name: 'Medium', code: 'e'}, ICON: {value: 2, name: 'Icon', code: 'i'}, }; const openTool = document.querySelector( '[class="PNyWAd ZXJQ7c"][jsname="I4bIT"]', ); if (openTool != null) { openTool.click(); } const openSizeDropDown = document.querySelector('[aria-label="Size"]'); if (openSizeDropDown != null) { openSizeDropDown.click(); } const dropDownWithSize = document.querySelector( '[class="xFo9P r9PaP Fmo8N"][jsname="wLFV5d"]', ); const getButton = (selector) => { let button; if (document.querySelector(selector) != null) { button = document.querySelector(selector); } else { button = null; } return button; }; const setImageSize = (dropDownWithSize, buttonSelector) => { let button = getButton(buttonSelector); if (dropDownWithSize == null && button != null) { button.click(); } else if (dropDownWithSize != null && button == null) { dropDownWithSize.click(); button = getButton(buttonSelector); button.click(); } else if (dropDownWithSize != null && button != null) { button.click(); } }; switch (size) { case sizeOptions.LARGE.code: if ( dropDownWithSize == null || dropDownWithSize.getAttribute('aria-label') != sizeOptions.LARGE.name ) { setImageSize( dropDownWithSize, '[class="MfLWbb"][aria-label="Large"]', ); } break; case sizeOptions.MEDIUM.code: if ( dropDownWithSize == null || dropDownWithSize.getAttribute('aria-label') != sizeOptions.MEDIUM.name ) { setImageSize( dropDownWithSize, '[class="MfLWbb"][aria-label="Medium"]', ); } break; case sizeOptions.ICON.code: if ( dropDownWithSize == null || dropDownWithSize.getAttribute('aria-label') != sizeOptions.ICON.name ) { setImageSize(dropDownWithSize, '[class="MfLWbb"][aria-label="Icon"]'); } break; default: break; } } } class BraveSearch { constructor(options) { this.options = options; } get urlPattern() { return /^https:\/\/search\.brave\.com/; } get searchBoxSelector() { return '.form-input, input[id=searchbox]'; } getTopMargin(element) { return getFixedSearchBoxTopMargin( document.querySelector('header.navbar'), element, ); } onChangedResults(callback) { const containers = document.querySelectorAll('#results'); const observer = new MutationObserver( debounce((mutationsList, observer) => { callback(true); }, 50), ); for (const container of containers) { observer.observe(container, { attributes: false, childList: true, subtree: true, }); } } static #getNewsTabResults() { const includedElements = [ { nodes: document.querySelectorAll('.snippet a'), highlightClass: 'wsn-brave-search-focused-news', containerSelector: (n) => n.parentElement, }, ]; return getSortedSearchResults(includedElements); } static #getVideosTabResults() { const includedElements = [ { nodes: document.querySelectorAll('.card a'), highlightClass: 'wsn-brave-search-focused-card', highlightedElementSelector: (n) => n.closest('.card'), containerSelector: (n) => n.parentElement, }, ]; return getSortedSearchResults(includedElements); } getSearchResults() { if (BraveSearch.#isTabActive(this.tabs.navigateNewsTab)) { return BraveSearch.#getNewsTabResults(); } else if (BraveSearch.#isTabActive(this.tabs.navigateVideosTab)) { return BraveSearch.#getVideosTabResults(); } const includedElements = [ { nodes: document.querySelectorAll('.snippet.fdb > a'), highlightClass: 'wsn-brave-search-focused-link', containerSelector: (n) => n.parentElement, }, // News cards { nodes: document.querySelectorAll( '.card[data-type="news"]:nth-child(-n+3)', ), highlightClass: 'wsn-brave-search-focused-card', }, // Video cards { nodes: document.querySelectorAll( '.card[data-type="videos"]:nth-child(-n+3)', ), highlightClass: 'wsn-brave-search-focused-card', }, ]; return getSortedSearchResults(includedElements); } static #isTabActive(tab) { return tab && tab.parentElement.classList.contains('active'); } get tabs() { return { navigateSearchTab: document.querySelector('a[href*="/search?q="]'), navigateImagesTab: document.querySelector( '#tab-images > a:first-of-type', ), navigateNewsTab: document.querySelector('a[href*="/news?q="]'), navigateVideosTab: document.querySelector( '#tab-videos > a:first-of-type', ), }; } } class StartPage { constructor(options) { this.options = options; } get urlPattern() { return /^https:\/\/(www\.)?startpage\./; } get searchBoxSelector() { return '#q'; } getTopMargin(element) { return getFixedSearchBoxTopMargin( document.querySelector('div.layout-web__header'), element, ); } getBottomMargin(element) { // Startpage in Firefox has an issue where trying to scroll can result in // window.scrollY being updated for a brief time although no scrolling is // done, which confuses the scrollToElement function, which can lead to // being stuck focused on a search result. return isFirefox() ? 0 : getDefaultBottomMargin(); } static #isSearchTab() { return document.querySelector('div.layout-web') != null; } static #isImagesTab() { return document.querySelector('div.layout-images') != null; } getSearchResults() { // Don't initialize results navigation on image search, since it doesn't // work there. if (StartPage.#isImagesTab()) { return []; } const containerSelector = (element) => { if (StartPage.#isSearchTab()) { return element.closest('.w-gl__result'); } return element; }; const includedElements = [ { nodes: document.querySelectorAll('a.w-gl__result-url'), highlightedElementSelector: containerSelector, highlightClass: 'wsn-startpage-focused-link', containerSelector: containerSelector, }, { nodes: document.querySelectorAll('.pagination--desktop button'), highlightClass: 'wsn-startpage-focused-link', }, // As of 2020-06-20, this doesn't seem to match anything. { nodes: document.querySelectorAll( '.vo-sp.vo-sp--default > a.vo-sp__link', ), highlightedElementSelector: containerSelector, highlightClass: 'wsn-startpage-focused-link', }, ]; const excludedElements = document.querySelectorAll('button[disabled]'); return getSortedSearchResults(includedElements, excludedElements); } get previousPageButton() { const menuLinks = document.querySelectorAll('.inline-nav-menu__link'); if (!menuLinks || menuLinks.length < 4) { return null; } return document.querySelector( 'form.pagination__form.next-prev-form--desktop:first-of-type', ); } get nextPageButton() { const menuLinks = document.querySelectorAll('.inline-nav-menu__link'); if (!menuLinks || menuLinks.length < 4) { return null; } return document.querySelector( 'form.pagination__form.next-prev-form--desktop:last-of-type', ); } get tabs() { const menuLinks = document.querySelectorAll('.inline-nav-menu__link'); if (!menuLinks || menuLinks.length < 4) { return {}; } return { navigateSearchTab: menuLinks[0], navigateImagesTab: menuLinks[1], navigateVideosTab: menuLinks[2], navigateNewsTab: menuLinks[3], }; } changeTools(period) { const forms = document.forms; let timeForm; for (let i = 0; i < forms.length; i++) { if (forms[i].className === 'search-filter-time__form') { timeForm = forms[i]; } } switch (period) { case 'd': timeForm.elements['with_date'][1].click(); break; case 'w': timeForm.elements['with_date'][2].click(); break; case 'm': timeForm.elements['with_date'][3].click(); break; case 'y': timeForm.elements['with_date'][4].click(); break; default: break; } } } class YouTube { constructor(options) { this.options = options; this.gridNavigation = false; } get urlPattern() { return /^https:\/\/(www)\.youtube\./; } get searchBoxSelector() { return 'input#search'; } getTopMargin(element) { return getFixedSearchBoxTopMargin( document.querySelector('#masthead-container'), element, ); } onChangedResults(callback) { // The ytd-section-list-renderer element may not exist yet when this code // runs, so we look for changes in the higher level elements until we find // ytd-section-list-renderer. const YT_CONTAINER_SELECTOR = [ 'ytd-section-list-renderer', '.ytd-section-list-renderer', 'ytd-rich-grid-renderer', 'ytd-shelf-renderer', ].join(','); const resultsObserver = new MutationObserver( debounce((mutationsList, observer) => { callback(true); }, 50), ); let lastLoadedURL = null; const pageObserverCallback = (mutationsList, observer) => { const url = window.location.pathname + window.location.search; if (url === lastLoadedURL) { return; } else { resultsObserver.disconnect(); } const containers = document.querySelectorAll(YT_CONTAINER_SELECTOR); if (containers.length == 0) { return; } lastLoadedURL = url; callback(false); for (const container of containers) { resultsObserver.observe(container, { attributes: false, childList: true, subtree: true, }); } }; // TODO: the observer callback is triggered many times because of the broad // changes that the observer tracks. I tried to use other observation specs // to limit it, but then it failed to detect URL changes without page load // (which is what happened in issue #337 [1]). // [1] https://github.com/infokiller/web-search-navigator/issues/337 const pageObserver = new MutationObserver( debounce(pageObserverCallback, 50), ); pageObserver.observe(document.querySelector('#page-manager'), { attributes: false, childList: true, subtree: true, }); } getSearchResults() { const includedElements = [ // Videos in vertical search results: https://imgur.com/a/Z8KV5Oe { nodes: document.querySelectorAll('a#video-title.ytd-video-renderer'), highlightClass: 'wsn-youtube-focused-video', highlightedElementSelector: (n) => n.closest('ytd-video-renderer'), containerSelector: (n) => n.closest('ytd-video-renderer'), }, // Playlist results in vertical search results: https://imgur.com/a/nPjGd9H { nodes: document.querySelectorAll( 'ytd-playlist-renderer a[href*="/playlist"]', ), highlightClass: 'wsn-youtube-focused-video', highlightedElementSelector: (n) => n.closest('ytd-playlist-renderer'), containerSelector: (n) => n.closest('ytd-playlist-renderer'), }, // Playlists { nodes: document.querySelectorAll('a.ytd-playlist-video-renderer'), highlightClass: 'wsn-youtube-focused-video', highlightedElementSelector: (n) => n.closest('ytd-playlist-video-renderer'), containerSelector: (n) => n.closest('ytd-playlist-video-renderer'), }, // Mixes { nodes: document.querySelectorAll('div#content a.ytd-radio-renderer'), highlightClass: 'wsn-youtube-focused-video', }, // Channels { nodes: document.querySelectorAll( 'ytd-grid-video-renderer a#video-title:not([aria-hidden="true"])', ), highlightClass: 'wsn-youtube-focused-grid-video', highlightedElementSelector: (n) => n.closest('ytd-grid-video-renderer'), containerSelector: (n) => n.closest('ytd-grid-video-renderer'), }, ]; // checking if homepage results are present const homePageElements = { nodes: document.querySelectorAll( 'ytd-rich-item-renderer a#video-title-link', ), highlightClass: 'wsn-youtube-focused-video', highlightedElementSelector: (n) => n.closest('ytd-rich-item-renderer'), containerSelector: (n) => n.closest('ytd-rich-item-renderer'), }; const results = getSortedSearchResults( [...includedElements, homePageElements], [], ); // When navigating away from the home page, the home page elements are still // in the DOM but they are not visible, so we must check if they are // visible (using offsetParent), not just if they are present. const isHomePage = Array.from(homePageElements.nodes).some( (n) => n.offsetParent != null, ); const gridRow = document.querySelector('ytd-rich-grid-row'); if (isHomePage && gridRow != null) { results.itemsPerRow = gridRow.getElementsByTagName( 'ytd-rich-item-renderer', ).length; results.gridNavigation = results.itemsPerRow > 0; } return results; } changeTools(period) { if (!document.querySelector('div#collapse-content')) { const toggleButton = document.querySelectorAll( 'a.ytd-toggle-button-renderer', )[0]; // Toggling the buttons ensures that div#collapse-content is loaded toggleButton.click(); toggleButton.click(); } const forms = document.querySelectorAll( 'div#collapse-content > *:first-of-type ytd-search-filter-renderer', ); let neededForm = null; switch (period) { case 'h': neededForm = forms[0]; break; case 'd': neededForm = forms[1]; break; case 'w': neededForm = forms[2]; break; case 'm': neededForm = forms[3]; break; case 'y': neededForm = forms[4]; break; } if (neededForm) { neededForm.childNodes[1].click(); } } } class GoogleScholar { constructor(options) { this.options = options; } get urlPattern() { return /^https:\/\/scholar\.google\./; } get searchBoxSelector() { return '#gs_hdr_tsi'; } getSearchResults() { const includedElements = [ { nodes: document.querySelectorAll('.gs_rt a'), highlightClass: 'wsn-google-focused-link', highlightedElementSelector: (n) => n.closest('.gs_rt'), containerSelector: (n) => n.parentElement.parentElement, }, { nodes: document.querySelectorAll( '.gs_ico_nav_previous, .gs_ico_nav_next', ), anchorSelector: (n) => n.parentElement, highlightClass: 'wsn-google-scholar-next-page', highlightedElementSelector: (n) => n.parentElement.children[1], containerSelector: (n) => n.parentElement.children[1], }, ]; return getSortedSearchResults(includedElements, []); } get previousPageButton() { const previousPageElement = document.querySelector('.gs_ico_nav_previous'); if (previousPageElement !== null) { return previousPageElement.parentElement; } return null; } get nextPageButton() { const nextPageElement = document.querySelector('.gs_ico_nav_next'); if (nextPageElement !== null) { return nextPageElement.parentElement; } return null; } } class Amazon { constructor(options) { this.options = options; } get urlPattern() { return /^https:\/\/(www\.)?amazon\./; } get searchBoxSelector() { return '#twotabsearchtextbox'; } onChangedResults(callback) { const container = document.querySelector('.s-main-slot'); if (!container) { return; } const observer = new MutationObserver( debounce((mutationsList, observer) => { callback(false); }, 50), ); observer.observe(container, { attributes: false, childList: true, subtree: false, }); } getSearchResults() { const includedElements = [ // Carousel items { nodes: document.querySelectorAll( '.s-main-slot .a-carousel-card h2 .a-link-normal.a-text-normal', ), highlightedElementSelector: (n) => n.closest('.a-carousel-card'), highlightClass: 'wsn-amazon-focused-carousel-item', containerSelector: (n) => n.closest('.a-carousel-card'), }, // Regular items. // NOTE: Must appear after the carousel items because this selector is // more general. { nodes: document.querySelectorAll( '.s-main-slot h2 .a-link-normal.a-text-normal', ), // highlightedElementSelector: (n) => n.parentElement.children[1], highlightedElementSelector: (n) => n.closest('.a-section').parentElement.closest('.a-section'), highlightClass: 'wsn-amazon-focused-item', containerSelector: (n) => n.closest('.a-section').parentElement.closest('.a-section'), }, // Next/previous and page numbers. { nodes: document.querySelectorAll('a.s-pagination-item'), highlightClass: 'wsn-amazon-focused-item', }, // Shopping card items { nodes: document.querySelectorAll( '.sc-list-item-content .a-list-item .a-link-normal', ), highlightClass: 'wsn-amazon-focused-cart-item', highlightedElementSelector: (n) => n.closest('.sc-list-item-content'), containerSelector: (n) => n.closest('.sc-list-item-content'), }, ]; // Exclude active page number and hidden carousel elements. // TODO: The hidden carousel elements do not match at page load because // they don't yet have the aria-hidden property set. const excludedElements = document.querySelectorAll( '.a-pagination .a-selected a, .a-carousel-card[aria-hidden="true"] a', ); return getSortedSearchResults(includedElements, excludedElements); } get previousPageButton() { return document.querySelector('a.s-pagination-previous'); } get nextPageButton() { return document.querySelector('a.s-pagination-next'); } } class Github { constructor(options) { this.options = options; } get urlPattern() { return /^https:\/\/(www\.)?github\.com/; } get searchBoxSelector() { // TODO: With the escape key, this only works the first time the keybinding // is used, Since Github seem to capture this as well, which causes it to // leave the search box. return 'input[name="q"]'; } static #getCommitSearchLinks() { const commitsContainers = document.querySelectorAll( '#commit_search_results .text-normal', ); const commits = []; for (const con of commitsContainers) { const links = con.querySelectorAll('a'); if (links.length === 0) { continue; } if (links.length === 1) { commits.push(links[0]); } else { const prLink = con.querySelector( 'a[data-hovercard-type="pull_request"]', ); if (prLink != null) { commits.push(prLink); } } } return commits; } getSearchResults() { const includedElements = [ // Repos { nodes: document.querySelectorAll('.repo-list a'), highlightClass: 'wsn-github-focused-item', containerSelector: (n) => n.closest('.mt-n1'), }, // Code { nodes: document.querySelectorAll('#code_search_results .text-normal a'), highlightClass: 'wsn-github-focused-item', }, // Commits/PRs { nodes: Github.#getCommitSearchLinks(), highlightClass: 'wsn-github-focused-item', }, // Issues { nodes: document.querySelectorAll( '#issue_search_results .text-normal a', ), highlightClass: 'wsn-github-focused-item', }, // Marketplace { nodes: document.querySelectorAll( '#marketplace_search_results .text-normal a', ), highlightClass: 'wsn-github-focused-item', }, // Topics { nodes: document.querySelectorAll( '#topic_search_results .text-normal a', ), highlightClass: 'wsn-github-focused-item', }, // Wikis { nodes: document.querySelectorAll('#wiki_search_results .text-normal a'), highlightClass: 'wsn-github-focused-item', }, // Users { nodes: document.querySelectorAll('#user_search_results .text-normal a'), highlightClass: 'wsn-github-focused-item', }, // Pinned repos in user profile { nodes: document.querySelectorAll( '.pinned-item-list-item-content span.repo', ), highlightClass: 'wsn-github-focused-item', highlightedElementSelector: (n) => n.closest('a'), containerSelector: (n) => n.closest('a'), anchorSelector: (n) => n.closest('a'), }, // Personal repos list in user profile { nodes: document.querySelectorAll( '#user-repositories-list a[itemprop*="codeRepository"]', ), highlightClass: 'wsn-github-focused-item', containerSelector: (n) => n.closest('li') || n, }, // Next/previous and page numbers. { nodes: document.querySelectorAll('.paginate-container a'), highlightClass: 'wsn-github-focused-pagination', }, ]; const searchParams = new URLSearchParams(window.location.search); // Starred repos of user if (searchParams.get('tab') === 'stars') { includedElements.push({ nodes: document.querySelectorAll('h3 a'), highlightClass: 'wsn-github-focused-item', }); } const excludedElements = [ // Exclude small links ...document.querySelectorAll('.muted-link, .Link--muted'), // Exclude topic tags ...document.querySelectorAll('.topic-tag'), // Exclude small links in commits // ...document.querySelectorAll( // '#commit_search_results .text-normal a.message'), ]; return getSortedSearchResults(includedElements, excludedElements); } onChangedResults(callback) { const container = document.querySelector('body'); if (!container) { return; } // Store the last URL to detect page navigations (for example going to the // next page of results). let lastURL = window.location.href; const observer = new MutationObserver( debounce((mutationsList, observer) => { let appendOnly = true; if (window.location.href !== lastURL) { lastURL = window.location.href; appendOnly = false; } callback(appendOnly); }, 50), ); observer.observe(container, { attributes: false, childList: true, subtree: false, }); } // Github already has built-in support for tabs: // https://docs.github.com/en/github/getting-started-with-github/keyboard-shortcuts get tabs() { return {}; } } class Gitlab { constructor(options) { this.options = options; } get urlPattern() { return /^https:\/\/(www\.)?gitlab\.com/; } get searchBoxSelector() { return '.form-input, input[id=search]'; } getTopMargin(element) { return getFixedSearchBoxTopMargin( document.querySelector('header.navbar'), element, ); } onChangedResults(callback) { const containers = document.querySelectorAll( '.projects-list, .groups-list, #content-body', ); const observer = new MutationObserver(async (mutationsList, observer) => { callback(true); }); for (const container of containers) { observer.observe(container, { attributes: false, childList: true, subtree: true, }); } } getSearchResults() { const includedElements = [ { nodes: document.querySelectorAll('li.project-row h2 a'), containerSelector: (n) => n.closest('li.project-row'), highlightedElementSelector: (n) => n.closest('li.project-row'), highlightClass: 'wsn-gitlab-focused-group-row', }, // Org subgroups, for example: // https://gitlab.archlinux.org/archlinux { nodes: document.querySelectorAll( 'ul.groups-list li.group-row a[aria-label]', ), containerSelector: (n) => n.closest('li.group-row'), highlightedElementSelector: (n) => n.closest('li.group-row'), highlightClass: 'wsn-gitlab-focused-group-row', }, // Prev/next page { nodes: document.querySelectorAll('li.page-item a.page-link'), containerSelector: (n) => n.closest('li.page-item'), highlightedElementSelector: (n) => n.closest('li.group-row'), highlightClass: 'wsn-gitlab-focused-group-row', }, ]; return getSortedSearchResults(includedElements); } } class CustomGitlab extends Gitlab { get urlPattern() { return new RegExp(this.options.customGitlabUrl); } } // Get search engine object matching the current url /* eslint-disable-next-line no-unused-vars */ const getSearchEngine = (options) => { const searchEngines = [ new GoogleSearch(options), new BraveSearch(options), new StartPage(options), new YouTube(options), new GoogleScholar(options), new Amazon(options), new Github(options), new Gitlab(options), new CustomGitlab(options), ]; // Switch over all compatible search engines const href = window.location.href; for (let i = 0; i < searchEngines.length; i++) { if (href.match(searchEngines[i].urlPattern)) { return searchEngines[i]; } } return null; }; // END: search_engines.js // START: main.js /* global ExtensionOptions, getSearchEngine, Mousetrap */ /* global getDefaultBottomMargin */ // TODO: Replace with enums when switching to typescript. const FOCUS_SCROLL_OFF = 0; const FOCUS_SCROLL_ON = 1; const FOCUS_SCROLL_ONLY = 2; // Returns true if scrolling was done. const scrollToElement = (searchEngine, element) => { if (element == null) { console.error('Cannot scroll to null element'); return; } let topMargin = 0; if (searchEngine.getTopMargin) { topMargin = searchEngine.getTopMargin(element); } let bottomMargin = getDefaultBottomMargin(); if (searchEngine.getBottomMargin) { bottomMargin = searchEngine.getBottomMargin(element); } const elementBounds = element.getBoundingClientRect(); const scrollY = window.scrollY; if (elementBounds.top < topMargin) { // scroll element to top element.scrollIntoView(true); window.scrollBy(0, -topMargin); } else if (elementBounds.bottom + bottomMargin > window.innerHeight) { // scroll element to bottom element.scrollIntoView(false); window.scrollBy(0, bottomMargin); } return Math.abs(window.scrollY - scrollY) > 0.01; }; const bindKeys = (bindings, toggle) => { // NOTE: Mousetrap calls the handler even if there's a larger sequence that // ends with the same key. For example, if the user binds both 'a b' and // 'b', when pressing the sequence 'a b' both handlers will be called, which // is not the desired behavior for this extension. Therefore, we first sort // all keybindings by their sequence length, so that handlers of larger // sequences will be called before the shorter ones. Then, we only call // other handlers if the previous handler returned true. bindings.sort((a, b) => { return b[0].split(' ').length - a[0].split(' ').length; }); let lastEvent; let lastHandlerResult; for (const [shortcut, element, global, callback] of bindings) { const wrappedCallback = (event) => { if (!toggle['active']) { return true; } if (event === lastEvent && !lastHandlerResult) { return; } lastEvent = event; lastHandlerResult = callback(event); return lastHandlerResult; }; if (global) { /* eslint-disable-next-line new-cap */ Mousetrap(element).bindGlobal(shortcut, wrappedCallback); } else { /* eslint-disable-next-line new-cap */ Mousetrap(element).bind(shortcut, wrappedCallback); } } }; class SearchResultsManager { constructor(searchEngine, options) { this.searchEngine = searchEngine; this.options = options; this.focusedIndex = -1; this.isInitialFocusSet = false; } reloadSearchResults() { this.searchResults = this.searchEngine.getSearchResults(); if (!this.isInitialFocusSet) { this.setInitialFocus(); } } setInitialFocus() { if (this.searchResults.length === 0) { return; } const lastNavigation = this.options.local.values; if ( location.href === lastNavigation.lastQueryUrl && lastNavigation.lastFocusedIndex >= 0 && lastNavigation.lastFocusedIndex < this.searchResults.length ) { this.focus(lastNavigation.lastFocusedIndex, FOCUS_SCROLL_ON); } else if (this.options.sync.get('autoSelectFirst')) { // Highlight the first result when the page is loaded, but don't scroll to // it because there may be KP cards such as stock graphs. this.focus(0, FOCUS_SCROLL_OFF); } } /** * Returns the element to click on upon navigation. The focused element in the * document is preferred (if there is one) over the highlighted result. Note * that the focused element does not have to be an anchor element. * * @param {boolean} linkOnly If true the focused element is preferred only * when it is a link with "href" attribute. * @return {Element} */ getElementToNavigate(linkOnly = false) { const focusedElement = document.activeElement; // StartPage seems to still focus and change it to body when the page loads. if (focusedElement == null || focusedElement.localName === 'body') { if ( this.focusedIndex < 0 || this.focusedIndex >= this.searchResults.length ) { return null; } return this.searchResults[this.focusedIndex].anchor; } const isLink = focusedElement.localName === 'a' && focusedElement.hasAttribute('href'); if (!linkOnly || isLink) { return focusedElement; } } highlight(searchResult) { const highlighted = searchResult.highlightedElement; if (highlighted == null) { console.error('No element to highlight: %o', highlighted); return; } highlighted.classList.add(searchResult.highlightClass); if ( this.options.sync.get('hideOutline') || searchResult.anchor !== highlighted ) { searchResult.anchor.classList.add('wsn-no-outline'); } } unhighlight(searchResult) { const highlighted = searchResult.highlightedElement; if (highlighted == null) { console.error('No element to unhighlight: %o', highlighted); return; } highlighted.classList.remove(searchResult.highlightClass); highlighted.classList.remove('wsn-no-outline'); } focus(index, scroll = FOCUS_SCROLL_ONLY) { if (this.focusedIndex >= 0) { const searchResult = this.searchResults[this.focusedIndex]; // If the current result is outside the viewport and FOCUS_SCROLL_ONLY was // requested, scroll to the current hidden result, but don't focus on the // new result. // This behavior is intended to handle cases where the user scrolls away // from the currently focused result and then presses the keybindings to // focus on the previous/next result. In this case, since the user // doesn't see the current result, it's more intuitive to only scroll to // the current result, and then on the next keypress they can focus on the // previous/next result and actually see on what result they want to focus // on. if ( scroll === FOCUS_SCROLL_ONLY && scrollToElement(this.searchEngine, searchResult.container) ) { return; } // Remove highlighting from previous item. this.unhighlight(searchResult); } const searchResult = this.searchResults[index]; if (!searchResult) { this.focusedIndex = -1; return; } this.highlight(searchResult); // We already scroll below, so no need for focus to scroll. The scrolling // behavior of `focus` also seems less predictable and caused an issue, see: // https://github.com/infokiller/web-search-navigator/issues/35 searchResult.anchor.focus({preventScroll: true}); // Ensure whole search result container is visible in the viewport, not only // the search result link. if (scroll !== FOCUS_SCROLL_OFF) { scrollToElement(this.searchEngine, searchResult.container); } this.focusedIndex = index; this.isInitialFocusSet = true; } focusNext(shouldWrap) { if (this.focusedIndex < this.searchResults.length - 1) { this.focus(this.focusedIndex + 1); } else if (shouldWrap) { this.focus(0); } } focusPrevious(shouldWrap) { if (this.focusedIndex > 0) { this.focus(this.focusedIndex - 1); } else if (shouldWrap) { this.focus(this.searchResults.length - 1); } else { window.scrollTo(window.scrollX, 0); } } focusDown(shouldWrap) { if ( this.focusedIndex + this.searchResults.itemsPerRow < this.searchResults.length ) { this.focus(this.focusedIndex + this.searchResults.itemsPerRow); } else if (shouldWrap) { const focusedRowIndex = this.focusedIndex % this.searchResults.itemsPerRow; this.focus(focusedRowIndex); } } focusUp(shouldWrap) { if (this.focusedIndex - this.searchResults.itemsPerRow >= 0) { this.focus(this.focusedIndex - this.searchResults.itemsPerRow); } else if (shouldWrap) { const focusedRowIndex = this.focusedIndex % this.searchResults.itemsPerRow; this.focus( this.searchResults - 1 - this.searchResults.itemsPerRow + focusedRowIndex, ); } else { window.scrollTo(window.scrollY, 0); } } } class WebSearchNavigator { constructor() { this.bindings = []; this.bindingsToggle = {active: true}; } async init() { this.options = new ExtensionOptions(); await this.options.load(); this.searchEngine = await getSearchEngine(this.options.sync.getAll()); if (this.searchEngine == null) { return; } const sleep = (milliseconds) => { return new Promise((resolve) => setTimeout(resolve, milliseconds)); }; await sleep(this.options.sync.get('delay')); this.injectCSS(); this.initKeybindings(); } injectCSS() { const style = document.createElement('style'); style.textContent = this.options.sync.get('customCSS'); document.head.append(style); } initKeybindings() { this.bindingsToggle['active'] = false; for (const [shortcut, element, ,] of this.bindings) { /* eslint-disable-next-line new-cap */ const ms = Mousetrap(element); ms.unbind(shortcut); ms.reset(); } const isFirstCall = this.bindings.length === 0; this.bindings = []; // UGLY WORKAROUND: Results navigation breaks YouTube space keybinding for // pausing/resuming a video. A workaround is to click on an element on the // page (except the video), but for now I'm disabling results navigation // when watching a video. // TODO: Find a proper fix. if (!window.location.href.match(/^https:\/\/(www)\.youtube\.com\/watch/)) { this.initResultsNavigation(isFirstCall); } this.initTabsNavigation(); this.initChangeToolsNavigation(); this.initSearchInputNavigation(); this.bindingsToggle = {active: true}; bindKeys(this.bindings, this.bindingsToggle); } initSearchInputNavigation() { let searchInput = document.querySelector( this.searchEngine.searchBoxSelector, ); if (searchInput == null) { return; } // Only apply the extension logic if the key is not something the user may // have wanted to type into the searchbox, so that we don't interfere with // regular typing. const shouldHandleSearchInputKey = (event) => { return event.ctrlKey || event.metaKey || event.key === 'Escape'; }; // In Github, the search input element changes while in the page, so we // redetect it if it's not visible. const detectSearchInput = () => { if (searchInput != null && searchInput.offsetParent != null) { return true; } searchInput = document.querySelector(this.searchEngine.searchBoxSelector); return searchInput != null && searchInput.offsetParent != null; }; // If insideSearchboxHandler returns true, outsideSearchboxHandler will also // be called (because it's defined on document, hence has lower priority), // in which case we don't want to handle the event. Therefore, we store the // last event handled in insideSearchboxHandler, and only handle the event // in outsideSearchboxHandler if it's not the same one. let lastEvent; const outsideSearchboxHandler = (event) => { if (!detectSearchInput()) { return; } if (event === lastEvent) { return !shouldHandleSearchInputKey(event); } const element = document.activeElement; if ( element.isContentEditable || ['textarea', 'input'].includes(element.tagName.toLowerCase()) ) { return true; } // Scroll to the search box in case it's outside the viewport so that it's // clear to the user that it has focus. scrollToElement(this.searchEngine, searchInput); searchInput.select(); // searchInput.click(); return false; }; const insideSearchboxHandler = (event) => { if (!detectSearchInput()) { return; } lastEvent = event; if (!shouldHandleSearchInputKey(event)) { return true; } // Everything is selected; deselect all. if ( searchInput.selectionStart === 0 && searchInput.selectionEnd === searchInput.value.length ) { // Scroll to the search box in case it's outside the viewport so that // it's clear to the user that it has focus. scrollToElement(this.searchEngine, searchInput); searchInput.setSelectionRange( searchInput.value.length, searchInput.value.length, ); return false; } // Closing search suggestions via document.body.click() or // searchInput.blur() breaks the state of google's controller. // The suggestion box is closed, yet it won't re-appear on the next // search box focus event. // Input can be blurred only when the suggestion box is already // closed, hence the blur event is queued. window.setTimeout(() => searchInput.blur()); // Invoke the default handler which will close-up search suggestions // properly (google's controller won't break), but it won't remove the // focus. return true; }; this.register( this.options.sync.get('focusSearchInput'), outsideSearchboxHandler, ); // Bind globally, otherwise Mousetrap ignores keypresses inside inputs. // We must bind it separately to the search box element, or otherwise the // key event won't always be captured (for example this is the case on // Google Search as of 2020-06-22), presumably because the javascript in the // page will disable further processing. this.register( this.options.sync.get('focusSearchInput'), insideSearchboxHandler, searchInput, true, ); } registerObject(obj) { for (const [optionName, elementOrGetter] of Object.entries(obj)) { this.register(this.options.sync.get(optionName), () => { if (elementOrGetter == null) { return true; } let element; if (elementOrGetter instanceof HTMLElement) { element = elementOrGetter; } else { element = elementOrGetter(); } if (element == null) { return true; } // Some search engines use forms instead of links for navigation if (element.tagName == 'FORM') { element.submit(); } else { element.click(); } return false; }); } } initTabsNavigation() { const tabs = this.searchEngine.tabs || {}; this.registerObject(tabs); } initResultsNavigation(isFirstCall) { this.registerObject({ navigatePreviousResultPage: this.searchEngine.previousPageButton, navigateNextResultPage: this.searchEngine.nextPageButton, }); this.resetResultsManager(); let gridNavigation = this.resultsManager.searchResults.gridNavigation; this.registerResultsNavigationKeybindings(gridNavigation); // NOTE: we must not call onChangedResults multiple times, otherwise the // URL change detection logic (which exists in YouTube) will break. if (!isFirstCall || !this.searchEngine.onChangedResults) { return; } this.searchEngine.onChangedResults((appendedOnly) => { if (appendedOnly) { this.resultsManager.reloadSearchResults(); } else { this.resetResultsManager(); } // In YouTube, the initial load does not always detect the grid navigation // (because it can happen before results are actually loaded to the page). // In this case, we must rebind the navigation keys after the results are // loaded. if (gridNavigation != this.resultsManager.searchResults.gridNavigation) { gridNavigation = this.resultsManager.searchResults.gridNavigation; this.initKeybindings(); } }); } resetResultsManager() { if (this.resultsManager != null && this.resultsManager.focusedIndex >= 0) { const searchResult = this.resultsManager.searchResults[this.resultsManager.focusedIndex]; // NOTE: it seems that search results can become undefined when the DOM // elements are removed (for example when the results change). if (searchResult != null) { this.resultsManager.unhighlight(searchResult); } } this.resultsManager = new SearchResultsManager( this.searchEngine, this.options, ); this.resultsManager.reloadSearchResults(); } registerResultsNavigationKeybindings(gridNavigation) { const getOpt = (key) => { return this.options.sync.get(key); }; const onFocusChange = (callback) => { return () => { if (!this.resultsManager.isInitialFocusSet) { this.resultsManager.focus(0); } else { const _callback = callback.bind(this.resultsManager); _callback(getOpt('wrapNavigation')); } return false; }; }; if (!gridNavigation) { this.register( getOpt('nextKey'), onFocusChange(this.resultsManager.focusNext), ); this.register( getOpt('previousKey'), onFocusChange(this.resultsManager.focusPrevious), ); } else { this.register( getOpt('nextKey'), onFocusChange(this.resultsManager.focusDown), ); this.register( getOpt('previousKey'), onFocusChange(this.resultsManager.focusUp), ); // Left this.register( getOpt('navigatePreviousResultPage'), onFocusChange(this.resultsManager.focusPrevious), ); // Right this.register( getOpt('navigateNextResultPage'), onFocusChange(this.resultsManager.focusNext), ); } this.register(getOpt('navigateKey'), () => { const link = this.resultsManager.getElementToNavigate(); if (link == null) { return true; } const lastNavigation = this.options.local.values; lastNavigation.lastQueryUrl = location.href; lastNavigation.lastFocusedIndex = this.resultsManager.focusedIndex; this.options.local.save(); // If the element is a link, use the href to directly navigate, since some // websites will open it in a new tab. if (link.localName === 'a' && link.href) { window.location.href = link.href; } else { link.click(); } return false; }); this.register(getOpt('navigateNewTabKey'), () => { const link = this.resultsManager.getElementToNavigate(true); if (link == null) { return true; } browser.runtime.sendMessage({ type: 'tabsCreate', options: { url: link.href, active: true, }, }); return false; }); this.register(getOpt('navigateNewTabBackgroundKey'), () => { const link = this.resultsManager.getElementToNavigate(true); if (link == null) { return true; } if (getOpt('simulateMiddleClick')) { const mouseEventParams = { bubbles: true, cancelable: false, view: window, button: 1, which: 2, buttons: 0, clientX: link.getBoundingClientRect().x, clientY: link.getBoundingClientRect().y, }; const middleClickMousedown = new MouseEvent( 'mousedown', mouseEventParams, ); link.dispatchEvent(middleClickMousedown); const middleClickMouseup = new MouseEvent('mouseup', mouseEventParams); link.dispatchEvent(middleClickMouseup); } browser.runtime.sendMessage({ type: 'tabsCreate', options: { url: link.href, active: false, }, }); return false; }); } initChangeToolsNavigation() { if (this.searchEngine.changeTools == null) { return; } const getOpt = (key) => { return this.options.sync.get(key); }; this.register(getOpt('navigateShowAll'), () => this.searchEngine.changeTools('a'), ); this.register(getOpt('navigateShowHour'), () => this.searchEngine.changeTools('h'), ); this.register(getOpt('navigateShowDay'), () => this.searchEngine.changeTools('d'), ); this.register(getOpt('navigateShowWeek'), () => this.searchEngine.changeTools('w'), ); this.register(getOpt('navigateShowMonth'), () => this.searchEngine.changeTools('m'), ); this.register(getOpt('navigateShowYear'), () => this.searchEngine.changeTools('y'), ); this.register(getOpt('toggleVerbatimSearch'), () => this.searchEngine.changeTools('v'), ); this.register(getOpt('toggleSort'), () => this.searchEngine.changeTools(null), ); this.register(getOpt('showImagesLarge'), () => this.searchEngine.changeImageSize('l'), ); this.register(getOpt('showImagesMedium'), () => this.searchEngine.changeImageSize('e'), ); this.register(getOpt('showImagesIcon'), () => this.searchEngine.changeImageSize('i'), ); } register(shortcuts, callback, element = document, global = false) { for (const shortcut of shortcuts) { this.bindings.push([shortcut, element, global, callback]); } } } const extension = new WebSearchNavigator(); extension.init(); // END: main.js ;} } catch(e) { _error(` Error executing scripts ${scriptPaths}`, e); } } else { _log(`Skipping document-end phase (no document).`); } // #region Wait for Document Idle _log(`Waiting for document idle state...`); if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') { await new Promise(resolve => window.requestIdleCallback(resolve, { timeout: 2000 })); // 2-second timeout fallback _log(`requestIdleCallback fired or timed out.`); } else { // Fallback: wait a short period after DOMContentLoaded/current execution if requestIdleCallback is unavailable await new Promise(resolve => setTimeout(resolve, 50)); _log(`Idle fallback timer completed.`); } // #endregion // #region Document Idle if (typeof document !== 'undefined') { _log(`Executing document-idle phase...`); const scriptPaths = []; _log(` Executing JS (idle): ${scriptPaths}`); try { // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically) with (globalThis){; ;} } catch(e) { _error(` Error executing scripts ${scriptPaths}`, e); } } else { _log(`Skipping document-idle phase (no document).`); } _log(`All execution phases complete, re-firing load events.`); document.dispatchEvent(new Event("DOMContentLoaded", { bubbles: true, cancelable: true })); } // #endregion // #region Event Listener No changes needed here --- window.addEventListener("message", (event) => { if (event.data.type === "openOptionsPage") { openOptionsPage(); } if (event.data.type === "openPopupPage") { openPopupPage(); } if (event.data.type === "closeOptionsPage") { closeOptionsModal(); } if (event.data.type === "closePopupPage") { closePopupModal(); } }); // #endregion // #region Refactored Modal Closing Functions Promise-based --- function closeOptionsModal() { return new Promise((resolve) => { const DURATION = 100; const backdrop = document.getElementById("extension-options-backdrop"); const modal = document.getElementById("extension-options-modal"); if (!backdrop || !modal) { return resolve(); } modal.style.animation = `modalCloseAnimation ${DURATION / 1000}s ease-out forwards`; backdrop.style.animation = `backdropFadeOut ${DURATION / 1000}s ease-out forwards`; setTimeout(() => { if (confirm("Close options and reload the page?")) { window.location.reload(); // Note: This will stop further execution } else { backdrop.remove(); } resolve(); }, DURATION); }); } function closePopupModal() { return new Promise((resolve) => { const DURATION = 100; const backdrop = document.getElementById("extension-popup-backdrop"); const modal = document.getElementById("extension-popup-modal"); if (!backdrop || !modal) { return resolve(); } modal.style.animation = `modalCloseAnimation ${DURATION / 1000}s ease-out forwards`; backdrop.style.animation = `backdropFadeOut ${DURATION / 1000}s ease-out forwards`; setTimeout(() => { backdrop.remove(); resolve(); }, DURATION); }); } // #endregion // #region Simplified Public API Functions --- async function openPopupPage() { if (!POPUP_PAGE_PATH || typeof EXTENSION_ASSETS_MAP === "undefined") { _warn("No popup page available."); return; } await openModal({ type: "popup", pagePath: POPUP_PAGE_PATH, defaultTitle: "Extension Popup", closeFn: closePopupModal, }); } async function openOptionsPage() { if (!OPTIONS_PAGE_PATH || typeof EXTENSION_ASSETS_MAP === "undefined") { _warn("No options page available."); return; } await openModal({ type: "options", pagePath: OPTIONS_PAGE_PATH, defaultTitle: "Extension Options", closeFn: closeOptionsModal, }); } // #endregion // #region Generic Modal Logic Style Injection --- let stylesInjected = false; function injectGlobalStyles() { if (stylesInjected) return; stylesInjected = true; const styles = ` .extension-backdrop { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.13); backdrop-filter: blur(3px); z-index: 2147483646; display: flex; align-items: center; justify-content: center; animation: backdropFadeIn 0.3s ease-out forwards; } .extension-modal { z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --background: #ffffff; --rad: 10px; --border: #666; --border-thickness: 2px; display: flex; flex-direction: column; overflow: hidden; animation: modalOpenAnimation 0.3s ease-out forwards; } /* Size specific styles */ .extension-modal.popup-size { width: 400px; height: 600px; max-width: calc(100vw - 40px); max-height: calc(100vh - 40px); } .extension-modal.options-size { width: calc(100vw - 80px); height: calc(100vh - 80px); max-width: 1200px; max-height: 800px; } /* Common modal components */ .extension-modal .modal-header { display: flex; justify-content: space-between; align-items: flex-end; padding: 0 16px; position: relative; flex-shrink: 0; } .extension-modal .tab { padding: 12px 16px; color: #606266; display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; border-radius: var(--rad) var(--rad) 0 0; transition: background-color 0.2s ease; user-select: none; } .extension-modal .tab.active, .extension-modal .tab.close-button { background-color: var(--background); border: var(--border-thickness) solid var(--border); border-bottom-color: var(--background); margin-bottom: -1px; z-index: 1; color: #303133; font-weight: 500; } .extension-modal .tab.close-button { padding: 8px; } .extension-modal .tab.close-button:hover { background-color: #f5f7fa; } .extension-modal .tab svg { stroke: currentColor; } .extension-modal .tab.active img { width: 16px; height: 16px; } .extension-modal .tab.close-button svg { width: 20px; height: 20px; } .extension-modal .modal-content { flex-grow: 1; position: relative; border-radius: var(--rad); overflow: hidden; bottom: calc(var(--border-thickness) - 1px); border: var(--border-thickness) solid var(--border); } .extension-modal .modal-content iframe { width: 100%; height: 100%; border: 0; background: white; } /* Animations */ @keyframes backdropFadeIn { from { opacity: 0; backdrop-filter: blur(0px); } to { opacity: 1; backdrop-filter: blur(3px); } } @keyframes backdropFadeOut { from { opacity: 1; backdrop-filter: blur(3px); } to { opacity: 0; backdrop-filter: blur(0px); } } @keyframes modalOpenAnimation { from { transform: scaleY(0.8); opacity: 0; } to { transform: scaleY(1); opacity: 1; } } @keyframes modalCloseAnimation { from { transform: scaleY(1); opacity: 1; } to { transform: scaleY(0.8); opacity: 0; } } `; const styleSheet = document.createElement("style"); styleSheet.id = "extension-global-styles"; styleSheet.innerText = styles; document.head.appendChild(styleSheet); } async function openModal(config) { injectGlobalStyles(); const { type, pagePath, defaultTitle, closeFn } = config; const html = EXTENSION_ASSETS_MAP[pagePath]; if (!html) { _warn(`${defaultTitle} HTML not found in asset map`); return; } const backdropId = `extension-${type}-backdrop`; const modalId = `extension-${type}-modal`; const sizeClass = `${type}-size`; // #endregion // #region Smoothly close the other modal if it s open --- const otherType = type === "popup" ? "options" : "popup"; const otherBackdrop = document.getElementById( `extension-${otherType}-backdrop` ); if (otherBackdrop) { // Await the correct close function await (otherType === "popup" ? closePopupModal() : closeOptionsModal()); } let backdrop = document.getElementById(backdropId); let modal, iframe; if (!backdrop) { backdrop = document.createElement("div"); backdrop.id = backdropId; backdrop.className = "extension-backdrop"; modal = document.createElement("div"); modal.id = modalId; modal.className = `extension-modal ${sizeClass}`; const extensionName = INJECTED_MANIFEST.name || defaultTitle; const iconSrc = EXTENSION_ICON || "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIHN0cm9rZT0ibm9uZSIgZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik00IDdoM2ExIDEgMCAwIDAgMSAtMXYtMWEyIDIgMCAwIDEgNCAwdjFhMSAxIDAgMCAwIDEgMWgzYTEgMSAwIDAgMSAxIDF2M2ExIDEgMCAwIDAgMSAxaDFhMiAyIDAgMCAxIDAgNGgtMWExIDEgMCAwIDAgLTEgMXYzYTEgMSAwIDAgMSAtMSAxaC0zYTEgMSAwIDAgMSAtMSAtMXYtMWEyIDIgMCAwIDAgLTQgMHYxYTEgMSAwIDAgMSAtMSAxaC0zYTEgMSAwIDAgMSAtMSAtMXYtM2ExIDEgMCAwIDEgMSAtMWgxYTIgMiAwIDAgMCAwIC00aC0xYTEgMSAwIDAgMSAtMSAtMXYtM2ExIDEgMCAwIDEgMSAtMSIgLz48L3N2Zz4="; modal.innerHTML = ` `; backdrop.appendChild(modal); backdrop.addEventListener("click", (e) => { if (e.target === backdrop) closeFn(); }); modal.querySelector(".close-button").addEventListener("click", closeFn); document.body.appendChild(backdrop); iframe = modal.querySelector("iframe"); } else { // If it already exists, just make sure it's visible backdrop.style.display = "flex"; modal = backdrop.querySelector(".extension-modal"); iframe = modal.querySelector("iframe"); } // Load content into iframe try { const polyfillString = generateCompletePolyfillForIframe(); const doc = new DOMParser().parseFromString(html, "text/html"); const script = doc.createElement("script"); script.textContent = polyfillString; doc.head.insertAdjacentElement("afterbegin", script); iframe.srcdoc = doc.documentElement.outerHTML; } catch (e) { _error("Error generating complete polyfill for iframe", e); iframe.srcdoc = html; } } function generateCompletePolyfillForIframe() { const polyfillString = "\n// -- Messaging implementation\n\nfunction createEventBus(\n scopeId,\n type = \"page\", // \"page\" or \"iframe\"\n { allowedOrigin = \"*\", children = [], parentWindow = null } = {}\n) {\n if (!scopeId) throw new Error(\"createEventBus requires a scopeId\");\n\n const handlers = {};\n\n function handleIncoming(ev) {\n if (allowedOrigin !== \"*\" && ev.origin !== allowedOrigin) return;\n\n const msg = ev.data;\n if (!msg || msg.__eventBus !== true || msg.scopeId !== scopeId) return;\n\n const { event, payload } = msg;\n\n // PAGE: if it's an INIT from an iframe, adopt it\n if (type === \"page\" && event === \"__INIT__\") {\n const win = ev.source;\n if (win && !children.includes(win)) {\n children.push(win);\n }\n return;\n }\n\n (handlers[event] || []).forEach((fn) =>\n fn(payload, { origin: ev.origin, source: ev.source })\n );\n }\n\n window.addEventListener(\"message\", handleIncoming);\n\n function emitTo(win, event, payload) {\n const envelope = {\n __eventBus: true,\n scopeId,\n event,\n payload,\n };\n win.postMessage(envelope, allowedOrigin);\n }\n\n // IFRAME: announce to page on startup\n if (type === \"iframe\") {\n setTimeout(() => {\n const pw = parentWindow || window.parent;\n if (pw && pw.postMessage) {\n emitTo(pw, \"__INIT__\", null);\n }\n }, 0);\n }\n\n return {\n on(event, fn) {\n handlers[event] = handlers[event] || [];\n handlers[event].push(fn);\n },\n off(event, fn) {\n if (!handlers[event]) return;\n handlers[event] = handlers[event].filter((h) => h !== fn);\n },\n /**\n * Emits an event.\n * @param {string} event - The event name.\n * @param {any} payload - The event payload.\n * @param {object} [options] - Emission options.\n * @param {Window} [options.to] - A specific window to target. If provided, message is ONLY sent to the target.\n */\n emit(event, payload, { to } = {}) {\n // If a specific target window is provided, send only to it and DO NOT dispatch locally.\n // This prevents a port from receiving its own messages.\n if (to) {\n if (to && typeof to.postMessage === \"function\") {\n emitTo(to, event, payload);\n }\n return; // Exit after targeted send.\n }\n\n // For broadcast messages (no 'to' target), dispatch locally first.\n (handlers[event] || []).forEach((fn) =>\n fn(payload, { origin: location.origin, source: window })\n );\n\n // Then propagate the broadcast to other windows.\n if (type === \"page\") {\n children.forEach((win) => emitTo(win, event, payload));\n } else {\n const pw = parentWindow || window.parent;\n if (pw && pw.postMessage) {\n emitTo(pw, event, payload);\n }\n }\n },\n };\n}\n\nfunction createRuntime(type = \"background\", bus) {\n let nextId = 1;\n const pending = {};\n const msgListeners = [];\n\n let nextPortId = 1;\n const ports = {};\n const onConnectListeners = [];\n\n function parseArgs(args) {\n let target, message, options, callback;\n const arr = [...args];\n if (arr.length === 0) {\n throw new Error(\"sendMessage requires at least one argument\");\n }\n if (arr.length === 1) {\n return { message: arr[0] };\n }\n // last object could be options\n if (\n arr.length &&\n typeof arr[arr.length - 1] === \"object\" &&\n !Array.isArray(arr[arr.length - 1])\n ) {\n options = arr.pop();\n }\n // last function is callback\n if (arr.length && typeof arr[arr.length - 1] === \"function\") {\n callback = arr.pop();\n }\n if (\n arr.length === 2 &&\n (typeof arr[0] === \"string\" || typeof arr[0] === \"number\")\n ) {\n [target, message] = arr;\n } else {\n [message] = arr;\n }\n return { target, message, options, callback };\n }\n\n if (type === \"background\") {\n bus.on(\"__REQUEST__\", ({ id, message }, { source }) => {\n let responded = false,\n isAsync = false;\n function sendResponse(resp) {\n if (responded) return;\n responded = true;\n // Target the response directly back to the window that sent the request.\n bus.emit(\"__RESPONSE__\", { id, response: resp }, { to: source });\n }\n const results = msgListeners\n .map((fn) => {\n try {\n // msg, sender, sendResponse\n const ret = fn(message, { id, tab: { id: source } }, sendResponse);\n if (ret === true || (ret && typeof ret.then === \"function\")) {\n isAsync = true;\n return ret;\n }\n return ret;\n } catch (e) {\n _error(e);\n }\n })\n .filter((r) => r !== undefined);\n\n const promises = results.filter((r) => r && typeof r.then === \"function\");\n if (!isAsync && promises.length === 0) {\n const out = results.length === 1 ? results[0] : results;\n sendResponse(out);\n } else if (promises.length) {\n Promise.all(promises).then((vals) => {\n if (!responded) {\n const out = vals.length === 1 ? vals[0] : vals;\n sendResponse(out);\n }\n });\n }\n });\n }\n\n if (type !== \"background\") {\n bus.on(\"__RESPONSE__\", ({ id, response }) => {\n const entry = pending[id];\n if (!entry) return;\n entry.resolve(response);\n if (entry.callback) entry.callback(response);\n delete pending[id];\n });\n }\n\n function sendMessage(...args) {\n // Background should be able to send message to itself\n // if (type === \"background\") {\n // throw new Error(\"Background cannot sendMessage to itself\");\n // }\n const { target, message, callback } = parseArgs(args);\n const id = nextId++;\n const promise = new Promise((resolve) => {\n pending[id] = { resolve, callback };\n bus.emit(\"__REQUEST__\", { id, message });\n });\n return promise;\n }\n\n bus.on(\"__PORT_CONNECT__\", ({ portId, name }, { source }) => {\n if (type !== \"background\") return;\n const backgroundPort = makePort(\"background\", portId, name, source);\n ports[portId] = backgroundPort;\n\n onConnectListeners.forEach((fn) => fn(backgroundPort));\n\n // send back a CONNECT_ACK so the client can\n // start listening on its end:\n bus.emit(\"__PORT_CONNECT_ACK__\", { portId, name }, { to: source });\n });\n\n // Clients handle the ACK and finalize their Port object by learning the remote window.\n bus.on(\"__PORT_CONNECT_ACK__\", ({ portId, name }, { source }) => {\n if (type === \"background\") return; // ignore\n const p = ports[portId];\n if (!p) return;\n // Call the port's internal finalize method to complete the handshake\n if (p._finalize) {\n p._finalize(source);\n }\n });\n\n // Any port message travels via \"__PORT_MESSAGE__\"\n bus.on(\"__PORT_MESSAGE__\", (envelope, { source }) => {\n const { portId } = envelope;\n const p = ports[portId];\n if (!p) return;\n p._receive(envelope, source);\n });\n\n // Any port disconnect:\n bus.on(\"__PORT_DISCONNECT__\", ({ portId }) => {\n const p = ports[portId];\n if (!p) return;\n p._disconnect();\n delete ports[portId];\n });\n\n // Refactored makePort to correctly manage internal state and the connection handshake.\n function makePort(side, portId, name, remoteWindow) {\n let onMessageHandlers = [];\n let onDisconnectHandlers = [];\n let buffer = [];\n // Unique instance ID for this port instance\n const instanceId = Math.random().toString(36).slice(2) + Date.now();\n // These state variables are part of the closure and are updated by _finalize\n let _ready = side === \"background\";\n\n function _drainBuffer() {\n buffer.forEach((m) => _post(m));\n buffer = [];\n }\n\n function _post(msg) {\n // Always use the 'to' parameter for port messages, making them directional.\n // Include senderInstanceId\n bus.emit(\n \"__PORT_MESSAGE__\",\n { portId, msg, senderInstanceId: instanceId },\n { to: remoteWindow }\n );\n }\n\n function postMessage(msg) {\n if (!_ready) {\n buffer.push(msg);\n } else {\n _post(msg);\n }\n }\n\n function _receive(envelope, source) {\n // envelope: { msg, senderInstanceId }\n if (envelope.senderInstanceId === instanceId) return; // Don't dispatch to self\n onMessageHandlers.forEach((fn) =>\n fn(envelope.msg, { id: portId, tab: { id: source } })\n );\n }\n\n function disconnect() {\n // Also use the 'to' parameter for disconnect messages\n bus.emit(\"__PORT_DISCONNECT__\", { portId }, { to: remoteWindow });\n _disconnect();\n delete ports[portId];\n }\n\n function _disconnect() {\n onDisconnectHandlers.forEach((fn) => fn());\n onMessageHandlers = [];\n onDisconnectHandlers = [];\n }\n\n // This function is called on the client port when the ACK is received from background.\n // It updates the port's state, completing the connection.\n function _finalize(win) {\n remoteWindow = win; // <-- This is the crucial part: learn the destination\n _ready = true;\n _drainBuffer();\n }\n\n return {\n name,\n sender: {\n id: portId,\n },\n onMessage: {\n addListener(fn) {\n onMessageHandlers.push(fn);\n },\n removeListener(fn) {\n onMessageHandlers = onMessageHandlers.filter((x) => x !== fn);\n },\n },\n onDisconnect: {\n addListener(fn) {\n onDisconnectHandlers.push(fn);\n },\n removeListener(fn) {\n onDisconnectHandlers = onDisconnectHandlers.filter((x) => x !== fn);\n },\n },\n postMessage,\n disconnect,\n // Internal methods used by the runtime\n _receive,\n _disconnect,\n _finalize, // Expose the finalizer for the ACK handler\n };\n }\n\n function connect(connectInfo = {}) {\n if (type === \"background\") {\n throw new Error(\"Background must use onConnect, not connect()\");\n }\n const name = connectInfo.name || \"\";\n const portId = nextPortId++;\n // create the client side port\n // remoteWindow is initially null; it will be set by _finalize upon ACK.\n const clientPort = makePort(\"client\", portId, name, null);\n ports[portId] = clientPort;\n\n // fire the connect event across the bus\n bus.emit(\"__PORT_CONNECT__\", { portId, name });\n return clientPort;\n }\n\n function onConnect(fn) {\n if (type !== \"background\") {\n throw new Error(\"connect event only fires in background\");\n }\n onConnectListeners.push(fn);\n }\n\n return {\n // rpc:\n sendMessage,\n onMessage: {\n addListener(fn) {\n msgListeners.push(fn);\n },\n removeListener(fn) {\n const i = msgListeners.indexOf(fn);\n if (i >= 0) msgListeners.splice(i, 1);\n },\n },\n\n // port API:\n connect,\n onConnect: {\n addListener(fn) {\n onConnect(fn);\n },\n removeListener(fn) {\n const i = onConnectListeners.indexOf(fn);\n if (i >= 0) onConnectListeners.splice(i, 1);\n },\n },\n };\n}\n\n\n// --- Abstraction Layer: PostMessage Target\n\nlet nextRequestId = 1;\nconst pendingRequests = new Map(); // requestId -> { resolve, reject, timeout }\n\nfunction sendAbstractionRequest(method, args = []) {\n return new Promise((resolve, reject) => {\n const requestId = nextRequestId++;\n\n const timeout = setTimeout(() => {\n pendingRequests.delete(requestId);\n reject(new Error(`PostMessage request timeout for method: ${method}`));\n }, 10000);\n\n pendingRequests.set(requestId, { resolve, reject, timeout });\n\n window.parent.postMessage({\n type: \"abstraction-request\",\n requestId,\n method,\n args,\n });\n });\n}\n\nwindow.addEventListener(\"message\", (event) => {\n const { type, requestId, success, result, error } = event.data;\n\n if (type === \"abstraction-response\") {\n const pending = pendingRequests.get(requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n pendingRequests.delete(requestId);\n\n if (success) {\n pending.resolve(result);\n } else {\n const err = new Error(error.message);\n err.stack = error.stack;\n pending.reject(err);\n }\n }\n }\n});\n\nasync function _storageSet(items) {\n return sendAbstractionRequest(\"_storageSet\", [items]);\n}\n\nasync function _storageGet(keys) {\n return sendAbstractionRequest(\"_storageGet\", [keys]);\n}\n\nasync function _storageRemove(keysToRemove) {\n return sendAbstractionRequest(\"_storageRemove\", [keysToRemove]);\n}\n\nasync function _storageClear() {\n return sendAbstractionRequest(\"_storageClear\");\n}\n\nasync function _cookieList(details) {\n return sendAbstractionRequest(\"_cookieList\", [details]);\n}\n\nasync function _cookieSet(details) {\n return sendAbstractionRequest(\"_cookieSet\", [details]);\n}\n\nasync function _cookieDelete(details) {\n return sendAbstractionRequest(\"_cookieDelete\", [details]);\n}\n\nasync function _fetch(url, options) {\n return sendAbstractionRequest(\"_fetch\", [url, options]);\n}\n\nfunction _registerMenuCommand(name, func) {\n _warn(\"_registerMenuCommand called from iframe context:\", name);\n return sendAbstractionRequest(\"_registerMenuCommand\", [\n name,\n func.toString(),\n ]);\n}\n\nfunction _openTab(url, active) {\n return sendAbstractionRequest(\"_openTab\", [url, active]);\n}\n\nasync function _initStorage() {\n return sendAbstractionRequest(\"_initStorage\");\n}\n\n\nconst EXTENSION_ASSETS_MAP = {{EXTENSION_ASSETS_MAP}};\n\n// -- Polyfill Implementation\nfunction buildPolyfill({ isBackground = false, isOtherPage = false } = {}) {\n // Generate a unique context ID for this polyfill instance\n const contextType = isBackground\n ? \"background\"\n : isOtherPage\n ? \"options\"\n : \"content\";\n const contextId = `${contextType}_${Math.random()\n .toString(36)\n .substring(2, 15)}`;\n\n const IS_IFRAME = \"true\" === \"true\";\n const BUS = (function () {\n if (globalThis.__BUS) {\n return globalThis.__BUS;\n }\n globalThis.__BUS = createEventBus(\n \"web-search-navigator\",\n IS_IFRAME ? \"iframe\" : \"page\",\n );\n return globalThis.__BUS;\n })();\n const RUNTIME = createRuntime(isBackground ? \"background\" : \"tab\", BUS);\n const createNoopListeners = () => ({\n addListener: (callback) => {\n _log(\"addListener\", callback);\n },\n removeListener: (callback) => {\n _log(\"removeListener\", callback);\n },\n });\n // TODO: Stub\n const storageChangeListeners = new Set();\n function broadcastStorageChange(changes, areaName) {\n storageChangeListeners.forEach((listener) => {\n listener(changes, areaName);\n });\n }\n\n let REQ_PERMS = [];\n\n // --- Chrome polyfill\n let chrome = {\n extension: {\n isAllowedIncognitoAccess: () => Promise.resolve(true),\n sendMessage: (...args) => _messagingHandler.sendMessage(...args),\n },\n permissions: {\n // TODO: Remove origin permission means exclude from origin in startup (when checking for content scripts)\n request: (permissions, callback) => {\n _log(\"permissions.request\", permissions, callback);\n if (Array.isArray(permissions)) {\n REQ_PERMS = [...REQ_PERMS, ...permissions];\n }\n if (typeof callback === \"function\") {\n callback(permissions);\n }\n return Promise.resolve(permissions);\n },\n contains: (permissions, callback) => {\n if (typeof callback === \"function\") {\n callback(true);\n }\n return Promise.resolve(true);\n },\n getAll: () => {\n return Promise.resolve({\n permissions: EXTENSION_PERMISSIONS,\n origins: ORIGIN_PERMISSIONS,\n });\n },\n onAdded: createNoopListeners(),\n onRemoved: createNoopListeners(),\n },\n i18n: {\n getUILanguage: () => {\n return USED_LOCALE || \"en\";\n },\n getMessage: (key, substitutions = []) => {\n if (typeof substitutions === \"string\") {\n substitutions = [substitutions];\n }\n if (typeof LOCALE_KEYS !== \"undefined\" && LOCALE_KEYS[key]) {\n return LOCALE_KEYS[key].message?.replace(\n /\\$(\\d+)/g,\n (match, p1) => substitutions[p1 - 1] || match,\n );\n }\n return key;\n },\n },\n alarms: {\n onAlarm: createNoopListeners(),\n create: () => {\n _log(\"alarms.create\", arguments);\n },\n get: () => {\n _log(\"alarms.get\", arguments);\n },\n },\n runtime: {\n ...RUNTIME,\n onInstalled: createNoopListeners(),\n onStartup: createNoopListeners(),\n // TODO: Postmessage to parent to open options page or call openOptionsPage\n openOptionsPage: () => {\n // const url = chrome.runtime.getURL(OPTIONS_PAGE_PATH);\n // console.log(\"openOptionsPage\", _openTab, url, EXTENSION_ASSETS_MAP);\n // _openTab(url);\n if (typeof openOptionsPage === \"function\") {\n openOptionsPage();\n } else if (window.parent) {\n window.parent.postMessage({ type: \"openOptionsPage\" }, \"*\");\n } else {\n _warn(\"openOptionsPage not available.\");\n }\n },\n getManifest: () => {\n // The manifest object will be injected into the scope where buildPolyfill is called\n if (typeof INJECTED_MANIFEST !== \"undefined\") {\n return JSON.parse(JSON.stringify(INJECTED_MANIFEST)); // Return deep copy\n }\n _warn(\"INJECTED_MANIFEST not found for chrome.runtime.getManifest\");\n return { name: \"Unknown\", version: \"0.0\", manifest_version: 2 };\n },\n getURL: (path) => {\n if (!path) return \"\";\n if (path.startsWith(\"/\")) {\n path = path.substring(1);\n }\n\n if (typeof _createAssetUrl === \"function\") {\n return _createAssetUrl(path);\n }\n\n _warn(\n `chrome.runtime.getURL fallback for '${path}'. Assets may not be available.`,\n );\n // Attempt a relative path resolution (highly context-dependent and likely wrong)\n try {\n if (window.location.protocol.startsWith(\"http\")) {\n return new URL(path, window.location.href).toString();\n }\n } catch (e) {\n /* ignore error, fallback */\n }\n return path;\n },\n id: \"polyfilled-extension-\" + Math.random().toString(36).substring(2, 15),\n lastError: null,\n setUninstallURL: () => {},\n setUpdateURL: () => {},\n getPlatformInfo: async () => {\n const platform = {\n os: \"unknown\",\n arch: \"unknown\",\n nacl_arch: \"unknown\",\n };\n\n if (typeof navigator !== \"undefined\") {\n const userAgent = navigator.userAgent.toLowerCase();\n if (userAgent.includes(\"mac\")) platform.os = \"mac\";\n else if (userAgent.includes(\"win\")) platform.os = \"win\";\n else if (userAgent.includes(\"linux\")) platform.os = \"linux\";\n else if (userAgent.includes(\"android\")) platform.os = \"android\";\n else if (userAgent.includes(\"ios\")) platform.os = \"ios\";\n\n if (userAgent.includes(\"x86_64\") || userAgent.includes(\"amd64\")) {\n platform.arch = \"x86-64\";\n } else if (userAgent.includes(\"i386\") || userAgent.includes(\"i686\")) {\n platform.arch = \"x86-32\";\n } else if (userAgent.includes(\"arm\")) {\n platform.arch = \"arm\";\n }\n }\n\n return platform;\n },\n getBrowserInfo: async () => {\n const info = {\n name: \"unknown\",\n version: \"unknown\",\n buildID: \"unknown\",\n };\n\n if (typeof navigator !== \"undefined\") {\n const userAgent = navigator.userAgent;\n if (userAgent.includes(\"Chrome\")) {\n info.name = \"Chrome\";\n const match = userAgent.match(/Chrome\\/([0-9.]+)/);\n if (match) info.version = match[1];\n } else if (userAgent.includes(\"Firefox\")) {\n info.name = \"Firefox\";\n const match = userAgent.match(/Firefox\\/([0-9.]+)/);\n if (match) info.version = match[1];\n } else if (userAgent.includes(\"Safari\")) {\n info.name = \"Safari\";\n const match = userAgent.match(/Version\\/([0-9.]+)/);\n if (match) info.version = match[1];\n }\n }\n\n return info;\n },\n },\n storage: {\n local: {\n get: function (keys, callback) {\n if (typeof _storageGet !== \"function\")\n throw new Error(\"_storageGet not defined\");\n\n const promise = _storageGet(keys);\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.get callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.get error:\", error);\n callback({});\n });\n return;\n }\n\n return promise;\n },\n set: function (items, callback) {\n if (typeof _storageSet !== \"function\")\n throw new Error(\"_storageSet not defined\");\n\n const promise = _storageSet(items).then((result) => {\n broadcastStorageChange(items, \"local\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.set callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.set error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n remove: function (keys, callback) {\n if (typeof _storageRemove !== \"function\")\n throw new Error(\"_storageRemove not defined\");\n\n const promise = _storageRemove(keys).then((result) => {\n const changes = {};\n const keyList = Array.isArray(keys) ? keys : [keys];\n keyList.forEach((key) => {\n changes[key] = { oldValue: undefined, newValue: undefined };\n });\n broadcastStorageChange(changes, \"local\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.remove callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.remove error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n clear: function (callback) {\n if (typeof _storageClear !== \"function\")\n throw new Error(\"_storageClear not defined\");\n\n const promise = _storageClear().then((result) => {\n broadcastStorageChange({}, \"local\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.clear callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.clear error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n onChanged: {\n addListener: (callback) => {\n storageChangeListeners.add(callback);\n },\n removeListener: (callback) => {\n storageChangeListeners.delete(callback);\n },\n },\n },\n sync: {\n get: function (keys, callback) {\n _warn(\"chrome.storage.sync polyfill maps to local\");\n return chrome.storage.local.get(keys, callback);\n },\n set: function (items, callback) {\n _warn(\"chrome.storage.sync polyfill maps to local\");\n\n const promise = chrome.storage.local.set(items).then((result) => {\n broadcastStorageChange(items, \"sync\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.sync.set callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.sync.set error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n remove: function (keys, callback) {\n _warn(\"chrome.storage.sync polyfill maps to local\");\n\n const promise = chrome.storage.local.remove(keys).then((result) => {\n const changes = {};\n const keyList = Array.isArray(keys) ? keys : [keys];\n keyList.forEach((key) => {\n changes[key] = { oldValue: undefined, newValue: undefined };\n });\n broadcastStorageChange(changes, \"sync\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.sync.remove callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.sync.remove error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n clear: function (callback) {\n _warn(\"chrome.storage.sync polyfill maps to local\");\n\n const promise = chrome.storage.local.clear().then((result) => {\n broadcastStorageChange({}, \"sync\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.sync.clear callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.sync.clear error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n onChanged: {\n addListener: (callback) => {\n storageChangeListeners.add(callback);\n },\n removeListener: (callback) => {\n storageChangeListeners.delete(callback);\n },\n },\n },\n onChanged: {\n addListener: (callback) => {\n storageChangeListeners.add(callback);\n },\n removeListener: (callback) => {\n storageChangeListeners.delete(callback);\n },\n },\n managed: {\n get: function (keys, callback) {\n _warn(\"chrome.storage.managed polyfill is read-only empty.\");\n\n const promise = Promise.resolve({});\n\n if (typeof callback === \"function\") {\n promise.then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.managed.get callback:\", e);\n }\n });\n return;\n }\n\n return promise;\n },\n },\n },\n cookies: (function () {\n const cookieChangeListeners = new Set();\n function broadcastCookieChange(changeInfo) {\n cookieChangeListeners.forEach((listener) => {\n try {\n listener(changeInfo);\n } catch (e) {\n _error(\"Error in cookies.onChanged listener:\", e);\n }\n });\n }\n\n function handlePromiseCallback(promise, callback) {\n if (typeof callback === \"function\") {\n promise\n .then((result) => callback(result))\n .catch((error) => {\n // chrome.runtime.lastError = { message: error.message }; // TODO: Implement lastError\n _error(error);\n callback(); // Call with undefined on error\n });\n return;\n }\n return promise;\n }\n\n return {\n get: function (details, callback) {\n if (typeof _cookieList !== \"function\") {\n return handlePromiseCallback(\n Promise.reject(new Error(\"_cookieList not defined\")),\n callback,\n );\n }\n const promise = _cookieList({\n url: details.url,\n name: details.name,\n storeId: details.storeId,\n partitionKey: details.partitionKey,\n }).then((cookies) => {\n if (!cookies || cookies.length === 0) {\n return null;\n }\n // Sort by path length (longest first), then creation time (earliest first, if available)\n cookies.sort((a, b) => {\n const pathLenDiff = (b.path || \"\").length - (a.path || \"\").length;\n if (pathLenDiff !== 0) return pathLenDiff;\n return (a.creationTime || 0) - (b.creationTime || 0);\n });\n return cookies[0];\n });\n return handlePromiseCallback(promise, callback);\n },\n\n getAll: function (details, callback) {\n if (typeof _cookieList !== \"function\") {\n return handlePromiseCallback(\n Promise.reject(new Error(\"_cookieList not defined\")),\n callback,\n );\n }\n if (details.partitionKey) {\n _warn(\n \"cookies.getAll: partitionKey is not fully supported in this environment.\",\n );\n }\n const promise = _cookieList(details);\n return handlePromiseCallback(promise, callback);\n },\n\n set: function (details, callback) {\n const promise = (async () => {\n if (\n typeof _cookieSet !== \"function\" ||\n typeof _cookieList !== \"function\"\n ) {\n throw new Error(\"_cookieSet or _cookieList not defined\");\n }\n if (details.partitionKey) {\n _warn(\n \"cookies.set: partitionKey is not fully supported in this environment.\",\n );\n }\n\n const getDetails = {\n url: details.url,\n name: details.name,\n storeId: details.storeId,\n };\n const oldCookies = await _cookieList(getDetails);\n const oldCookie = oldCookies && oldCookies[0];\n\n if (oldCookie) {\n broadcastCookieChange({\n cause: \"overwrite\",\n cookie: oldCookie,\n removed: true,\n });\n }\n\n await _cookieSet(details);\n const newCookies = await _cookieList(getDetails);\n const newCookie = newCookies && newCookies[0];\n\n if (newCookie) {\n broadcastCookieChange({\n cause: \"explicit\",\n cookie: newCookie,\n removed: false,\n });\n }\n return newCookie || null;\n })();\n return handlePromiseCallback(promise, callback);\n },\n\n remove: function (details, callback) {\n const promise = (async () => {\n if (\n typeof _cookieDelete !== \"function\" ||\n typeof _cookieList !== \"function\"\n ) {\n throw new Error(\"_cookieDelete or _cookieList not defined\");\n }\n const oldCookies = await _cookieList(details);\n const oldCookie = oldCookies && oldCookies[0];\n\n if (!oldCookie) return null; // Nothing to remove\n\n await _cookieDelete(details);\n\n broadcastCookieChange({\n cause: \"explicit\",\n cookie: oldCookie,\n removed: true,\n });\n\n return {\n url: details.url,\n name: details.name,\n storeId: details.storeId || \"0\",\n partitionKey: details.partitionKey,\n };\n })();\n return handlePromiseCallback(promise, callback);\n },\n\n getAllCookieStores: function (callback) {\n const promise = Promise.resolve([\n { id: \"0\", tabIds: [1] }, // Mock store for the current context\n ]);\n return handlePromiseCallback(promise, callback);\n },\n\n getPartitionKey: function (details, callback) {\n _warn(\n \"chrome.cookies.getPartitionKey is not supported in this environment.\",\n );\n const promise = Promise.resolve({ partitionKey: {} }); // Return empty partition key\n return handlePromiseCallback(promise, callback);\n },\n\n onChanged: {\n addListener: (callback) => {\n if (typeof callback === \"function\") {\n cookieChangeListeners.add(callback);\n }\n },\n removeListener: (callback) => {\n cookieChangeListeners.delete(callback);\n },\n },\n };\n })(),\n tabs: {\n query: async (queryInfo) => {\n _warn(\"chrome.tabs.query polyfill only returns current tab info.\");\n const dummyId = Math.floor(Math.random() * 1000) + 1;\n return [\n {\n id: dummyId,\n url: CURRENT_LOCATION,\n active: true,\n windowId: 1,\n status: \"complete\",\n },\n ];\n },\n create: async ({ url, active = true }) => {\n _log(`[Polyfill tabs.create] URL: ${url}`);\n if (typeof _openTab !== \"function\")\n throw new Error(\"_openTab not defined\");\n _openTab(url, active);\n const dummyId = Math.floor(Math.random() * 1000) + 1001;\n return Promise.resolve({\n id: dummyId,\n url: url,\n active,\n windowId: 1,\n });\n },\n sendMessage: async (tabId, message) => {\n _warn(\n `chrome.tabs.sendMessage polyfill (to tab ${tabId}) redirects to runtime.sendMessage (current context).`,\n );\n return chrome.runtime.sendMessage(message);\n },\n onActivated: createNoopListeners(),\n onUpdated: createNoopListeners(),\n onRemoved: createNoopListeners(),\n onReplaced: createNoopListeners(),\n onCreated: createNoopListeners(),\n onMoved: createNoopListeners(),\n onDetached: createNoopListeners(),\n onAttached: createNoopListeners(),\n },\n windows: {\n onFocusChanged: createNoopListeners(),\n onCreated: createNoopListeners(),\n onRemoved: createNoopListeners(),\n onFocused: createNoopListeners(),\n onFocus: createNoopListeners(),\n onBlur: createNoopListeners(),\n onFocused: createNoopListeners(),\n },\n notifications: {\n create: async (notificationId, options) => {\n try {\n let id = notificationId;\n let notificationOptions = options;\n\n if (typeof notificationId === \"object\" && notificationId !== null) {\n notificationOptions = notificationId;\n id = \"notification_\" + Math.random().toString(36).substring(2, 15);\n } else if (typeof notificationId === \"string\" && options) {\n id = notificationId;\n notificationOptions = options;\n } else {\n throw new Error(\"Invalid parameters for notifications.create\");\n }\n\n if (!notificationOptions || typeof notificationOptions !== \"object\") {\n throw new Error(\"Notification options must be an object\");\n }\n\n const {\n title,\n message,\n iconUrl,\n type = \"basic\",\n } = notificationOptions;\n\n if (!title || !message) {\n throw new Error(\"Notification must have title and message\");\n }\n\n if (\"Notification\" in window) {\n if (Notification.permission === \"granted\") {\n const notification = new Notification(title, {\n body: message,\n icon: iconUrl,\n tag: id,\n });\n\n _log(`[Notifications] Created notification: ${id}`);\n return id;\n } else if (Notification.permission === \"default\") {\n const permission = await Notification.requestPermission();\n if (permission === \"granted\") {\n const notification = new Notification(title, {\n body: message,\n icon: iconUrl,\n tag: id,\n });\n _log(\n `[Notifications] Created notification after permission: ${id}`,\n );\n return id;\n } else {\n _warn(\"[Notifications] Permission denied for notifications\");\n return id;\n }\n } else {\n _warn(\"[Notifications] Notifications are blocked\");\n return id;\n }\n } else {\n _warn(\n \"[Notifications] Native notifications not supported, using console fallback\",\n );\n _log(`[Notification] ${title}: ${message}`);\n return id;\n }\n } catch (error) {\n _error(\"[Notifications] Error creating notification:\", error.message);\n throw error;\n }\n },\n clear: async (notificationId) => {\n _log(`[Notifications] Clear notification: ${notificationId}`);\n // For native notifications, there's no direct way to clear by ID\n // This is a limitation of the Web Notifications API\n return true;\n },\n getAll: async () => {\n _warn(\"[Notifications] getAll not fully supported in polyfill\");\n return {};\n },\n getPermissionLevel: async () => {\n if (\"Notification\" in window) {\n const permission = Notification.permission;\n return { level: permission === \"granted\" ? \"granted\" : \"denied\" };\n }\n return { level: \"denied\" };\n },\n },\n contextMenus: {\n create: (createProperties, callback) => {\n try {\n if (!createProperties || typeof createProperties !== \"object\") {\n throw new Error(\"Context menu create properties must be an object\");\n }\n\n const { id, title, contexts = [\"page\"], onclick } = createProperties;\n const menuId =\n id || `menu_${Math.random().toString(36).substring(2, 15)}`;\n\n if (!title || typeof title !== \"string\") {\n throw new Error(\"Context menu must have a title\");\n }\n\n // Store menu items for potential use\n if (!window._polyfillContextMenus) {\n window._polyfillContextMenus = new Map();\n }\n\n window._polyfillContextMenus.set(menuId, {\n id: menuId,\n title,\n contexts,\n onclick,\n enabled: createProperties.enabled !== false,\n });\n\n _log(\n `[ContextMenus] Created context menu item: ${title} (${menuId})`,\n );\n\n // Try to register a menu command as fallback\n if (typeof _registerMenuCommand === \"function\") {\n try {\n _registerMenuCommand(\n title,\n onclick ||\n (() => {\n _log(`Context menu clicked: ${title}`);\n }),\n );\n } catch (e) {\n _warn(\n \"[ContextMenus] Failed to register as menu command:\",\n e.message,\n );\n }\n }\n\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n\n return menuId;\n } catch (error) {\n _error(\"[ContextMenus] Error creating context menu:\", error.message);\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n throw error;\n }\n },\n update: (id, updateProperties, callback) => {\n try {\n if (\n !window._polyfillContextMenus ||\n !window._polyfillContextMenus.has(id)\n ) {\n throw new Error(`Context menu item not found: ${id}`);\n }\n\n const menuItem = window._polyfillContextMenus.get(id);\n Object.assign(menuItem, updateProperties);\n\n _log(`[ContextMenus] Updated context menu item: ${id}`);\n\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n } catch (error) {\n _error(\"[ContextMenus] Error updating context menu:\", error.message);\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n }\n },\n remove: (menuItemId, callback) => {\n try {\n if (\n window._polyfillContextMenus &&\n window._polyfillContextMenus.has(menuItemId)\n ) {\n window._polyfillContextMenus.delete(menuItemId);\n _log(`[ContextMenus] Removed context menu item: ${menuItemId}`);\n } else {\n _warn(\n `[ContextMenus] Context menu item not found for removal: ${menuItemId}`,\n );\n }\n\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n } catch (error) {\n _error(\"[ContextMenus] Error removing context menu:\", error.message);\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n }\n },\n removeAll: (callback) => {\n try {\n if (window._polyfillContextMenus) {\n const count = window._polyfillContextMenus.size;\n window._polyfillContextMenus.clear();\n _log(`[ContextMenus] Removed all ${count} context menu items`);\n }\n\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n } catch (error) {\n _error(\n \"[ContextMenus] Error removing all context menus:\",\n error.message,\n );\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n }\n },\n onClicked: {\n addListener: (callback) => {\n if (!window._polyfillContextMenuListeners) {\n window._polyfillContextMenuListeners = new Set();\n }\n window._polyfillContextMenuListeners.add(callback);\n _log(\"[ContextMenus] Added click listener\");\n },\n removeListener: (callback) => {\n if (window._polyfillContextMenuListeners) {\n window._polyfillContextMenuListeners.delete(callback);\n _log(\"[ContextMenus] Removed click listener\");\n }\n },\n },\n },\n };\n\n const tc = (fn) => {\n try {\n fn();\n } catch (e) {}\n };\n const loggingProxyHandler = (_key) => ({\n get(target, key, receiver) {\n tc(() => _log(`[${contextType}] [CHROME - ${_key}] Getting ${key}`));\n return Reflect.get(target, key, receiver);\n },\n set(target, key, value, receiver) {\n tc(() =>\n _log(`[${contextType}] [CHROME - ${_key}] Setting ${key} to ${value}`),\n );\n return Reflect.set(target, key, value, receiver);\n },\n has(target, key) {\n tc(() =>\n _log(`[${contextType}] [CHROME - ${_key}] Checking if ${key} exists`),\n );\n return Reflect.has(target, key);\n },\n });\n chrome = Object.fromEntries(\n Object.entries(chrome).map(([key, value]) => [\n key,\n new Proxy(value, loggingProxyHandler(key)),\n ]),\n );\n\n // Alias browser to chrome for common Firefox pattern\n const browser = new Proxy(chrome, loggingProxyHandler);\n\n const oldGlobalThis = globalThis;\n const oldWindow = window;\n const oldSelf = self;\n const oldGlobal = globalThis;\n const __globalsStorage = {};\n\n const TO_MODIFY = [oldGlobalThis, oldWindow, oldSelf, oldGlobal];\n const set = (k, v) => {\n __globalsStorage[k] = v;\n TO_MODIFY.forEach((target) => {\n target[k] = v;\n });\n };\n const proxyHandler = {\n get(target, key, receiver) {\n const fns = [\n () => __globalsStorage[key],\n () => Reflect.get(target, key, target),\n () => target[key],\n ];\n const out = fns\n .map((f) => {\n try {\n let out = f();\n return out;\n } catch (e) {\n return undefined;\n }\n })\n .find((f) => f !== undefined);\n if (typeof out === \"function\") {\n return out.bind(target);\n }\n return out;\n },\n set(target, key, value, receiver) {\n try {\n tc(() => _log(`[${contextType}] Setting ${key} to ${value}`));\n set(key, value);\n return Reflect.set(target, key, value, receiver);\n } catch (e) {\n _error(\"Error setting\", key, value, e);\n try {\n target[key] = value;\n return true;\n } catch (e) {\n _error(\"Error setting\", key, value, e);\n }\n return false;\n }\n },\n has(target, key) {\n try {\n return key in __globalsStorage || key in target;\n } catch (e) {\n _error(\"Error has\", key, e);\n try {\n return key in __globalsStorage || key in target;\n } catch (e) {\n _error(\"Error has\", key, e);\n }\n return false;\n }\n },\n getOwnPropertyDescriptor(target, key) {\n try {\n if (key in __globalsStorage) {\n return {\n configurable: true,\n enumerable: true,\n writable: true,\n value: __globalsStorage[key],\n };\n }\n // fall back to the real globalThis\n const desc = Reflect.getOwnPropertyDescriptor(target, key);\n // ensure it's configurable so the with‑scope binding logic can override it\n if (desc && !desc.configurable) {\n desc.configurable = true;\n }\n return desc;\n } catch (e) {\n _error(\"Error getOwnPropertyDescriptor\", key, e);\n return {\n configurable: true,\n enumerable: true,\n writable: true,\n value: undefined,\n };\n }\n },\n\n defineProperty(target, key, descriptor) {\n try {\n // Normalize descriptor to avoid mixed accessor & data attributes\n const hasAccessor = \"get\" in descriptor || \"set\" in descriptor;\n\n if (hasAccessor) {\n // Build a clean descriptor without value/writable when accessors present\n const normalized = {\n configurable:\n \"configurable\" in descriptor ? descriptor.configurable : true,\n enumerable:\n \"enumerable\" in descriptor ? descriptor.enumerable : false,\n };\n if (\"get\" in descriptor) normalized.get = descriptor.get;\n if (\"set\" in descriptor) normalized.set = descriptor.set;\n\n // Store accessor references for inspection but avoid breaking invariants\n set(key, {\n get: descriptor.get,\n set: descriptor.set,\n });\n\n return Reflect.defineProperty(target, key, normalized);\n }\n\n // Data descriptor path\n set(key, descriptor.value);\n return Reflect.defineProperty(target, key, descriptor);\n } catch (e) {\n _error(\"Error defineProperty\", key, descriptor, e);\n return false;\n }\n },\n };\n\n // Create proxies once proxyHandler is defined\n const proxyWindow = new Proxy(oldWindow, proxyHandler);\n const proxyGlobalThis = new Proxy(oldGlobalThis, proxyHandler);\n const proxyGlobal = new Proxy(oldGlobal, proxyHandler);\n const proxySelf = new Proxy(oldSelf, proxyHandler);\n\n // Seed storage with core globals so lookups succeed inside `with` blocks\n Object.assign(__globalsStorage, {\n chrome,\n browser,\n window: proxyWindow,\n globalThis: proxyGlobalThis,\n global: proxyGlobal,\n self: proxySelf,\n document: oldWindow.document,\n });\n\n const __globals = {\n chrome,\n browser,\n window: proxyWindow,\n globalThis: proxyGlobalThis,\n global: proxyGlobal,\n self: proxySelf,\n __globals: __globalsStorage,\n };\n\n __globals.contextId = contextId;\n __globals.contextType = contextType;\n __globals.module = undefined;\n __globals.amd = undefined;\n __globals.define = undefined;\n __globals.importScripts = (...args) => {\n _log(\"importScripts\", args);\n };\n\n return __globals;\n}\n\n\nif (typeof window !== 'undefined') {\n window.buildPolyfill = buildPolyfill;\n}\n" let newMap = JSON.parse(JSON.stringify(EXTENSION_ASSETS_MAP)); delete newMap[OPTIONS_PAGE_PATH]; const PASS_ON = Object.fromEntries( Object.entries({ LOCALE_KEYS, INJECTED_MANIFEST, USED_LOCALE, EXTENSION_ICON, CURRENT_LOCATION, OPTIONS_PAGE_PATH, CAN_USE_BLOB_CSP, ALL_PERMISSIONS, ORIGIN_PERMISSIONS, EXTENSION_PERMISSIONS, SCRIPT_NAME, _base64ToBlob, _getMimeTypeFromPath, _isTextAsset, _createAssetUrl, _matchGlobPattern, _isWebAccessibleResource, _log, _warn, _error, }).map((i) => { let out = [...i]; if (typeof i[1] === "function") { out[1] = i[1].toString(); } else { out[1] = JSON.stringify(i[1]); } return out; }) ); _log(PASS_ON); return ` ${Object.entries(PASS_ON) .map( (i) => `const ${i[0]} = ${i[1]};\nwindow[${JSON.stringify(i[0])}] = ${i[0]}` ) .join("\n")} _log("Initialized polyfill", {${Object.keys(PASS_ON).join(", ")}}) ${polyfillString.replaceAll("{{EXTENSION_ASSETS_MAP}}", `JSON.parse(unescape(atob("${btoa(encodeURIComponent(JSON.stringify(EXTENSION_ASSETS_MAP)))}")))`)} // Initialize the polyfill context for options page const polyfillCtx = buildPolyfill({ isOtherPage: true }); const APPLY_TO = [window, self, globalThis]; for (const obj of APPLY_TO) { obj.chrome = polyfillCtx.chrome; obj.browser = polyfillCtx.browser; obj.INJECTED_MANIFEST = ${JSON.stringify(INJECTED_MANIFEST)}; } `; } async function main() { _log(`Initializing...`, performance.now()); if (typeof _initStorage === "function") { try { _initStorage() .then(() => { _log(`Storage initialized.`); }) .catch((e) => { _error("Error during storage initialization:", e); }); } catch (e) { _error("Error during storage initialization:", e); } } _log(`Starting content scripts...`); const currentUrl = window.location.href; let shouldRunAnyScript = false; _log(`Checking URL: ${currentUrl}`); if ( CONTENT_SCRIPT_CONFIGS_FOR_MATCHING && CONTENT_SCRIPT_CONFIGS_FOR_MATCHING.length > 0 ) { for (const config of CONTENT_SCRIPT_CONFIGS_FOR_MATCHING) { if ( config.matches && config.matches.some((pattern) => { try { const regex = convertMatchPatternToRegExp(pattern); if (regex.test(currentUrl)) { return true; } return false; } catch (e) { _error(`Error testing match pattern "${pattern}":`, e); return false; } }) ) { shouldRunAnyScript = true; _log(`URL match found via config:`, config); break; } } } else { _log(`No content script configurations found in manifest data.`); } if (shouldRunAnyScript) { let polyfillContext; try { polyfillContext = buildPolyfill({ isBackground: false }); } catch (e) { _error(`Failed to build polyfill:`, e); return; } _log(`Polyfill built. Executing combined script logic...`); // async function executeAllScripts({chrome, browser, global, window, globalThis, self, __globals}, extensionCssData) { await executeAllScripts.call( polyfillContext.globalThis, polyfillContext, extensionCssData ); } else { _log( `No matching content script patterns for this URL. No scripts will be executed.` ); } if (OPTIONS_PAGE_PATH) { if (typeof _registerMenuCommand === "function") { try { _registerMenuCommand("Open Options", openOptionsPage); _log(`Options menu command registered.`); } catch (e) { _error("Failed to register menu command", e); } } } if (POPUP_PAGE_PATH) { if (typeof _registerMenuCommand === "function") { try { _registerMenuCommand("Open Popup", openPopupPage); _log(`Popup menu command registered.`); } catch (e) { _error("Failed to register popup menu command", e); } } } _log(`Initialization sequence complete.`); } main()//.catch((e) => _error(`Error during script initialization:`, e)); try { const fnKey = "OPEN_OPTIONS_PAGE_" + String(SCRIPT_NAME).replace(/\s+/g, "_"); window[fnKey] = openOptionsPage; } catch (e) {} try { const fnKey = "OPEN_POPUP_PAGE_" + String(SCRIPT_NAME).replace(/\s+/g, "_"); window[fnKey] = openPopupPage; } catch (e) {} })(); // #endregion // #endregion