Selection Context

Get the selected text along with text before and after the selection

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/528822/1737952/Selection%20Context.js

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 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         Selection Context
// @namespace    http://tampermonkey.net/
// @version      0.3.2
// @description  Get the selected text along with text before and after the selection
// @author       RoCry
// @license MIT
// ==/UserScript==
const DEFAULT_CONTEXT_LENGTH = 500;
const MAX_CONTEXT_LENGTH = 8192;
const BLOCK_SELECTORS =
  "article, section, main, p, div, li, td, th, blockquote, pre";
function getSelectionRoot(range) {
  const container =
    range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
      ? range.commonAncestorContainer
      : range.commonAncestorContainer.parentElement;
  if (!container) return document.body;
  return container.closest(BLOCK_SELECTORS) || document.body;
}
function extractSelectedHTML(range) {
  try {
    const fragment = range.cloneContents();
    const container = document.createElement("div");
    container.appendChild(fragment);
    const parentElement =
      range.commonAncestorContainer.nodeType === Node.TEXT_NODE
        ? range.commonAncestorContainer.parentElement
        : range.commonAncestorContainer;
    if (parentElement && parentElement.nodeName !== "BODY") {
      const tagName = parentElement.nodeName.toLowerCase();
      return `<${tagName}>${container.innerHTML}</${tagName}>`;
    }
    return container.innerHTML;
  } catch (error) {
    console.error("Error extracting HTML from selection:", error);
    return null;
  }
}
function getTextNodesIn(node) {
  const textNodes = [];
  const walker = document.createTreeWalker(
    node,
    NodeFilter.SHOW_TEXT,
    null,
    false,
  );
  let currentNode = walker.nextNode();
  while (currentNode) {
    textNodes.push(currentNode);
    currentNode = walker.nextNode();
  }
  return textNodes;
}
/**
 * Gets the selected text along with text before and after the selection
 * @param {number} tryContextLength - Desired length of context to try to collect (before + after selection)
 * @returns {Object|null} Object containing selectedHTML, selectedText, textBefore, textAfter, paragraphText
 */
