Selection Context

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

Ten skrypt nie powinien być instalowany bezpośrednio. Jest to biblioteka dla innych skyptów do włączenia dyrektywą meta // @require https://update.greatest.deepsurf.us/scripts/528822/1737952/Selection%20Context.js

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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