// ==UserScript== // @name Kapylup // @namespace https://github.com/hanenashi/cudloun // @version 0.1.3 // @description Interactive Kapyguts-powered Kapybara element explorer. // @author hanenashi // @match https://kapybara.okoun.cz/* // @run-at document-idle // @require https://raw.githubusercontent.com/hanenashi/cudloun/main/modules/sys-kapyguts.js?v=0.6.31 // @updateURL https://raw.githubusercontent.com/hanenashi/cudloun/main/kapylup.user.js // @downloadURL https://raw.githubusercontent.com/hanenashi/cudloun/main/kapylup.user.js // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM.getValue // @grant GM.setValue // @grant GM.registerMenuCommand // @grant GM.setClipboard // ==/UserScript== (function () { "use strict"; const VERSION = "0.1.3"; const SETTINGS_KEY = "kapylup.settings.v1"; const DEFAULTS = Object.freeze({ hotkey: "K", cyclePreviousKey: "[", cycleNextKey: "]", showPanelOnSelection: true, panel: { left: 24, top: 72, width: 430, height: 560 }, }); const state = { settings: null, selecting: false, panelVisible: false, view: "welcome", selectedElement: null, result: null, related: [], uiHost: null, shadow: null, panel: null, panelBody: null, panelTitle: null, modeBadge: null, cycleNav: null, highlight: null, cursorStyle: null, hoverFrame: 0, lastWheelCycle: 0, hoveredElement: null, candidates: [], candidateIndex: -1, pointer: { x: 0, y: 0 }, badgeTimer: 0, saveGeometryTimer: 0, resizeObserver: null, }; if (window.Kapylup?.version) return; void start(); async function start() { const kapyguts = window.Kapyguts; if (!kapyguts?.explain) { console.error("[Kapylup] Kapyguts did not load; interactive explorer is unavailable."); return; } state.settings = normalizeSettings(await readSetting(SETTINGS_KEY, DEFAULTS)); installUi(); installEvents(); registerMenus(); exposeApi(); renderWelcome(); console.info(`[Kapylup ${VERSION}] ready; Kapyguts ${kapyguts.version}; hotkey ${state.settings.hotkey}`); } function exposeApi() { window.Kapylup = Object.freeze({ version: VERSION, kapygutsVersion: window.Kapyguts.version, toggle: toggleSelection, startSelection: () => setSelecting(true), stopSelection: () => setSelecting(false), inspect: inspectElement, show: showPanel, hide: hidePanel, settings: openSettings, }); } function installUi() { const host = document.createElement("div"); host.setAttribute("data-kapylup-ui", "true"); const shadow = host.attachShadow({ mode: "open" }); shadow.innerHTML = `
`; document.documentElement.appendChild(host); const highlight = document.createElement("div"); highlight.setAttribute("data-kapylup-highlight", "true"); Object.assign(highlight.style, { position: "fixed", display: "none", zIndex: "2147483646", border: "2px solid #ff8a24", background: "rgba(255,138,36,.12)", boxShadow: "0 0 0 1px rgba(0,0,0,.7)", pointerEvents: "none", }); document.documentElement.appendChild(highlight); const cursorStyle = document.createElement("style"); cursorStyle.textContent = ` html[data-kapylup-selecting="true"], html[data-kapylup-selecting="true"] body, html[data-kapylup-selecting="true"] body * { cursor: crosshair !important; } `; document.documentElement.appendChild(cursorStyle); state.uiHost = host; state.shadow = shadow; state.panel = shadow.querySelector(".window"); state.panelBody = shadow.querySelector(".window-body"); state.panelTitle = shadow.querySelector(".window-title"); state.modeBadge = shadow.querySelector(".mode-badge"); state.cycleNav = shadow.querySelector(".cycle-nav"); state.highlight = highlight; state.cursorStyle = cursorStyle; applyPanelGeometry(); installPanelInteractions(); } function installEvents() { document.addEventListener("keydown", handleGlobalKeydown, true); document.addEventListener("pointermove", handlePointerMove, true); document.addEventListener("wheel", handleSelectionWheel, { capture: true, passive: false }); document.addEventListener("click", handleSelectionClick, true); window.addEventListener("resize", clampPanelGeometry); } function installPanelInteractions() { state.shadow.addEventListener("click", (event) => { const action = event.target.closest?.("[data-action]")?.dataset.action; if (action === "close") hidePanel(); if (action === "settings") openSettings(); if (action === "help") renderHelp(); if (action === "toggle") toggleSelection(); if (action === "copy") void handleCopy(event.target.dataset.copy); if (action === "save-settings") void saveSettingsFromPanel(); if (action === "reset-hotkey") setHotkeyDraft("K"); if (action === "reset-cycle-keys") { setHotkeyDraft("[", "cycle-previous"); setHotkeyDraft("]", "cycle-next"); } if (action === "back") state.result ? renderInspector() : renderWelcome(); const cycle = event.target.closest?.("[data-cycle]")?.dataset.cycle; if (cycle === "previous") cycleCandidate(-1); if (cycle === "next") cycleCandidate(1); if (cycle === "parent") cycleHierarchy(1); if (cycle === "child") cycleHierarchy(-1); if (cycle === "confirm" && state.hoveredElement) inspectElement(state.hoveredElement); }); const header = state.shadow.querySelector(".window-header"); header.addEventListener("pointerdown", beginPanelDrag); if (typeof ResizeObserver === "function") { state.resizeObserver = new ResizeObserver(() => scheduleGeometrySave()); state.resizeObserver.observe(state.panel); } } function handleGlobalKeydown(event) { if (event.composedPath().includes(state.uiHost)) return; if (event.key === "Escape" && state.selecting) { event.preventDefault(); setSelecting(false); return; } if (isEditableEvent(event) || event.repeat) return; const hotkey = eventHotkey(event); if (state.selecting && hotkey === state.settings.cyclePreviousKey) { event.preventDefault(); event.stopImmediatePropagation(); cycleCandidate(-1); return; } if (state.selecting && hotkey === state.settings.cycleNextKey) { event.preventDefault(); event.stopImmediatePropagation(); cycleCandidate(1); return; } if (hotkey !== state.settings.hotkey) return; event.preventDefault(); event.stopImmediatePropagation(); toggleSelection(); } function handlePointerMove(event) { if (!state.selecting || event.composedPath().includes(state.uiHost)) return; state.pointer = { x: event.clientX, y: event.clientY }; if (state.hoverFrame) return; state.hoverFrame = requestAnimationFrame(() => { state.hoverFrame = 0; refreshCandidateStack(state.pointer.x, state.pointer.y); }); } function handleSelectionWheel(event) { if (!state.selecting || event.composedPath().includes(state.uiHost) || !event.deltaY) return; event.preventDefault(); event.stopImmediatePropagation(); const now = performance.now(); if (now - state.lastWheelCycle < 80) return; state.lastWheelCycle = now; if (!state.candidates.length) refreshCandidateStack(event.clientX, event.clientY); cycleCandidate(event.deltaY > 0 ? 1 : -1); } function handleSelectionClick(event) { if (!state.selecting || event.composedPath().includes(state.uiHost)) return; if (!state.candidates.length) refreshCandidateStack(event.clientX, event.clientY); const element = state.hoveredElement || event.composedPath().find((node) => node instanceof Element && !isKapylupElement(node)); if (!element) return; event.preventDefault(); event.stopImmediatePropagation(); inspectElement(element); } function refreshCandidateStack(x, y) { const current = state.hoveredElement; const candidates = document.elementsFromPoint(x, y).filter((element, index, all) => ( isSelectableCandidate(element) && all.indexOf(element) === index )); state.candidates = candidates; const preservedIndex = candidates.indexOf(current); selectCandidate(preservedIndex >= 0 ? preservedIndex : candidates.length ? 0 : -1); } function isSelectableCandidate(element) { if (!isElement(element) || isKapylupElement(element)) return false; if (element === document.documentElement || element === document.body) return false; const rect = element.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return false; const style = getComputedStyle(element); return style.display !== "none" && style.visibility !== "hidden" && style.pointerEvents !== "none"; } function cycleCandidate(direction) { if (!state.candidates.length) return; const current = state.candidateIndex >= 0 ? state.candidateIndex : 0; selectCandidate((current + direction + state.candidates.length) % state.candidates.length); } function cycleHierarchy(direction) { if (!state.hoveredElement || !state.candidates.length) return; const current = state.candidateIndex; const indexes = direction > 0 ? Array.from({ length: state.candidates.length - current - 1 }, (_, index) => current + index + 1) : Array.from({ length: current }, (_, index) => current - index - 1); const next = indexes.find((index) => direction > 0 ? state.candidates[index].contains(state.hoveredElement) : state.hoveredElement.contains(state.candidates[index])); if (next !== undefined) selectCandidate(next); } function selectCandidate(index) { state.candidateIndex = index; state.hoveredElement = index >= 0 ? state.candidates[index] : null; highlightElement(state.hoveredElement); updateSelectionUi(); } function inspectElement(element) { const result = window.Kapyguts.explain(element); state.selectedElement = element; state.result = result; state.related = relatedElements(element, result); logInspection(result, state.related); highlightElement(result.target || element); renderInspector(); if (state.settings.showPanelOnSelection) showPanel(); return result; } function relatedElements(element, result) { const collected = new Map(); addRelated(collected, "selected", element); addRelated(collected, "translated target", result.target); const post = window.Kapyguts.allPosts().find((candidate) => candidate.contains(element)); if (post) addPartObject(collected, "post", window.Kapyguts.postParts(post)); const composer = window.Kapyguts.allComposers().find((candidate) => candidate.contains(element)); if (composer) addPartObject(collected, "composer", window.Kapyguts.composerParts(composer)); for (const [group, parts] of [ ["page header", window.Kapyguts.pageHeaderParts()], ["board header", window.Kapyguts.boardHeaderParts()], ]) { const elements = Object.values(parts).filter(isElement); if (elements.some((candidate) => candidate === element || candidate.contains(element))) { addPartObject(collected, group, parts); } } return Array.from(collected.values()).slice(0, 30); } function addPartObject(collected, group, parts) { Object.entries(parts || {}).forEach(([name, element]) => { if (isElement(element)) addRelated(collected, `${group}.${name}`, element); }); } function addRelated(collected, name, element) { if (!isElement(element) || collected.has(element)) return; const explanation = window.Kapyguts.explain(element); collected.set(element, { name, element, descriptor: describeElement(element), selector: explanation.recommendedSelector, }); } function logInspection(result, related) { const heading = result.recommendedSelector || describeElement(result.element) || "unknown element"; console.groupCollapsed(`[Kapylup] ${result.component}: ${heading}`); console.log("Selected element:", result.element); console.log("Kapyguts target:", result.target); console.log("Translation:", result); console.log("Related elements:", related.map(({ name, element, selector }) => ({ name, element, selector }))); console.groupEnd(); } function renderWelcome() { state.view = "welcome"; state.panelTitle.textContent = "Kapylup"; state.panelBody.innerHTML = `

