Greasy Fork is available in English.

Ctrl-Hover Linkifier

Hold Ctrl and hover over plain text URLs, hostnames, or IPs to temporarily turn them into clickable links.

当前为 2026-07-17 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Ctrl-Hover Linkifier
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Hold Ctrl and hover over plain text URLs, hostnames, or IPs to temporarily turn them into clickable links.
// @author       Gemini, skygate2012
// @match        *://*/*
// @license MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // State variables
    let mouseX = 0;
    let mouseY = 0;
    let ctrlHeld = false;
    let activeLinkData = null;

    // Regex to detect standard URLs, domains (e.g. example.com), and IPs (e.g. 192.168.1.1)
    const linkRegex = /^(?:https?:\/\/)?(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})(?::\d+)?(?:\/[^\s]*)?$/i;

    // Boundary check for word extraction
    const isBoundary = (char) => /[\s<>"']/.test(char);

    // Track mouse position continuously (extremely cheap operation)
    document.addEventListener('mousemove', (e) => {
        mouseX = e.clientX;
        mouseY = e.clientY;

        // If a temporary link exists, check if we are still hovering it
        if (activeLinkData) {
            // e.target is the element currently under the mouse
            if (e.target === activeLinkData.aElement) {
                return; // Still hovering the created link
            } else {
                revertLink(); // Mouse moved away
            }
        }

        // If Ctrl is held and we have no active link, scan the point
        if (ctrlHeld) {
            processPoint(mouseX, mouseY);
        }
    }, { passive: true });

    // Track Ctrl key press
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Control' && !ctrlHeld) {
            ctrlHeld = true;
            processPoint(mouseX, mouseY);
        }
    }, { passive: true });

    // Track Ctrl key release
    document.addEventListener('keyup', (e) => {
        if (e.key === 'Control') {
            ctrlHeld = false;
            revertLink();
        }
    }, { passive: true });

    // Revert the injected <a> tag back to a standard text node
    function revertLink() {
        if (!activeLinkData) return;

        const { aElement, word } = activeLinkData;

        // Ensure the element is still in the DOM before trying to replace it
        if (aElement.parentNode) {
            const textNode = document.createTextNode(word);
            aElement.replaceWith(textNode);
        }

        activeLinkData = null;
    }

    // Identify text under the cursor and linkify if appropriate
    function processPoint(x, y) {
        if (activeLinkData) return;

        // Cross-browser method to get the exact text node and character offset under the mouse
        let textNode, offset;
        if (document.caretPositionFromPoint) {
            const pos = document.caretPositionFromPoint(x, y);
            if (!pos) return;
            textNode = pos.offsetNode;
            offset = pos.offset;
        } else if (document.caretRangeFromPoint) { // WebKit / Blink fallback
            const range = document.caretRangeFromPoint(x, y);
            if (!range) return;
            textNode = range.startContainer;
            offset = range.startOffset;
        } else {
            return;
        }

        // Bail out early if we aren't hovering over a text node
        if (textNode.nodeType !== Node.TEXT_NODE) return;

        const parent = textNode.parentNode;

        // Fast bailouts: already in a link, or inside an input/editable area
        if (!parent || parent.closest('a') || parent.closest('input, textarea, [contenteditable="true"]')) return;

        const text = textNode.nodeValue;
        if (!text) return;

        // Expand outwards from the mouse offset to find the current "word" boundaries
        let start = offset;
        while (start > 0 && !isBoundary(text[start - 1])) {
            start--;
        }

        let end = offset;
        while (end < text.length && !isBoundary(text[end])) {
            end++;
        }

        let word = text.slice(start, end);
        if (word.length < 4) return; // Too short to be a valid domain/IP

        // Trim trailing punctuation (e.g. end of a sentence like "example.com.")
        let trimEnd = 0;
        while (word.length > 0 && /[.,!?;:]$/.test(word)) {
            word = word.slice(0, -1);
            trimEnd++;
        }
        end -= trimEnd;

        // Trim leading punctuation
        let trimStart = 0;
        while (word.length > 0 && /^[([]/.test(word)) {
            word = word.slice(1);
            trimStart++;
        }
        start += trimStart;

        // Test if the extracted word resembles a link
        if (linkRegex.test(word)) {
            createLink(textNode, start, end, word);
        }
    }

    // Convert the text into an <a> element seamlessly
    function createLink(textNode, start, end, urlText) {
        const text = textNode.nodeValue;
        const beforeText = text.slice(0, start);
        const afterText = text.slice(end);

        const a = document.createElement('a');
        a.textContent = urlText;
        // Prepend https:// if protocol is missing
        a.href = /^(https?:\/\/)/i.test(urlText) ? urlText : `https://${urlText}`;
        a.style.color = '#0056b3';
        a.style.textDecoration = 'underline';
        a.style.cursor = 'pointer';

        // Use a DocumentFragment to safely replace the text node without disrupting layout
        const frag = document.createDocumentFragment();
        if (beforeText) frag.appendChild(document.createTextNode(beforeText));
        frag.appendChild(a);
        if (afterText) frag.appendChild(document.createTextNode(afterText));

        textNode.parentNode.replaceChild(frag, textNode);

        // Save reference so we can revert it later
        activeLinkData = {
            aElement: a,
            word: urlText
        };
    }
})();