Text Converter

A userscript that converts text in the browser.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Text Converter
// @namespace    npm/vite-plugin-monkey
// @version      0.0.0
// @author       Andy Hsu
// @description  A userscript that converts text in the browser.
// @license      MIT
// @icon         https://cdn.jsdmirror.cn/gh/xhofe/xhofe/avatar/avatar.svg
// @match        *://*/*
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):(document.head||document.documentElement).appendChild(document.createElement("style")).append(t);})(e));};

  const converters = [];
  function registerConverter(converter) {
    converters.push(converter);
  }
  function getConverters() {
    return converters;
  }
  const unicodeDecode = {
    name: "Unicode Decode",
    convert(text) {
      return text.replace(
        /\\u([0-9a-fA-F]{4})/g,
        (_, hex) => String.fromCharCode(parseInt(hex, 16))
      );
    }
  };
  function getSelectedText() {
    const selection = window.getSelection();
    return selection?.toString() ?? "";
  }
  function getTextNodesInRange(range) {
    const result = [];
    if (range.startContainer === range.endContainer && range.startContainer.nodeType === Node.TEXT_NODE) {
      return [
        {
          node: range.startContainer,
          startOffset: range.startOffset,
          endOffset: range.endOffset
        }
      ];
    }
    const walker = document.createTreeWalker(
      range.commonAncestorContainer,
      NodeFilter.SHOW_TEXT
    );
    let current;
    while (current = walker.nextNode()) {
      if (!range.intersectsNode(current)) continue;
      let startOffset = 0;
      let endOffset = current.textContent?.length ?? 0;
      if (current === range.startContainer) {
        startOffset = range.startOffset;
      }
      if (current === range.endContainer) {
        endOffset = range.endOffset;
      }
      if (startOffset < endOffset) {
        result.push({ node: current, startOffset, endOffset });
      }
    }
    return result;
  }
  function replaceSelectedText(convert) {
    const activeEl = document.activeElement;
    if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement) {
      const start = activeEl.selectionStart ?? 0;
      const end = activeEl.selectionEnd ?? 0;
      const value = activeEl.value;
      const converted = convert(value.slice(start, end));
      activeEl.value = value.slice(0, start) + converted + value.slice(end);
      activeEl.selectionStart = start;
      activeEl.selectionEnd = start + converted.length;
      activeEl.dispatchEvent(new Event("input", { bubbles: true }));
      return;
    }
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) return;
    const range = selection.getRangeAt(0);
    const textNodes = getTextNodesInRange(range);
    for (const { node, startOffset, endOffset } of textNodes) {
      const original = node.textContent ?? "";
      const selected = original.slice(startOffset, endOffset);
      const converted = convert(selected);
      node.textContent = original.slice(0, startOffset) + converted + original.slice(endOffset);
    }
    selection.removeAllRanges();
  }
  const styleCss = ".tc-trigger{position:absolute;z-index:2147483647;width:24px;height:24px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;box-shadow:0 2px 8px #00000026;cursor:pointer;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:13px;font-weight:600;color:#555;line-height:1;-webkit-user-select:none;user-select:none;transition:background .15s,color .15s}.tc-trigger:hover{background:#f0f5ff;color:#1a73e8}.tc-menu{position:fixed;z-index:2147483647;min-width:160px;padding:4px 0;background:#fff;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 4px 12px #00000026;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:13px;color:#333;-webkit-user-select:none;user-select:none}.tc-menu-header{padding:6px 12px;font-size:11px;color:#999;border-bottom:1px solid #f0f0f0}.tc-menu-item{padding:6px 12px;cursor:pointer;white-space:nowrap}.tc-menu-item:hover{background:#f0f5ff;color:#1a73e8}";
  importCSS(styleCss);
  let menuEl = null;
  let triggerEl = null;
  function hideTrigger() {
    if (triggerEl) {
      triggerEl.remove();
      triggerEl = null;
    }
  }
  function showTrigger() {
    hideTrigger();
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) return;
    const range = selection.getRangeAt(0);
    const rect = range.getBoundingClientRect();
    if (rect.width === 0 && rect.height === 0) return;
    triggerEl = document.createElement("div");
    triggerEl.className = "tc-trigger";
    triggerEl.textContent = "T";
    triggerEl.addEventListener("mousedown", (e) => {
      e.preventDefault();
      e.stopPropagation();
      const btnRect = triggerEl.getBoundingClientRect();
      showMenu(btnRect.left, btnRect.bottom + 4);
    });
    document.body.appendChild(triggerEl);
    let left = rect.right + window.scrollX + 4;
    let top = rect.top + window.scrollY - 28;
    triggerEl.style.left = `${left}px`;
    triggerEl.style.top = `${top}px`;
    requestAnimationFrame(() => {
      if (!triggerEl) return;
      const btnRect = triggerEl.getBoundingClientRect();
      if (btnRect.right > window.innerWidth) {
        left = rect.left + window.scrollX - btnRect.width - 4;
        triggerEl.style.left = `${left}px`;
      }
      if (btnRect.top < 0) {
        top = rect.bottom + window.scrollY + 4;
        triggerEl.style.top = `${top}px`;
      }
    });
  }
  function hideMenu() {
    if (menuEl) {
      menuEl.remove();
      menuEl = null;
    }
  }
  function hideAll() {
    hideMenu();
    hideTrigger();
  }
  function showMenu(x, y) {
    hideMenu();
    const converters2 = getConverters();
    if (converters2.length === 0) return;
    menuEl = document.createElement("div");
    menuEl.className = "tc-menu";
    document.body.appendChild(menuEl);
    const header = document.createElement("div");
    header.className = "tc-menu-header";
    header.textContent = "Text Converter";
    menuEl.appendChild(header);
    for (const converter of converters2) {
      const item = document.createElement("div");
      item.className = "tc-menu-item";
      item.textContent = converter.name;
      item.addEventListener("click", (e) => {
        e.stopPropagation();
        replaceSelectedText(converter.convert);
        hideAll();
      });
      menuEl.appendChild(item);
    }
    menuEl.style.left = `${x}px`;
    menuEl.style.top = `${y}px`;
    requestAnimationFrame(() => {
      if (!menuEl) return;
      const rect = menuEl.getBoundingClientRect();
      if (rect.right > window.innerWidth) {
        menuEl.style.left = `${window.innerWidth - rect.width - 4}px`;
      }
      if (rect.bottom > window.innerHeight) {
        menuEl.style.top = `${window.innerHeight - rect.height - 4}px`;
      }
    });
  }
  function onKeydown(e) {
    if (e.key === "Escape") {
      hideAll();
      return;
    }
    if ((e.metaKey || e.ctrlKey) && e.altKey && e.code === "KeyC") {
      const selectedText = getSelectedText();
      if (!selectedText) return;
      e.preventDefault();
      const selection = window.getSelection();
      if (!selection || selection.rangeCount === 0) return;
      const rect = selection.getRangeAt(0).getBoundingClientRect();
      showMenu(rect.left, rect.bottom + 4);
    }
  }
  function initContextMenu() {
    document.addEventListener("mouseup", () => {
      setTimeout(() => {
        const text = getSelectedText();
        if (text) {
          showTrigger();
        } else {
          hideTrigger();
        }
      }, 10);
    });
    document.addEventListener("mousedown", (e) => {
      if (menuEl && !menuEl.contains(e.target)) {
        hideMenu();
      }
      if (triggerEl && !triggerEl.contains(e.target) && !(menuEl && menuEl.contains(e.target))) {
        hideTrigger();
      }
    });
    document.addEventListener("keydown", onKeydown);
  }
  registerConverter(unicodeDecode);
  initContextMenu();

})();