URL Sniffer

Sniff URLs in HTML

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();