Ctrl-Hover Linkifier

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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