// ==UserScript==
// @name Axiom Trade - Keyword Highlighter
// @namespace [URL REMOVED]
// @version 7.3.0
// @description Highlights whole-word keywords only inside Axiom's tweet/profile popups
// @author You
// @match [URL REMOVED]
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'axiom_highlight_keywords';
const POS_KEY = 'axiom_panel_position';
const SETTINGS_ID = 'axiom-kw-settings';
const HOST_ID = 'axiom-kw-host';
const HIGHLIGHT_CLR = '#FFE033';
const HIGHLIGHT_BG = '#2a2000';
const MARK_ATTR = 'data-axiom-kw';
// ─── Storage ──────────────────────────────────────────────────────────────────
function getKeywords() {
try { return JSON.parse(GM_getValue(STORAGE_KEY, '[]')); }
catch { return []; }
}
function saveKeywords(list) {
GM_setValue(STORAGE_KEY, JSON.stringify(list));
}
function getSavedPos() {
try { return JSON.parse(GM_getValue(POS_KEY, 'null')); }
catch { return null; }
}
function savePos(x, y) {
GM_setValue(POS_KEY, JSON.stringify({ x, y }));
}
// ─── Pattern (whole-word, case-insensitive) ───────────────────────────────────
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function buildPattern(keywords) {
const terms = keywords.map(k => k.trim()).filter(Boolean).map(escapeRegex);
return terms.length ? new RegExp(`\\b(${terms.join('|')})\\b`, 'gi') : null;
}
// ─── DOM Highlighting ─────────────────────────────────────────────────────────
const SKIP_TAGS = new Set(['SCRIPT','STYLE','NOSCRIPT','TEXTAREA','INPUT','SELECT','MARK']);
function highlightInContainer(container, pattern) {
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const p = node.parentElement;
if (!p || SKIP_TAGS.has(p.tagName)) return NodeFilter.FILTER_REJECT;
if (p.closest(`[${MARK_ATTR}]`)) return NodeFilter.FILTER_REJECT;
if (!node.textContent.trim()) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
const nodes = [];
let n;
while ((n = walker.nextNode())) nodes.push(n);
for (const tn of nodes) {
const text = tn.textContent;
pattern.lastIndex = 0;
if (!pattern.test(text)) continue;
pattern.lastIndex = 0;
const frag = document.createDocumentFragment();
let last = 0, m;
while ((m = pattern.exec(text)) !== null) {
if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index)));
const mark = document.createElement('mark');
mark.setAttribute(MARK_ATTR, '1');
mark.style.cssText =
`background:${HIGHLIGHT_CLR}!important;color:#1a1400!important;` +
`padding:0 2px;border-radius:2px;font-weight:700;font-style:normal;text-decoration:none;`;
mark.textContent = m[0];
frag.appendChild(mark);
last = pattern.lastIndex;
}
if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
pattern.lastIndex = 0;
if (tn.parentNode) tn.parentNode.replaceChild(frag, tn);
}
}
// ─── Popup Detection ──────────────────────────────────────────────────────────
function isTweetPopup(el) {
if (!el || !el.querySelector) return false;
const cs = window.getComputedStyle(el);
if (cs.position !== 'fixed' && cs.position !== 'absolute') return false;
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
const text = el.textContent || '';
if (text.trim().length < 10) return false;
if (/read more on (x|twitter)/i.test(text)) return true;
if (/joined\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d{4}/i.test(text)) return true;
if (/[\d,.]+\s*(k|m|b)?\s+followers?/i.test(text)) return true;
return false;
}
function alreadyHighlighted(el) {
return !!el.querySelector(`[${MARK_ATTR}]`);
}
function isOwnUI(el) {
return el.id === HOST_ID || el.id === SETTINGS_ID || !!el.closest?.('#' + HOST_ID);
}
// ─── Process a candidate element ─────────────────────────────────────────────
function processPopup(el) {
if (!el || el.nodeType !== 1) return false;
if (isOwnUI(el)) return false;
if (!isTweetPopup(el)) return false;
if (alreadyHighlighted(el)) return false;
const kws = getKeywords();
if (!kws.length) return false;
const pattern = buildPattern(kws);
if (!pattern) return false;
highlightInContainer(el, pattern);
return true;
}
// ─── Cursor-targeted scan ─────────────────────────────────────────────────────
let cursorX = 0, cursorY = 0;
document.addEventListener('mousemove', e => { cursorX = e.clientX; cursorY = e.clientY; }, true);
let scanInterval = null;
let hoverWindowOpen = false;
let hoverTimer = null;
function startCursorScan() {
stopCursorScan();
let ticks = 0;
scanInterval = setInterval(() => {
ticks++;
if (ticks > 30 || !hoverWindowOpen) { stopCursorScan(); return; }
const pts = [
[cursorX, cursorY],
[cursorX, cursorY + 120],
[cursorX, cursorY + 250],
[cursorX - 150, cursorY + 120],
[cursorX + 150, cursorY + 120],
[cursorX, cursorY - 120],
];
const seen = new Set();
for (const [x, y] of pts) {
if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) continue;
for (const el of document.elementsFromPoint(x, y)) {
if (seen.has(el)) continue;
seen.add(el);
if (processPopup(el)) { stopCursorScan(); return; }
}
}
}, 100);
}
function stopCursorScan() {
if (scanInterval) { clearInterval(scanInterval); scanInterval = null; }
}
// ─── Social Icon Hover Detection ──────────────────────────────────────────────
function isTwitterHref(href) {
return /https?:\/\/(www\.)?(twitter|x)\.com\//.test(href || '');
}
function findSocialAnchor(target) {
let node = target;
for (let i = 0; i < 8; i++) {
if (!node) break;
if (node.tagName === 'A' && isTwitterHref(node.href)) return node;
if (node.querySelector) {
const a = node.querySelector('a[href*="twitter.com/"],a[href*="x.com/"]');
if (a) return a;
}
node = node.parentElement;
}
return null;
}
document.addEventListener('mouseover', e => {
if (!findSocialAnchor(e.target)) return;
hoverWindowOpen = true;
clearTimeout(hoverTimer);
hoverTimer = setTimeout(() => { hoverWindowOpen = false; stopCursorScan(); }, 3000);
startCursorScan();
}, true);
// ─── MutationObserver (handles React portal popups added to DOM) ──────────────
new MutationObserver(mutations => {
if (!hoverWindowOpen) return;
for (const mut of mutations) {
if (mut.type !== 'childList') continue;
for (const node of mut.addedNodes) {
if (node.nodeType !== 1) continue;
if (processPopup(node)) continue;
for (const child of (node.children || [])) {
if (processPopup(child)) break;
for (const gc of (child.children || [])) {
if (processPopup(gc)) break;
}
}
}
}
}).observe(document.documentElement, { childList: true, subtree: true });
// ─── Drag helper ─────────────────────────────────────────────────────────────
//
// Attaches to a handle element and moves the host around the screen.
// On first drag it switches the host from centred (left:50%+transform)
// to absolute pixel coordinates so it tracks the cursor exactly.
// Position is saved and restored across page loads.
function makeDraggable(handle, host) {
let dragging = false;
let startMouseX, startMouseY, startElX, startElY;
function applyPos(x, y) {
// Clamp inside viewport
const W = window.innerWidth, H = window.innerHeight;
const w = host.offsetWidth || 300;
const h = host.offsetHeight || 50;
x = Math.max(0, Math.min(x, W - w));
y = Math.max(0, Math.min(y, H - h));
host.style.left = x + 'px';
host.style.top = y + 'px';
host.style.transform = 'none';
}
function getHostPos() {
const r = host.getBoundingClientRect();
return { x: r.left, y: r.top };
}
handle.addEventListener('mousedown', e => {
// Only drag on left-button on the handle itself (not on buttons/inputs inside)
if (e.button !== 0) return;
if (e.target.closest('button,input,a,[id="axiom-kw-close"]')) return;
e.preventDefault();
dragging = true;
const pos = getHostPos();
startElX = pos.x;
startElY = pos.y;
startMouseX = e.clientX;
startMouseY = e.clientY;
handle.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
const dx = e.clientX - startMouseX;
const dy = e.clientY - startMouseY;
applyPos(startElX + dx, startElY + dy);
}, true);
document.addEventListener('mouseup', e => {
if (!dragging) return;
dragging = false;
handle.style.cursor = 'grab';
const pos = getHostPos();
savePos(pos.x, pos.y);
}, true);
}
// ─── Settings Panel ───────────────────────────────────────────────────────────
let panelHost = null;
function ensurePanelHost() {
if (panelHost && document.documentElement.contains(panelHost)) return panelHost;
panelHost = document.createElement('div');
panelHost.id = HOST_ID;
const saved = getSavedPos();
if (saved) {
panelHost.style.cssText =
`all:unset;position:fixed;top:${saved.y}px;left:${saved.x}px;transform:none;` +
'z-index:2147483647;pointer-events:all;' +
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;';
} else {
panelHost.style.cssText =
'all:unset;position:fixed;top:10px;left:50%;transform:translateX(-50%);' +
'z-index:2147483647;pointer-events:all;' +
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;';
}
document.documentElement.appendChild(panelHost);
return panelHost;
}
function createSettingsPanel() {
const host = ensurePanelHost();
if (host.querySelector('#' + SETTINGS_ID)) return;
// ── Collapsed pill: small square with bold K ──────────────────────────────
const pill = document.createElement('div');
pill.id = 'axiom-kw-pill';
pill.title = 'Keyword Highlighter — drag to move';
pill.style.cssText =
'display:none;width:34px;height:34px;border-radius:8px;' +
'background:#181818;border:1px solid #2a2a2a;cursor:grab;' +
'box-shadow:0 4px 16px rgba(0,0,0,0.8);' +
'align-items:center;justify-content:center;';
pill.innerHTML = ``;
// ── Full panel ────────────────────────────────────────────────────────────
const panel = document.createElement('div');
panel.id = SETTINGS_ID;
panel.style.cssText =
'background:#0f0f0f;border:1px solid #2a2a2a;border-radius:12px;' +
'box-shadow:0 8px 32px rgba(0,0,0,0.9);font-size:13px;color:#e0e0e0;width:284px;overflow:hidden;';
panel.innerHTML = `