|
// ==UserScript== |
|
// @name OpenCC 繁简转换器 |
|
// @name:en OpenCC Chinese Converter |
|
// @namespace https://gist.github.com/Backsoon0/6d9ea3def62b61e5796f10efde6528d1 |
|
// @version 1.1 |
|
// @description 多向繁简转换工具:支持繁→简 / 简→繁 / 短语转换,可通过菜单随时切换,偏好自动保存。基于 opencc-js 1.3.1。 |
|
// @description:en Multi-directional Chinese converter: Traditional↔Simplified with phrase support. Switch modes via menu, preferences auto-saved. Powered by opencc-js 1.3.1. |
|
// @author Backsoon0 |
|
// @license MIT |
|
// @match *://*/* |
|
// @grant GM_registerMenuCommand |
|
// @grant GM_getValue |
|
// @grant GM_setValue |
|
// @grant GM_addStyle |
|
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/full.js |
|
// @downloadURL https://gist.github.com/Backsoon0/6d9ea3def62b61e5796f10efde6528d1/raw/OpenCC%20%E7%B9%81%E7%AE%80%E8%BD%AC%E6%8D%A2%E5%99%A8.user.js |
|
// @updateURL https://gist.github.com/Backsoon0/6d9ea3def62b61e5796f10efde6528d1/raw/OpenCC%20%E7%B9%81%E7%AE%80%E8%BD%AC%E6%8D%A2%E5%99%A8.user.js |
|
// @run-at document-idle |
|
// ==/UserScript== |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
GM_addStyle('#opencc-overlay{all:initial;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:2147483647;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}#opencc-dialog{background:#fff;border-radius:12px;padding:24px;min-width:280px;box-shadow:0 8px 32px rgba(0,0,0,0.2);color:#333}.opencc-title{font-size:18px;font-weight:600;margin-bottom:16px;text-align:center;user-select:none}.opencc-option{padding:10px 16px;margin:4px 0;border-radius:8px;cursor:pointer;font-size:15px;color:#555;user-select:none;transition:background 0.15s}.opencc-option:hover{background:#f0f4ff}.opencc-option.opencc-active{background:#e8f0fe;color:#1a73e8;font-weight:600}.opencc-close{display:block;margin:16px auto 0;padding:8px 32px;border:1px solid #ddd;border-radius:6px;background:#f8f8f8;cursor:pointer;font-size:14px;color:#666}.opencc-close:hover{background:#eee}'); |
|
|
|
// 检查 OpenCC 是否成功加载 |
|
if (typeof OpenCC === 'undefined') { |
|
console.error('OpenCC-js 加载失败。'); |
|
return; |
|
} |
|
|
|
// 支持的语言对 |
|
const langOptions = [ |
|
{ label: '繁 (台湾)→简', from: 'tw', to: 'cn' }, |
|
{ label: '繁 (香港)→简', from: 'hk', to: 'cn' }, |
|
{ label: '简→繁 (台湾)', from: 'cn', to: 'tw' }, |
|
{ label: '简→繁 (香港)', from: 'cn', to: 'hk' }, |
|
{ label: '繁 (台湾+短语)→简', from: 'twp', to: 'cn' }, |
|
]; |
|
|
|
let currentFrom, currentTo, converter; |
|
function initConverter(from, to) { |
|
currentFrom = from; |
|
currentTo = to; |
|
converter = OpenCC.Converter({ from, to }); |
|
} |
|
initConverter(GM_getValue('opencc_from', 'tw'), GM_getValue('opencc_to', 'cn')); |
|
|
|
// 定义需要忽略的 HTML 标签,防止破坏网页原有功能或修改代码 |
|
const ignoreTags = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'CODE', 'PRE']); |
|
|
|
// 检查节点的父元素和祖先链是否需要跳过转换 |
|
function isNodeSkippable(node) { |
|
let el = node.nodeType === Node.TEXT_NODE ? node.parentNode : node; |
|
while (el && el.nodeType === Node.ELEMENT_NODE) { |
|
if (ignoreTags.has(el.tagName.toUpperCase())) return true; |
|
if (el.isContentEditable) return true; |
|
if (el.classList && el.classList.contains('ignore-opencc')) return true; |
|
if (el.hasAttribute('data-opencc-converted')) return true; |
|
el = el.parentNode; |
|
} |
|
return false; |
|
} |
|
|
|
// 核心转换函数:遍历 DOM 树 |
|
function convertNode(node) { |
|
if (node.nodeType === Node.TEXT_NODE) { |
|
if (isNodeSkippable(node)) return; |
|
const text = node.nodeValue; |
|
// 简单正则判断:如果包含非 ASCII 字符再进行转换,避免无意义的性能损耗 |
|
if (/[^\x00-\xff]/.test(text)) { |
|
node.nodeValue = converter(text); |
|
if (node.parentNode) node.parentNode.setAttribute('data-opencc-converted', ''); |
|
} |
|
} else if (node.nodeType === Node.ELEMENT_NODE) { |
|
if (ignoreTags.has(node.tagName.toUpperCase())) { |
|
return; // 跳过输入框、代码块等特定标签 |
|
} |
|
|
|
// 跳过 contenteditable 元素(富文本编辑器),避免干扰用户输入 |
|
if (node.isContentEditable) { |
|
return; |
|
} |
|
|
|
// 支持 opencc-js 的 ignore-opencc CSS 类,允许网页主动标记不转换的区域 |
|
if (node.classList && node.classList.contains('ignore-opencc')) { |
|
return; |
|
} |
|
|
|
// 转换 HTML 属性中的占位符和悬浮提示 |
|
if (node.hasAttribute('placeholder')) { |
|
node.setAttribute('placeholder', converter(node.getAttribute('placeholder'))); |
|
} |
|
if (node.hasAttribute('title')) { |
|
node.setAttribute('title', converter(node.getAttribute('title'))); |
|
} |
|
|
|
// 递归遍历所有子节点 |
|
node.childNodes.forEach(convertNode); |
|
} |
|
} |
|
|
|
// 1. 网页首次加载时,转换现有的内容和网页标题 |
|
convertNode(document.body); |
|
if (document.title) { |
|
document.title = converter(document.title); |
|
} |
|
|
|
// 3. 获取当前模式标签 |
|
function currentLabel() { |
|
const opt = langOptions.find(o => o.from === currentFrom && o.to === currentTo); |
|
return opt ? opt.label : currentFrom + '→' + currentTo; |
|
} |
|
|
|
// 4. 切换转换模式后全页重新转换 |
|
function applyLanguage(from, to) { |
|
GM_setValue('opencc_from', from); |
|
GM_setValue('opencc_to', to); |
|
initConverter(from, to); |
|
observer.disconnect(); |
|
document.querySelectorAll('[data-opencc-converted]').forEach(function(el) { |
|
el.removeAttribute('data-opencc-converted'); |
|
}); |
|
convertNode(document.body); |
|
if (document.title) document.title = converter(document.title); |
|
startObserving(); |
|
} |
|
|
|
// 5. 创建转换模式选择弹窗 |
|
function showSettingsDialog() { |
|
const existing = document.getElementById('opencc-overlay'); |
|
if (existing) existing.remove(); |
|
|
|
const overlay = document.createElement('div'); |
|
overlay.id = 'opencc-overlay'; |
|
overlay.addEventListener('click', function(e) { |
|
if (e.target === overlay) overlay.remove(); |
|
}); |
|
|
|
const dialog = document.createElement('div'); |
|
dialog.id = 'opencc-dialog'; |
|
dialog.innerHTML = '<div class="opencc-title">选择转换模式</div>'; |
|
|
|
langOptions.forEach(function(opt) { |
|
const isActive = currentFrom === opt.from && currentTo === opt.to; |
|
const item = document.createElement('div'); |
|
item.className = 'opencc-option' + (isActive ? ' opencc-active' : ''); |
|
item.textContent = (isActive ? '● ' : ' ') + opt.label; |
|
item.addEventListener('click', function() { |
|
applyLanguage(opt.from, opt.to); |
|
const opts = dialog.querySelectorAll('.opencc-option'); |
|
langOptions.forEach(function(o, i) { |
|
const act = currentFrom === o.from && currentTo === o.to; |
|
opts[i].className = 'opencc-option' + (act ? ' opencc-active' : ''); |
|
opts[i].textContent = (act ? '● ' : ' ') + o.label; |
|
}); |
|
}); |
|
dialog.appendChild(item); |
|
}); |
|
|
|
const closeBtn = document.createElement('button'); |
|
closeBtn.className = 'opencc-close'; |
|
closeBtn.textContent = '关闭'; |
|
closeBtn.addEventListener('click', function() { overlay.remove(); }); |
|
dialog.appendChild(closeBtn); |
|
|
|
overlay.appendChild(dialog); |
|
document.body.appendChild(overlay); |
|
} |
|
|
|
// 6. 注册原生菜单 |
|
GM_registerMenuCommand('切换转换模式...', showSettingsDialog); |
|
|
|
// 4. 使用 MutationObserver 监听动态加载的新内容 |
|
const observer = new MutationObserver((mutations) => { |
|
// 暂时断开监听,防止我们在修改 DOM 时触发死循环 |
|
observer.disconnect(); |
|
|
|
mutations.forEach((mutation) => { |
|
if (mutation.type === 'childList') { |
|
mutation.addedNodes.forEach((node) => { |
|
convertNode(node); |
|
}); |
|
} else if (mutation.type === 'characterData') { |
|
if (isNodeSkippable(mutation.target)) return; |
|
const text = mutation.target.nodeValue; |
|
if (/[^\x00-\xff]/.test(text)) { |
|
mutation.target.nodeValue = converter(text); |
|
if (mutation.target.parentNode) mutation.target.parentNode.setAttribute('data-opencc-converted', ''); |
|
} |
|
} |
|
}); |
|
|
|
// 转换完毕,恢复监听 |
|
startObserving(); |
|
}); |
|
|
|
function startObserving() { |
|
observer.observe(document.body, { |
|
childList: true, // 监听子节点的变动 |
|
subtree: true, // 监听所有后代节点 |
|
characterData: true // 监听文本节点内容的变动 |
|
}); |
|
} |
|
|
|
startObserving(); |
|
})(); |
OpenCC 繁简转换器
基于 网页繁体转简体 (OpenCC)(作者 mayunqing1230)的二改增强版。
与原版的对比
继承的原版特性
<script>、<style>、<input>、<textarea>、<code>、<pre>标签增强特性
ignore-openccCSS 类标记跳过区域支持的转换模式
使用方式
许可证
MIT,与原始项目保持一致。