Sniff URLs in HTML
// ==UserScript== // @name URL Sniffer // @name:zh-CN URL 嗅探器 // @namespace https://gera2ld.space/ // @description Sniff URLs in HTML // @description:zh-CN 从 HTML 中嗅探 URL // @match *://*/* // @version 0.2.0 // @author Gerald <[email protected]> // @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/[email protected] // @supportURL https://github.com/intellilab/url-sniffer.user.js // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_unregisterMenuCommand // ==/UserScript== (function () { 'use strict'; function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } var styles = {"root":"style-module_root__1vyw2","toast":"style-module_toast__OcS5G","image":"style-module_image__1P0bD"}; var stylesheet=".style-module_root__1vyw2{background:#0008;inset:0;position:fixed;z-index:10000}.style-module_root__1vyw2:before{background:#0008;color:#bbb;content:\"Double click anywhere on the mask to exit\";font-size:12px;left:50%;padding:8px 16px;position:absolute;top:0;transform:translateX(-50%)}.style-module_root__1vyw2>*{border:2px solid;left:0;position:absolute;top:0}.style-module_toast__OcS5G{z-index:10001!important}.style-module_image__1P0bD{background:#0008;inset:80px;overflow:auto;position:absolute}.style-module_image__1P0bD>img{position:absolute;transform-origin:top left}"; const _excluded = ["elements", "getItem"]; const STYLE_CURRENT = { stroke: '#0f08', fill: '#0f02' }; const STYLE_SELECTION = { stroke: '#bbf8', fill: '#bbf2' }; const STYLE_SELECTED = { stroke: '#ff08', fill: '#ff02' }; const STYLE_TO_DESELECT = { stroke: '#88d8', fill: '#88d2' }; const STYLE_TO_SELECT = { stroke: '#bb08', fill: '#bb02' }; const MODE_SINGLE = 0; const MODE_MULTIPLE = 1; let rendering = false; const mask = VM.getHostElement(false); mask.addStyle(stylesheet); mask.root.className = styles.root; mask.root.addEventListener('mousedown', handleMouseDown); mask.root.addEventListener('mouseup', handleMouseUp); mask.root.addEventListener('mousemove', handleMouseMove); mask.root.addEventListener('click', handleClick); mask.root.addEventListener('dblclick', handleCallback); GM_registerMenuCommand('Sniff links', sniffLinks); GM_registerMenuCommand('Sniff images', sniffImages); let context; function sniffLinks() { if (context) close(); start({ elements: document.querySelectorAll('a[href]'), getItem(el) { const href = el.tagName.toLowerCase() === 'a' && el.getAttribute('href'); if (href && !/^(?:#|javascript:)/.test(href)) return { el }; }, mode: MODE_MULTIPLE, callback(selectedItems) { copy(selectedItems); close(); } }); } function sniffImages() { if (context) close(); const imageViewer = VM.hm("div", { className: styles.image, onClick: e => e.stopPropagation() }); const showImage = img => { mask.root.append(imageViewer); const { naturalWidth, naturalHeight } = img; const containerWidth = imageViewer.clientWidth; const containerHeight = imageViewer.clientHeight; const scale = Math.min(1, containerWidth / naturalWidth); const width = naturalWidth * scale; const height = naturalHeight * scale; const x = Math.max(0, (containerWidth - width) / 2); const y = Math.max(0, (containerHeight - height) / 2); imageViewer.innerHTML = ''; imageViewer.append(img); img.style.transform = `scale(${scale}) translate(${x}px,${y}px)`; context.paused = true; mask.root.addEventListener('click', closeViewer); }; const closeViewer = () => { imageViewer.innerHTML = ''; imageViewer.remove(); context.paused = false; mask.root.removeEventListener('click', closeViewer); }; start({ getItem(el) { let url; if (el.tagName.toLowerCase() === 'img') { url = el.src; } else { const bgImg = el.style.backgroundImage.match(/^url\((['"]?)(.*?)\1\)/); url = bgImg == null ? void 0 : bgImg[2]; } return url && { el, url }; }, mode: MODE_SINGLE, callback([item]) { if (!item) return close(); const img = new Image(); img.src = item.url; img.onload = () => { showImage(img); }; } }); } function start(opts) { if (context) throw new Error('Context already exists'); const { elements, getItem } = opts, rest = _objectWithoutPropertiesLoose(opts, _excluded); const items = Array.from(elements || document.querySelectorAll('*')).map(getItem).filter(Boolean); context = _extends({}, rest, { items, index: -1, active: null, disconnect: VM.observe(document.body, mutations => { mutations.forEach(mut => { if (mut.type === 'childList') { const newItems = Array.from(mut.addedNodes).filter(el => !mask.root.contains(el)).map(getItem).filter(Boolean); context.items.push(...newItems); } }); }) }); update(); mask.show(); document.addEventListener('scroll', update); document.addEventListener('resize', update); } function close() { if (!context) return; context.disconnect == null ? void 0 : context.disconnect(); mask.root.innerHTML = ''; mask.hide(); context = null; document.removeEventListener('scroll', update); document.removeEventListener('resize', update); GM_unregisterMenuCommand('Copy URLs'); } function update() { if (rendering) return; rendering = true; requestAnimationFrame(() => { context.items.forEach(item => { const rect = item.el.getBoundingClientRect(); item.pos = { x: rect.left, y: rect.top, w: rect.width, h: rect.height }; }); render(); rendering = false; }); } function render() { renderActive(); renderSelected(); } function updateStyle(el, style) { el.style.borderColor = style.stroke; el.style.background = style.fill; } function updatePosition(el, pos, padding = 2) { Object.assign(el.style, { width: `${pos.w + padding * 2}px`, height: `${pos.h + padding * 2}px`, transform: `translate(${pos.x - padding}px,${pos.y - padding}px)` }); } function renderActive() { const activeItem = !context.dragging && context.items[context.index]; if (!activeItem) { if (context.active) { context.active.remove(); context.active = null; } } else { if (!context.active) { context.active = VM.hm(mask.id, null); updateStyle(context.active, STYLE_CURRENT); mask.root.append(context.active); } updatePosition(context.active, activeItem.pos); } } function renderSelected() { context.items.forEach(item => { if (item.rect) updatePosition(item.rect, item.pos); }); } function setItemRect(item, style) { if (style) { if (!item.rect) { item.rect = VM.hm(mask.id, null); mask.root.append(item.rect); } updateStyle(item.rect, style); updatePosition(item.rect, item.pos); } else if (item.rect) { item.rect.remove(); item.rect = null; } } function handleClick() { if (context.paused) return; const activeItem = context.items[context.index]; if (activeItem) { if (context.mode === MODE_SINGLE) { context.callback([activeItem]); } else { activeItem.selected = !activeItem.selected; setItemRect(activeItem, activeItem.selected && STYLE_SELECTED); } } } function handleMouseDown(e) { if (context.dragging || context.mode === MODE_SINGLE || context.paused) return; const x = e.clientX; const y = e.clientY; context.dragging = { x, y }; } function handleMouseMove(e) { if (context.paused) return; const x = e.clientX; const y = e.clientY; if (context.dragging) { if (!context.dragging.rect) { const rect = VM.hm(mask.id, null); updateStyle(rect, STYLE_SELECTION); mask.root.append(rect); context.dragging.rect = rect; } context.index = -1; let x0 = context.dragging.x; let y0 = context.dragging.y; const w = Math.abs(x - x0); const h = Math.abs(y - y0); x0 = Math.min(x0, x); y0 = Math.min(y0, y); updatePosition(context.dragging.rect, { x: x0, y: y0, w, h }, 0); context.items.forEach(item => { item.inSelection = item.pos.x >= x0 && item.pos.x + item.pos.w <= x0 + w && item.pos.y >= y0 && item.pos.y + item.pos.h <= y0 + h; const state = (item.inSelection ? 2 : 0) + (item.selected ? 1 : 0); setItemRect(item, { 1: STYLE_SELECTED, 2: STYLE_TO_SELECT, 3: STYLE_TO_DESELECT }[state]); }); } else { context.index = context.items.findIndex(({ pos }) => x >= pos.x && x <= pos.x + pos.w && y >= pos.y && y <= pos.y + pos.h); } render(); } function handleMouseUp() { if (!context.dragging) return; if (context.dragging.rect) { context.dragging.rect.remove(); context.items.forEach(item => { if (item.inSelection) { item.inSelection = false; item.selected = !item.selected; setItemRect(item, item.selected && STYLE_SELECTED); } }); } context.dragging = null; } function handleCallback() { const selectedItems = context.items.filter(item => item.selected); context.callback(selectedItems); } function copy(selectedItems) { const urls = selectedItems.map(item => item.el.href); if (!urls.length) return; GM_setClipboard(urls.join('\r\n')); VM.showToast('URLs copied', { shadow: false, className: styles.toast }); } })();