URL Sniffer

Sniff URLs in HTML

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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
  });
}

})();