A userscript that converts text in the browser.
// ==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();
})();