Ruční průzkumník Kapybary. Přepněte výběrový kurzor, ukažte na prvek a klikněte. Překlad vždy poskytuje Kapyguts.

Kapylup
${VERSION}
Kapyguts
${escapeHtml(window.Kapyguts.version)}
Použití z konzole
Kapylup.inspect($0)\nKapyguts.explain($0)
`; } function renderHelp() { state.view = "help"; state.panelTitle.textContent = "Kapylup · nápověda"; state.panelBody.innerHTML = `

TL;DR: Stiskněte ${escapeHtml(state.settings.hotkey)}, ukažte na část Kapybary a klikněte. Kapylup ji přeloží přes Kapyguts a nabídne bezpečný selektor i CSS kostru ke zkopírování.

Překrývající se prvky
Výsledek

Každou hodnotu lze kopírovat zvlášť, nebo tlačítkem „Kopírovat vše“ získat celý souhrn. Okno můžete táhnout za záhlaví a měnit jeho velikost za pravý dolní roh.

`; showPanel(); } function renderInspector() { if (!state.result) { renderWelcome(); return; } state.view = "inspector"; const result = state.result; state.panelTitle.textContent = `Kapylup · ${result.component}`; state.panelBody.innerHTML = `
${fieldMarkup("Komponenta", result.component, "component")} ${fieldMarkup("Označený element", describeElement(result.element), "selected")} ${fieldMarkup("Přeložený element", describeElement(result.target), "target")} ${fieldMarkup("Doporučený selektor", result.recommendedSelector || "—", "selector")} ${fieldMarkup("Nepoužívat", result.avoid.join("\n") || "—", "avoid")} ${fieldMarkup("Poznámky", result.notes.join("\n") || "—", "notes")}
CSS kostra
Související elementy
`; state.panelBody.querySelector("textarea").value = result.css; } function fieldMarkup(label, value, copyKey) { return `
${escapeHtml(label)}
${escapeHtml(value)}
`; } function openSettings() { state.view = "settings"; state.panelTitle.textContent = "Kapylup · nastavení"; state.panelBody.innerHTML = `