function GetSelectionContext(tryContextLength = DEFAULT_CONTEXT_LENGTH) {
  const selection = window.getSelection();
  if (!selection || selection.rangeCount === 0) return null;
  const selectedText = selection.toString().trim();
  if (!selectedText) return null;
  const range = selection.getRangeAt(0);
  const actualContextLength = Math.min(tryContextLength, MAX_CONTEXT_LENGTH);
  const halfContextLength = Math.floor(actualContextLength / 2);
  const root = getSelectionRoot(range) || document.body;
  const allTextNodes = getTextNodesIn(root);
  const startNode = range.startContainer;
  const endNode = range.endContainer;
  const startIndex = allTextNodes.indexOf(startNode);
  const endIndex = allTextNodes.indexOf(endNode);
  if (startIndex === -1 || endIndex === -1) {
    console.warn(
      "Selection nodes not found in text node list. Returning minimal context.",
    );
    return {
      selectedHTML: extractSelectedHTML(range) || selectedText,
      selectedText,
      textBefore: "",
      textAfter: "",
      paragraphText: selectedText,
    };
  }
  let textBefore = "";
  let textAfter = "";
  let currentLength = 0;
  if (startNode.nodeType === Node.TEXT_NODE) {
    textBefore = startNode.textContent.substring(0, range.startOffset);
    currentLength = textBefore.length;
  }
  let beforeIndex = startIndex - 1;
  while (beforeIndex >= 0 && currentLength < halfContextLength) {
    const nodeText = allTextNodes[beforeIndex].textContent || "";
    textBefore = `${nodeText}\n${textBefore}`;
    currentLength += nodeText.length;
    beforeIndex -= 1;
  }
  if (beforeIndex >= 0) {
    textBefore = `...\n${textBefore}`;
  }
  currentLength = 0;
  if (endNode.nodeType === Node.TEXT_NODE) {
    textAfter = endNode.textContent.substring(range.endOffset);
    currentLength = textAfter.length;
  }
  let afterIndex = endIndex + 1;
  while (
    afterIndex < allTextNodes.length &&
    currentLength < halfContextLength
  ) {
    const nodeText = allTextNodes[afterIndex].textContent || "";
    textAfter += `${nodeText}\n`;
    currentLength += nodeText.length;
    afterIndex += 1;
  }
  if (afterIndex < allTextNodes.length) {
    textAfter += "\n...";
  }
  textBefore = textBefore.trim();
  textAfter = textAfter.trim();
  const paragraphText = `${textBefore} ${selectedText} ${textAfter}`.trim();
  return {
    selectedHTML: extractSelectedHTML(range) || selectedText,
    selectedText,
    textBefore,
    textAfter,
    paragraphText,
  };
}
const TextExplainerUI = (() => {
  const IDS = {
    popup: "explainer-popup",
    overlay: "explainer-overlay",
    content: "explainer-content",
    loading: "explainer-loading",
    error: "explainer-error",
    floatingButton: "explainer-floating-button",
  };
  const POPUP_WIDTH = 450;
  const POPUP_MAX_HEIGHT_RATIO = 0.8;
  const STYLE_TEXT = `#${IDS.popup}{position:absolute;width:${POPUP_WIDTH}px;max-width:90vw;max-height:80vh;padding:16px 16px 14px;z-index:2147483647;overflow:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;background:rgba(255,255,255,0.96);border:1px solid rgba(15,23,42,0.12);border-radius:10px;box-shadow:0 12px 28px rgba(15,23,42,0.12);color:#0f172a;font-family:inherit;font-size:0.98rem;line-height:1.65;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);transition:opacity 0.2s ease,transform 0.2s ease;}#${IDS.popup}.dark-theme{background:rgba(24,24,28,0.96);border:1px solid rgba(255,255,255,0.12);box-shadow:0 16px 34px rgba(0,0,0,0.5);color:#e5e7eb;}#${IDS.overlay}{position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483646;background:transparent;}@supports (-webkit-touch-callout: none){#${IDS.popup}{backdrop-filter:none;-webkit-backdrop-filter:none;}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeOut{from{opacity:1}to{opacity:0}}#${IDS.content}{font-family:inherit;font-size:0.98rem;line-height:1.65;}#${IDS.content} p{margin:0 0 10px;}#${IDS.content} ul,#${IDS.content} ol{margin:6px 0 10px 20px;padding:0;}#${IDS.content} li{margin:4px 0;}#${IDS.content} a{color:inherit;text-decoration:underline;text-decoration-color:rgba(15,23,42,0.35);text-decoration-thickness:2px;text-underline-offset:3px;}#${IDS.popup}.dark-theme #${IDS.content} a{text-decoration-color:rgba(229,231,235,0.5);}#${IDS.content} code{font-family:ui-monospace,"SFMono-Regular","Menlo",monospace;font-size:0.92em;background:rgba(15,23,42,0.08);padding:2px 4px;border-radius:4px;}#${IDS.popup}.dark-theme #${IDS.content} code{background:rgba(255,255,255,0.12);}#${IDS.loading}{text-align:center;padding:14px 0;display:flex;align-items:center;justify-content:center;}#${IDS.loading}:after{content:"";width:20px;height:20px;border:3px solid rgba(15,23,42,0.12);border-top:3px solid rgba(15,23,42,0.45);border-radius:50%;animation:spin 1s linear infinite;display:inline-block;}#${IDS.popup}.dark-theme #${IDS.loading}:after{border:3px solid rgba(255,255,255,0.18);border-top:3px solid rgba(255,255,255,0.55);}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}#${IDS.error}{color:#b42318;padding:8px 10px;border-radius:6px;margin-bottom:10px;font-size:0.9rem;display:none;background:rgba(180,35,24,0.08);}#${IDS.popup}.dark-theme #${IDS.error}{background:rgba(220,80,80,0.18);color:#ffb4b4;}@media (prefers-color-scheme: dark){#${IDS.popup}{background:rgba(24,24,28,0.96);color:#e5e7eb;}#${IDS.floatingButton}{background-color:rgba(33,150,243,0.9);}}@media (hover:none) and (pointer:coarse){#${IDS.popup}{width:95vw;max-height:90vh;padding:16px;font-size:1rem;}#${IDS.popup} p,#${IDS.popup} li{line-height:1.7;margin-bottom:12px;}#${IDS.popup} a{padding:8px 0;}}`;
  let stylesInjected = false;
  let currentPopup = null;
  function ensureStyles() {
    if (stylesInjected) return;
    const addStyle =
      typeof GM_addStyle === "function"
        ? GM_addStyle
        : (cssText) => {
            const style = document.createElement("style");
            style.textContent = cssText;
            document.head.appendChild(style);
            return style;
          };
    if (!document.head) {
      throw new Error("document.head is not available");
    }
    addStyle(STYLE_TEXT);
    stylesInjected = true;
  }
  function isTouchDevice() {
    return (
      "ontouchstart" in window ||
      navigator.maxTouchPoints > 0 ||
      navigator.msMaxTouchPoints > 0
    );
  }
  function parseRgb(color) {
    const match = color.match(
      /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/,
    );
    if (!match) return null;
    return {
      r: Number(match[1]),
      g: Number(match[2]),
      b: Number(match[3]),
    };
  }
  function luminance(color) {
    const rgb = parseRgb(color);
    if (!rgb) return 128;
    return 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
  }
  function isPageDarkMode() {
    const bodyStyle = window.getComputedStyle(document.body);
    const htmlStyle = window.getComputedStyle(document.documentElement);
    const bodyBg = bodyStyle.backgroundColor;
    const htmlBg = htmlStyle.backgroundColor;
    const threshold = 128;
    const prefersDark = window.matchMedia(
      "(prefers-color-scheme: dark)",
    ).matches;
    if (luminance(bodyBg) < threshold) return true;
    if (bodyBg === "rgba(0, 0, 0, 0)" && luminance(htmlBg) < threshold)
      return true;
    if (bodyBg === "rgba(0, 0, 0, 0)" && htmlBg === "rgba(0, 0, 0, 0)")
      return prefersDark;
    return false;
  }
  function calculatePopupPosition() {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) return null;
    const range = selection.getRangeAt(0);
    const selectionRect = range.getBoundingClientRect();
    const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
    const scrollTop = window.scrollY || document.documentElement.scrollTop;
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;
    const popupHeight = Math.min(500, viewportHeight * POPUP_MAX_HEIGHT_RATIO);
    const margin = 20;
    const position = {};
    if (selectionRect.bottom + margin + popupHeight <= viewportHeight) {
      position.top = selectionRect.bottom + scrollTop + margin;
      position.left = Math.min(
        Math.max(
          10 + scrollLeft,
          selectionRect.left +
            scrollLeft +
            selectionRect.width / 2 -
            POPUP_WIDTH / 2,
        ),
        viewportWidth + scrollLeft - POPUP_WIDTH - 10,
      );
      position.placement = "below";
      return position;
    }
    if (selectionRect.top - margin - popupHeight >= 0) {
      position.top = selectionRect.top + scrollTop - margin - popupHeight;
      position.left = Math.min(
        Math.max(
          10 + scrollLeft,
          selectionRect.left +
            scrollLeft +
            selectionRect.width / 2 -
            POPUP_WIDTH / 2,
        ),
        viewportWidth + scrollLeft - POPUP_WIDTH - 10,
      );
      position.placement = "above";
      return position;
    }
    if (selectionRect.right + margin + POPUP_WIDTH <= viewportWidth) {
      position.top = Math.max(
        10 + scrollTop,
        Math.min(
          selectionRect.top + scrollTop,
          viewportHeight + scrollTop - popupHeight - 10,
        ),
      );
      position.left = selectionRect.right + scrollLeft + margin;
      position.placement = "right";
      return position;
    }
    if (selectionRect.left - margin - POPUP_WIDTH >= 0) {
      position.top = Math.max(
        10 + scrollTop,
        Math.min(
          selectionRect.top + scrollTop,
          viewportHeight + scrollTop - popupHeight - 10,
        ),
      );
      position.left = selectionRect.left + scrollLeft - margin - POPUP_WIDTH;
      position.placement = "left";
      return position;
    }
    position.top = Math.max(
      10 + scrollTop,
      Math.min(
        selectionRect.top + selectionRect.height + scrollTop + margin,
        viewportHeight / 2 + scrollTop - popupHeight / 2,
      ),
    );
    position.left = Math.max(
      10 + scrollLeft,
      Math.min(
        selectionRect.left +
          selectionRect.width / 2 +
          scrollLeft -
          POPUP_WIDTH / 2,
        viewportWidth + scrollLeft - POPUP_WIDTH - 10,
      ),
    );
    position.placement = "center";
    return position;
  }
  function openPopup({ isTouch, isDark }) {
    ensureStyles();
    closePopup();
    const popup = document.createElement("div");
    popup.id = IDS.popup;
    if (isDark) popup.classList.add("dark-theme");
    popup.innerHTML = `
      <div id="${IDS.error}"></div>
      <div id="${IDS.loading}"></div>
      <div id="${IDS.content}"></div>
    `;
    if (!document.body) {
      throw new Error("document.body is not available");
    }
    document.body.appendChild(popup);
    if (isTouch) {
      popup.style.position = "fixed";
      popup.style.top = "50%";
      popup.style.left = "50%";
      popup.style.transform = "translate(-50%, -50%)";
      popup.style.width = "90vw";
      popup.style.maxHeight = "85vh";
    } else {
      const position = calculatePopupPosition();
      if (position) {
        popup.style.transform = "none";
        if (position.top !== undefined) popup.style.top = `${position.top}px`;
        if (position.left !== undefined)
          popup.style.left = `${position.left}px`;
      } else {
        popup.style.top = "50%";
        popup.style.left = "50%";
        popup.style.transform = "translate(-50%, -50%)";
      }
    }
    popup.style.animation = "fadeIn 0.3s ease";
    const popupState = {
      popup,
      contentEl: popup.querySelector(`#${IDS.content}`),
      loadingEl: popup.querySelector(`#${IDS.loading}`),
      errorEl: popup.querySelector(`#${IDS.error}`),
      overlay: null,
      cleanup: [],
    };
    function closeOnEsc(event) {
      if (event.key === "Escape") {
        closePopup();
      }
    }
    document.addEventListener("keydown", closeOnEsc);
    popupState.cleanup.push(() =>
      document.removeEventListener("keydown", closeOnEsc),
    );
    if (isTouch) {
      const overlay = document.createElement("div");
      overlay.id = IDS.overlay;
      popupState.overlay = overlay;
      document.body.appendChild(overlay);
      let touchStarted = false;
      let startX = 0;
      let startY = 0;
      const moveThreshold = 30;
      function onOverlayTouchStart(event) {
        touchStarted = true;
        startX = event.touches[0].clientX;
        startY = event.touches[0].clientY;
      }
      function onOverlayTouchEnd(event) {
        if (!touchStarted) return;
        const touch = event.changedTouches[0];
        const moveX = Math.abs(touch.clientX - startX);
        const moveY = Math.abs(touch.clientY - startY);
        if (moveX < moveThreshold && moveY < moveThreshold) {
          closePopup();
        }
        touchStarted = false;
      }
      function stopPropagation(event) {
        event.stopPropagation();
      }
      overlay.addEventListener("touchstart", onOverlayTouchStart, {
        passive: true,
      });
      overlay.addEventListener("touchmove", () => {}, { passive: true });
      overlay.addEventListener("touchend", onOverlayTouchEnd, {
        passive: true,
      });
      popup.addEventListener("touchstart", stopPropagation, { passive: false });
      popupState.cleanup.push(() =>
        overlay.removeEventListener("touchstart", onOverlayTouchStart),
      );
      popupState.cleanup.push(() =>
        overlay.removeEventListener("touchend", onOverlayTouchEnd),
      );
      popupState.cleanup.push(() =>
        popup.removeEventListener("touchstart", stopPropagation),
      );
    } else {
      function onOutsideClick(event) {
        if (popup.contains(event.target)) return;
        closePopup();
      }
      document.addEventListener("click", onOutsideClick);
      popupState.cleanup.push(() =>
        document.removeEventListener("click", onOutsideClick),
      );
    }
    currentPopup = popupState;
    return popupState;
  }
  function closePopup() {
    if (!currentPopup) return;
    const popup = currentPopup.popup;
    popup.style.animation = "fadeOut 0.2s ease";
    const { overlay, cleanup } = currentPopup;
    const remove = () => {
      cleanup.forEach((fn) => fn());
      if (overlay) overlay.remove();
      popup.remove();
      currentPopup = null;
    };
    setTimeout(remove, 200);
  }
  function setLoading(popupState, isVisible) {
    if (!popupState || !popupState.loadingEl) return;
    popupState.loadingEl.style.display = isVisible ? "flex" : "none";
  }
  function showError(popupState, message) {
    if (!popupState || !popupState.errorEl) return;
    popupState.errorEl.textContent = message;
    popupState.errorEl.style.display = "block";
    setLoading(popupState, false);
  }
  function updateContent(popupState, text) {
    if (!popupState || !popupState.contentEl) return;
    if (!text) return;
    let content = text.trim();
    if (!content) return;
    try {
      if (content.startsWith("```")) {
        if (content.endsWith("```")) {
          content = content.split("\n").slice(1, -1).join("\n");
        } else {
          content = content.split("\n").slice(1).join("\n");
        }
      }
      if (!content.startsWith("<")) {
        content = `<p>${content.replace(/\n/g, "<br>")}</p>`;
      }
      popupState.contentEl.innerHTML = content;
    } catch (error) {
      popupState.contentEl.innerHTML = `<p>${content.replace(/\n/g, "<br>")}</p>`;
    }
  }
  function createFloatingButton({ size, onTrigger, label }) {
    const button = document.createElement("div");
    button.id = IDS.floatingButton;
    let buttonSize = "50px";
    if (size === "small") buttonSize = "40px";
    if (size === "large") buttonSize = "60px";
    button.style.cssText = `
      width: ${buttonSize};
      height: ${buttonSize};
      border-radius: 50%;
      background-color: rgba(33, 150, 243, 0.8);
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      position: fixed;
      z-index: 9999;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
      cursor: pointer;
      font-weight: bold;
      font-size: ${parseInt(buttonSize, 10) * 0.4}px;
      opacity: 0;
      transition: opacity 0.3s ease, transform 0.2s ease;
      pointer-events: none;
      touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
    `;
    button.setAttribute("aria-label", "Explain selection");
    button.innerHTML = label || "TE";
    if (!document.body) {
      throw new Error("document.body is not available");
    }
    document.body.appendChild(button);
    function handleButtonAction(event) {
      event.preventDefault();
      event.stopPropagation();
      if (typeof onTrigger === "function") {
        onTrigger(event);
      }
    }
    button.addEventListener("click", handleButtonAction);
    button.addEventListener(
      "touchstart",
      (event) => {
        event.preventDefault();
        event.stopPropagation();
        button.style.transform = "scale(0.95)";
      },
      { passive: false },
    );
    button.addEventListener(
      "touchend",
      (event) => {
        event.preventDefault();
        event.stopPropagation();
        button.style.transform = "scale(1)";
        handleButtonAction(event);
      },
      { passive: false },
    );
    button.addEventListener("mousedown", (event) => {
      event.preventDefault();
      event.stopPropagation();
    });
    return button;
  }
  function showFloatingButton(button) {
    if (!button) return false;
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) {
      hideFloatingButton(button);
      return false;
    }
    const range = selection.getRangeAt(0);
    const rect = range.getBoundingClientRect();
    const buttonSize = parseInt(button.style.width, 10);
    const margin = 10;
    let top = rect.bottom + margin;
    let left = rect.left + rect.width / 2 - buttonSize / 2;
    if (top + buttonSize > window.innerHeight) {
      top = rect.top - buttonSize - margin;
    }
    left = Math.max(10, Math.min(left, window.innerWidth - buttonSize - 10));
    button.style.top = `${top}px`;
    button.style.left = `${left}px`;
    button.style.opacity = "1";
    button.style.pointerEvents = "auto";
    return true;
  }
  function hideFloatingButton(button) {
    if (!button) return;
    button.style.opacity = "0";
    button.style.pointerEvents = "none";
  }
  return {
    ensureStyles,
    isTouchDevice,
    isPageDarkMode,
    openPopup,
    closePopup,
    setLoading,
    showError,
    updateContent,
    createFloatingButton,
    showFloatingButton,
    hideFloatingButton,
  };
})();
window.GetSelectionContext = GetSelectionContext;
window.TextExplainerUI = TextExplainerUI;
if (typeof module !== "undefined" && module.exports) {
  module.exports = { GetSelectionContext, TextExplainerUI };
} else {
  window.SelectionUtils = window.SelectionUtils || {};
  window.SelectionUtils.GetSelectionContext = GetSelectionContext;
}