Nastavení se ukládá přes userscript manager. Klávesová zkratka se ignoruje při psaní do editorů a formulářů.

Verze Kapylupu
${VERSION}
Zdroj překladu
Kapyguts ${escapeHtml(window.Kapyguts.version)}
`; state.panelBody.querySelectorAll(".hotkey-input").forEach((input) => input.addEventListener("keydown", captureHotkey)); showPanel(); } function captureHotkey(event) { event.preventDefault(); event.stopPropagation(); const hotkey = eventHotkey(event); if (!hotkey || hotkey === "Escape" || modifierOnly(event.key)) { showBadge("Tuto klávesu nelze použít."); return; } event.currentTarget.value = hotkey; } function setHotkeyDraft(value, setting = "hotkey") { const input = state.panelBody.querySelector(`[data-setting='${setting}']`); if (input) input.value = value; } async function saveSettingsFromPanel() { const hotkey = state.panelBody.querySelector("[data-setting='hotkey']")?.value || "K"; const cyclePreviousKey = state.panelBody.querySelector("[data-setting='cycle-previous']")?.value || "["; const cycleNextKey = state.panelBody.querySelector("[data-setting='cycle-next']")?.value || "]"; const showPanelOnSelection = !!state.panelBody.querySelector("[data-setting='show-panel']")?.checked; if (new Set([hotkey, cyclePreviousKey, cycleNextKey]).size !== 3) { showBadge("Ovládací klávesy musí být navzájem odlišné."); return; } state.settings = normalizeSettings({ ...state.settings, hotkey, cyclePreviousKey, cycleNextKey, showPanelOnSelection }); await writeSetting(SETTINGS_KEY, state.settings); showBadge("Nastavení uloženo."); state.result ? renderInspector() : renderWelcome(); } async function handleCopy(key) { const result = state.result; let value = ""; if (key === "console-help") value = "Kapylup.inspect($0)\nKapyguts.explain($0)"; if (result) { if (key === "all") value = fullSummary(); if (key === "component") value = result.component; if (key === "selected") value = describeElement(result.element); if (key === "target") value = describeElement(result.target); if (key === "selector") value = result.recommendedSelector; if (key === "avoid") value = result.avoid.join("\n"); if (key === "notes") value = result.notes.join("\n"); if (key === "css") value = result.css; if (key?.startsWith("related:")) { const related = state.related[Number(key.split(":")[1])]; value = related?.selector || related?.descriptor || ""; } } if (!value) { showBadge("Není co kopírovat."); return; } await copyText(value); showBadge("Zkopírováno."); } function fullSummary() { const result = state.result; const route = window.Kapyguts.route(); return [ `Kapylup ${VERSION} / Kapyguts ${window.Kapyguts.version}`, `Route: ${route.path}${route.search}${route.hash}`, `Component: ${result.component}`, `Selected: ${describeElement(result.element)}`, `Target: ${describeElement(result.target)}`, `Selector: ${result.recommendedSelector || "—"}`, `Avoid: ${result.avoid.join(", ") || "—"}`, `Notes: ${result.notes.join(" | ") || "—"}`, "", result.css || "", "", "Related:", ...state.related.map((item) => `- ${item.name}: ${item.selector || item.descriptor}`), ].join("\n"); } function toggleSelection() { setSelecting(!state.selecting); return state.selecting; } function setSelecting(enabled) { state.selecting = enabled === true; document.documentElement.toggleAttribute("data-kapylup-selecting", state.selecting); if (!state.selecting) { state.highlight.style.display = "none"; state.hoveredElement = null; state.candidates = []; state.candidateIndex = -1; } state.cycleNav.dataset.visible = state.selecting ? "true" : "false"; showBadge( state.selecting ? selectionBadgeText() : "Kapylup: normální kurzor", state.selecting, ); if (state.view === "welcome") renderWelcome(); if (state.view === "inspector") renderInspector(); } function highlightElement(element) { if (!isElement(element) || !element.isConnected) { state.highlight.style.display = "none"; return; } const rect = element.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { state.highlight.style.display = "none"; return; } Object.assign(state.highlight.style, { display: "block", left: `${Math.max(0, rect.left)}px`, top: `${Math.max(0, rect.top)}px`, width: `${Math.min(window.innerWidth, rect.right) - Math.max(0, rect.left)}px`, height: `${Math.min(window.innerHeight, rect.bottom) - Math.max(0, rect.top)}px`, }); } function showPanel(view = "") { state.panelVisible = true; state.panel.dataset.visible = "true"; clampPanelGeometry(); if (view === "settings") openSettings(); else if (view === "inspector") renderInspector(); else if (view === "welcome") renderWelcome(); } function hidePanel() { state.panelVisible = false; state.panel.dataset.visible = "false"; } function beginPanelDrag(event) { if (event.button !== 0 || event.target.closest("button,input")) return; event.preventDefault(); const rect = state.panel.getBoundingClientRect(); const origin = { x: event.clientX, y: event.clientY, left: rect.left, top: rect.top }; state.panel.setPointerCapture(event.pointerId); const move = (moveEvent) => { state.panel.style.left = `${origin.left + moveEvent.clientX - origin.x}px`; state.panel.style.top = `${origin.top + moveEvent.clientY - origin.y}px`; clampPanelGeometry(); }; const finish = () => { state.panel.removeEventListener("pointermove", move); state.panel.removeEventListener("pointerup", finish); state.panel.removeEventListener("pointercancel", finish); scheduleGeometrySave(); }; state.panel.addEventListener("pointermove", move); state.panel.addEventListener("pointerup", finish); state.panel.addEventListener("pointercancel", finish); } function applyPanelGeometry() { const panel = state.settings.panel; state.panel.style.left = `${panel.left}px`; state.panel.style.top = `${panel.top}px`; state.panel.style.width = `${panel.width}px`; state.panel.style.height = `${panel.height}px`; clampPanelGeometry(); } function clampPanelGeometry() { if (!state.panel) return; const rect = state.panel.getBoundingClientRect(); const width = Math.min(Math.max(280, rect.width || state.settings.panel.width), Math.max(280, window.innerWidth - 8)); const height = Math.min(Math.max(260, rect.height || state.settings.panel.height), Math.max(260, window.innerHeight - 8)); const left = Math.min(Math.max(4, rect.left || state.settings.panel.left), Math.max(4, window.innerWidth - width - 4)); const top = Math.min(Math.max(4, rect.top || state.settings.panel.top), Math.max(4, window.innerHeight - height - 4)); Object.assign(state.panel.style, { left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px`, }); } function scheduleGeometrySave() { clearTimeout(state.saveGeometryTimer); state.saveGeometryTimer = window.setTimeout(() => void savePanelGeometry(), 250); } async function savePanelGeometry() { if (!state.panel || !state.settings) return; const rect = state.panel.getBoundingClientRect(); if (!state.panelVisible || rect.width <= 0 || rect.height <= 0) return; state.settings = normalizeSettings({ ...state.settings, panel: { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, }); await writeSetting(SETTINGS_KEY, state.settings); } function showBadge(text, persistent = false) { clearTimeout(state.badgeTimer); state.modeBadge.textContent = text; state.modeBadge.dataset.visible = "true"; if (!persistent) { state.badgeTimer = window.setTimeout(() => { if (state.selecting) { state.modeBadge.textContent = selectionBadgeText(); state.modeBadge.dataset.visible = "true"; } else { state.modeBadge.dataset.visible = "false"; } }, 1500); } } function selectionBadgeText() { if (state.hoveredElement && state.candidates.length) { const component = window.Kapyguts.explain(state.hoveredElement).component; return `Kapylup · ${state.candidateIndex + 1}/${state.candidates.length} · ${component} · klik = vybrat`; } return `Kapylup: vyberte prvek · ${state.settings.cyclePreviousKey}/${state.settings.cycleNextKey} nebo kolečko pro vrstvy · Esc ukončí`; } function updateSelectionUi() { if (!state.selecting) return; showBadge(selectionBadgeText(), true); state.cycleNav.querySelector('[data-cycle="previous"]').title = `Předchozí (${state.settings.cyclePreviousKey})`; state.cycleNav.querySelector('[data-cycle="next"]').title = `Další (${state.settings.cycleNextKey})`; } function isKapylupElement(element) { return element === state.uiHost || element === state.highlight || element.hasAttribute("data-kapylup-ui") || element.hasAttribute("data-kapylup-highlight"); } function isEditableEvent(event) { return event.composedPath().some((node) => isElement(node) && ( node.matches("input,textarea,select,[contenteditable='true']") || node.isContentEditable )); } function eventHotkey(event) { const key = normalizedKey(event.key); if (!key || modifierOnly(key)) return ""; const modifiers = []; if (event.ctrlKey) modifiers.push("Ctrl"); if (event.altKey) modifiers.push("Alt"); if (event.shiftKey) modifiers.push("Shift"); if (event.metaKey) modifiers.push("Meta"); modifiers.push(key.length === 1 ? key.toUpperCase() : key); return modifiers.join("+"); } function normalizedKey(key) { if (key === " ") return "Space"; if (key === "Esc") return "Escape"; return String(key || ""); } function modifierOnly(key) { return ["Control", "Alt", "Shift", "Meta"].includes(key); } function describeElement(element) { if (!isElement(element)) return "—"; const tag = element.tagName.toLocaleLowerCase("en"); const id = element.id ? `#${element.id}` : ""; const classes = Array.from(element.classList).map((name) => `.${name}`).join(""); return `${tag}${id}${classes}`; } function isElement(value) { return value instanceof Element; } function normalizeSettings(value) { const panel = value?.panel || {}; return { hotkey: typeof value?.hotkey === "string" && value.hotkey && value.hotkey !== "Escape" ? value.hotkey : DEFAULTS.hotkey, cyclePreviousKey: typeof value?.cyclePreviousKey === "string" && value.cyclePreviousKey && value.cyclePreviousKey !== "Escape" ? value.cyclePreviousKey : DEFAULTS.cyclePreviousKey, cycleNextKey: typeof value?.cycleNextKey === "string" && value.cycleNextKey && value.cycleNextKey !== "Escape" ? value.cycleNextKey : DEFAULTS.cycleNextKey, showPanelOnSelection: value?.showPanelOnSelection !== false, panel: { left: finite(panel.left, DEFAULTS.panel.left), top: finite(panel.top, DEFAULTS.panel.top), width: finite(panel.width, DEFAULTS.panel.width), height: finite(panel.height, DEFAULTS.panel.height), }, }; } function finite(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; } async function readSetting(key, fallback) { try { if (typeof GM_getValue === "function") return GM_getValue(key, fallback); if (typeof GM !== "undefined" && typeof GM.getValue === "function") return await GM.getValue(key, fallback); const raw = localStorage.getItem(key); return raw === null ? fallback : JSON.parse(raw); } catch (_error) { return fallback; } } async function writeSetting(key, value) { try { if (typeof GM_setValue === "function") return GM_setValue(key, value); if (typeof GM !== "undefined" && typeof GM.setValue === "function") return await GM.setValue(key, value); localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.warn("[Kapylup] Settings could not be saved.", error?.name || "Error"); } return undefined; } function registerMenus() { registerMenu(`Kapylup: nastavení (v${VERSION})`, openSettings); registerMenu("Kapylup: přepnout výběr prvku", toggleSelection); registerMenu("Kapylup: ukázat/skrýt okno", () => state.panelVisible ? hidePanel() : showPanel()); } function registerMenu(label, callback) { try { if (typeof GM_registerMenuCommand === "function") GM_registerMenuCommand(label, callback); else if (typeof GM !== "undefined" && typeof GM.registerMenuCommand === "function") GM.registerMenuCommand(label, callback); } catch (_error) { // Menu support is optional; keyboard and window controls remain available. } } async function copyText(text) { if (typeof GM_setClipboard === "function") { GM_setClipboard(text, "text"); return; } if (typeof GM !== "undefined" && typeof GM.setClipboard === "function") { await GM.setClipboard(text, "text"); return; } if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; } const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); textarea.remove(); } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } })();