// ==UserScript== // @name youdao-trans // @namespace http://182.61.43.228/ // @supportURL http://182.61.43.228/operation/mail.html // @version 0.23 // @description 简化有道翻译页面的工具 // @author UFO // @match http://fanyi.youdao.com/* // @match https://fanyi.youdao.com/* // @connect iot2ai.top // @connect ai2ufo.ltd // @connect 182.61.43.228 // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant unsafeWindow // @grant GM_addStyle // @grant GM_xmlhttpRequest // @downloadURL https://update.greasyfork.icu/scripts/429992/youdao-trans.user.js // @updateURL https://update.greasyfork.icu/scripts/429992/youdao-trans.meta.js // ==/UserScript== (function() { 'use strict'; // 普通样式切换 function toggleAll(selector, display) { var qa = document.querySelectorAll(selector); for (var i = 0; i < qa.length; i++) { qa[i].style.display = display; } } // 修复隐藏全屏广告无法滚动问题 function fixPopup() { var ele = document.querySelector('.close'); if (ele) { console.log('popup view closed!'); ele.click(); } if (document.body.style.position === 'fixed') { document.body.style.position = "static"; } } // 调整输入区域大小 function fixWidth() { var ele = document.querySelector('.translate-tab-container'); if (ele) { ele.style.width = 'auto'; ele.style.margin = '0 5px'; ele.style.marginTop = '0'; ele.style.paddingTop = '5px'; // 解除宽度限制,实现铺满 // web-frame-inner-content ele.parentElement.style.width = '100%'; // 解除高度限制,实现滚动 ele = document.querySelector('.web-frame-inner-container'); if (ele) { ele.style.height = 'auto'; // 去除宽度限制,导致有滚动时出现水平滚动 // web-frame-content-container ele.parentElement.style.width = 'auto'; // web-frame-container ele.parentElement.parentElement.style.height = 'auto'; ele.parentElement.parentElement.style.width = 'auto'; } } if (window.innerWidth < 980 && window.devicePixelRatio > 1) { ele = document.querySelector("meta[name=viewport]"); if (ele) { ele.content = 'width=980'; setTimeout(fixPopup, 1000); } } else if (window.devicePixelRatio > 1) { // 开始宽度可能很大,但是适配后会缩小 setTimeout(function() { if (window.innerWidth < 980) { ele = document.querySelector("meta[name=viewport]"); if (ele) { ele.content = 'width=980'; setTimeout(fixPopup, 1000); } } }, 1000); } } function fixButton(name, callback) { var ele = document.querySelector('.tab-header .tab-left .tab-item'); var newEle = document.createElement('div'); for (var i = 0; i < ele.attributes.length; i++) { newEle.setAttribute(ele.attributes[i].name, ele.attributes[i].value); } newEle.classList.remove('active'); newEle.innerText = name; newEle.onclick = callback; ele.parentElement.appendChild(newEle); } // 兼容输入框事件属性 function getVei(ele) { var vei = ele._vei; if (!vei) { var sa = Object.getOwnPropertySymbols(ele); for (var i = 0; i < sa.length; i++) { if (sa[i].toString().indexOf('_vei')) { vei = ele[sa[i]]; break; } } } return vei; } // 调整输入区域大小 function fixInput() { var input = document.querySelector('#inputOriginal'); if (!input) { setTimeout(fixInput, 1000); return; } fixPopup(); fixWidth(); fixEvent(); // 立即翻译按钮 fixButton('立即翻译', function() { var ele = document.querySelector('#js_fanyi_input'); getVei(ele).onInput({target: ele}); }); // 显示导航栏按钮 fixButton('显示导航', function() { var ele = document.querySelector('.header-outer-container'); if (ele.style.display === 'none') { ele.style.display = ''; toggleAll('.sidebar-container',''); this.innerText = '隐藏导航'; } else { ele.style.display = 'none'; toggleAll('.sidebar-container','none'); this.innerText = '显示导航'; } }); var parentEle = input.parentElement; // 手动操作区 var operations = document.querySelector('.tab-header'); toggleAll('.header-outer-container,.sidebar-container,.tab-header,.text-translate-top', 'none'); // 增加文章加载功能 var nvOut = document.createElement('div'); nvOut.style.marginTop = '5px'; nvOut.style.marginBottom = '5px'; var nvArr = []; nvArr.push(''); nvArr.push(''); nvArr.push(''); nvArr.push(''); nvArr.push(''); nvArr.push(''); nvArr.push('|'); nvArr.push(''); nvArr.push(''); nvArr.push(''); nvOut.innerHTML = nvArr.join(''); // 内容临时处理区域。后面是否换成iframe比较好? var dataEle = document.createElement('div'); dataEle.style.display = 'none'; // 事件处理 nvOut.onclick = function(e) { if (e.target.tagName !== 'BUTTON') { return; } var op = e.target.innerText; if (op === '显示选项' || op === '隐藏选项') { if (operations.style.display === 'none') { operations.style.display = ''; toggleAll('.text-translate-top',''); e.target.innerText = '隐藏选项'; } else { operations.style.display = 'none'; toggleAll('.text-translate-top','none'); e.target.innerText = '显示选项'; } } else if (op === '加载') { if (e.altKey) { // 读取文件 readNovel(nvOut, dataEle, !e.ctrlKey); } else { // 在线加载 loadNovel(nvOut, dataEle, !e.ctrlKey); } } else if (op === '上页') { prevBlock(!e.ctrlKey); } else if (op === '下页') { nextBlock(!e.ctrlKey); } else if (op === '分页大小') { var p = prompt('请输入分页大小(1~5000)或跳转索引(@索引)', blockSize.toString()); if (p) { if (p.search(/^\d{1,4}$/) !== -1) { blockSize = parseInt(p); if (blockSize > 5000 || blockSize === 0) { blockSize = 5000; } GM_setValue('block_size', blockSize); } else if (p.search(/^@\d+$/) !== -1) { rawOffset = parseInt(p.substring(1)); nextBlock(!e.ctrlKey); } } } else if (op === '前へ') { prevNovel(nvOut, dataEle, !e.ctrlKey); } else if (op === '次へ') { nextNovel(nvOut, dataEle, !e.ctrlKey); } else if (op === '目次') { listNovel(nvOut, dataEle, !e.ctrlKey); } }; parentEle.insertBefore(nvOut, input); parentEle.appendChild(dataEle); // 返回顶部按钮 showTop(); // 最后输入的记录 nvOut.querySelector('input').value = GM_getValue('input_url', ''); } // 显示顶部按钮 function showTop() { var topEle = document.querySelector('#_yt_top'); if (!topEle) { topEle = document.createElement('a'); topEle.id = '_yt_top'; topEle.style.marginLeft = '12px'; // 由于页面对滚动事件的处理,使用滚动属性回到顶部存在变动问题。这个暂时无法处理! // 同时,手机经常缩放页面,滚动属性无法适配缩放的比例,即滚动的顶部并不代表缩放的顶部 // 所以手机需要使用锚点跳转。手机首次跳转缩放比例会回到0,之后就能始终保持跳转到顶部 if (unsafeWindow.AiView) { topEle.href = 'javascript:AiView.setScrollY(0,false,null);'; } else if (navigator.appVersion.match(/Mobile|YaBrowser/)) { topEle.href = '#'; } else { topEle.href = 'javascript:document.body.scrollTop=document.documentElement.scrollTop=0;'; } document.querySelector('.targetAction .opt-left').appendChild(topEle); } if (rawText) { topEle.innerText = '顶部(' + rawOffset + '/' + rawText.length + ')'; } else { topEle.innerText = '顶部'; } } // 加载内容 function fillUrl(nvOut) { var urlEle = nvOut.querySelector('input'); var url = urlEle.value.trim(); if (!url) return null; if (url.indexOf('://') === -1) { url = url.replace(/\S*nv=/, ''); url = 'http://182.61.43.228/cgi-bin/intel/syosetu.html?nv=' + url; } return url; } function setUrl(nvOut, dataEle, url, load) { var urlEle = nvOut.querySelector('input'); urlEle.value = url.replace(/\S+\/intel\/syosetu.html\?nv=([^&]+)$/, '$1'); GM_setValue('input_url', urlEle.value); loadNovel(nvOut, dataEle, load); } function getNovel(url) { return url.match(/(\S+)(\?nv=)([^&]+)$/); } function trimNovel(str, isHtml, dataEle, load, offset) { if (isHtml) { var mat = str.match(/([\s\S]+)<\/body>/); if (mat) { // 需要的话,建议移除脚本。不过我的页面不可能存在脚本,所以先不处理 // 若自己添加了其它白名单,建议优化一下。当然,跨域也会保证页面安全,问题也不是很大 dataEle.innerHTML = mat[1]; } else { dataEle.innerHTML = '

内容が見つからない

'; } } else { // 为了进行片假名替换,同时过滤一些标签,处理实体符号,建议使用段落包裹 dataEle.innerHTML = '

' + str + '

'; } if (isReplaceKatakana) { runKanaToRomaji(dataEle); } str = dataEle.innerText.trim(); dataEle.innerHTML = ''; // 去除多余的空行 str = str.replace(/(\n)\s+(\n)/g, '$1$2'); // 跳转索引(@索引) // 这个索引是相对格式化的文本偏移,不是源文本! // 可以在顶部信息获得这个偏移 if (offset) { offset = parseInt(offset[1]); if (str.length > offset) { str = str.substring(offset); } else { str = ''; } } setRawText(str); nextBlock(load); } function loadNovel(nvOut, dataEle, load) { var url = fillUrl(nvOut); if (!url) return; if (url.search(/^file:/) === 0) { readNovel(nvOut, dataEle, load, url.match(/@(\d+)$/)); return; } if (url.search(/^content:/) === 0) { readContent(nvOut, dataEle, load, url); return; } GM_xmlhttpRequest({ url: url, onload: function(xhr) { trimNovel(xhr.response, true, dataEle, load); }, onerror: function(e) { alert(e.error || '无法加载'); } }); } function readNovel(nvOut, dataEle, load, offset) { var input = document.createElement('input'); input.type = 'file'; input.onchange = function() { var f = this; if (!f.files.length || (f.files[0].type !== 'text/html' && f.files[0].type !== 'text/plain')) { return; } var isHtml = f.files[0].type === 'text/html'; var r = new FileReader(); r.readAsText(f.files[0], 'utf-8'); r.onload = function() { trimNovel(this.result, isHtml, dataEle, load, offset); }; }; input.click(); } function readContent(nvOut, dataEle, load, url) { // 扩展协议。使用这个接口为插件上扩展个性化功能,例如自动下载和推送 var c = unsafeWindow.nvContent; if (!c || !(c.text || typeof(c) === 'function')) return; if (!c.text) { c = c.call(window, url); if (!c || !c.text) return; } trimNovel(c.text, c.isHtml, dataEle, c.load === false ? false : load, c.offset); } function prevNovel(nvOut, dataEle, load) { var url = fillUrl(nvOut); if (!url) return; var mat = getNovel(url); if (!mat) return; var nv = mat[3]; if (nv.charAt(nv.length - 1) !== '/') { return; } var na = nv.substring(0, nv.length - 1).split('/'); if (na.length === 1) { url = mat[1] + mat[2] + na.join('/') + '/1/'; setUrl(nvOut, dataEle, url, load); } else if (na.length === 2 && na[1].search(/^\d+$/) !== -1) { na[1] = parseInt(na[1]) - 1; if (na[1] <= 0) { alert('前面没有文章了'); return; } url = mat[1] + mat[2] + na.join('/') + '/'; setUrl(nvOut, dataEle, url, load); } } function nextNovel(nvOut, dataEle, load) { var url = fillUrl(nvOut); if (!url) return; var mat = getNovel(url); if (!mat) return; var nv = mat[3]; if (nv.charAt(nv.length - 1) !== '/') { return; } var na = nv.substring(0, nv.length - 1).split('/'); if (na.length === 1) { url = mat[1] + mat[2] + na.join('/') + '/1/'; setUrl(nvOut, dataEle, url, load); } else if (na.length === 2 && na[1].search(/^\d+$/) !== -1) { na[1] = parseInt(na[1]) + 1; url = mat[1] + mat[2] + na.join('/') + '/'; setUrl(nvOut, dataEle, url, load); } } function listNovel(nvOut, dataEle, load) { var url = fillUrl(nvOut); if (!url) return; var mat = getNovel(url); if (!mat) return; var nv = mat[3]; if (nv.charAt(nv.length - 1) !== '/') { return; } var na = nv.substring(0, nv.length - 1).split('/'); if (na.length === 2 && na[1].search(/^\d+$/) !== -1) { url = mat[1] + mat[2] + na[0] + '/'; setUrl(nvOut, dataEle, url, load); } } // 翻译内容 var isAutoTrans = GM_getValue('auto_trans', true); function translate(str, load, key) { var ele = document.querySelector('#js_fanyi_input'); ele.innerText = str; if (isAutoTrans && load) { getVei(ele).onInput({target: ele}); } } // 分页机制 var rawText; var rawOffset, rawPaging; var blockSize = GM_getValue('block_size', 5000); function setRawText(str) { rawText = str; rawOffset = 0; rawPaging = []; } function prevBlock(load) { if (!rawText || rawPaging.length < 2) { alert('前面没有更多内容了'); return; } // 当前的位置 rawPaging.pop(); // 上一个位置 rawOffset = rawPaging.pop(); nextBlock(load); } function nextBlock(load) { if (!rawText || rawOffset >= rawText.length) { alert('后面没有更多内容了'); return; } var remain = rawText.length - rawOffset; if (remain <= blockSize) { translate(rawOffset > 0 ? rawText.substring(rawOffset) : rawText, load, '_' + rawOffset + '_' + rawText.length); rawPaging.push(rawOffset); rawOffset = rawText.length; } else { var end = rawOffset + blockSize - 1; while(end > rawOffset && rawText.charAt(end) !== '\n') { end--; } if (end === rawOffset) { end = rawOffset + blockSize - 1; } end++; var str = rawText.substring(rawOffset, end); translate(str.trim(), load, '_' + rawOffset + '_' + end); rawPaging.push(rawOffset); rawOffset = end; } // 显示当前已读位置和总数 showTop(); } // 优化输入翻译 var isInputTrans = GM_getValue('input_trans', false); function fixEvent() { if (!isInputTrans) { var ele = document.querySelector('#js_fanyi_input'); ele.removeEventListener('input', getVei(ele).onInput); console.log('input event removed!'); } } // 元素会重置的,只能使用样式隐藏 GM_addStyle('.sticky-sidebar,.footer,.fixedBottomActionBar-border-box,.translate-domain-text,.banner,.pop-up-comp,.download_ch,.dict-website-footer,.document-upload-entrance-container,.top-banner-outer-container{display:none !important}.clearBtn{top: 0 !important; right: 0 !important; z-index: 1}'); fixInput(); // 移除日志事件 unsafeWindow._rlog.push = console.log; // 调整一下标题,方便识别 document.title = document.title.replace(/有道/g, 'syosetu'); // load前的一次请求没法拦截,因为这里慢于那部分代码 console.log('youdao is clear!'); // 片假名特殊优化,更多细节参考kana-to-romaji // 是否替换片假名。若不要替换,改为false var isReplaceKatakana = GM_getValue('katakana_flag', true); // 片假名 // ア -> ン行是清音(Seion),ガ ->バ行是浊音(Dakuon),パ行是半浊音(Half-Dakuon) // 拗音(Youon)组合的音节ャ、ュ、ョ是半角,但按全角替换,即キャ替换成kiya,而不是kya // 特殊音用到的ァ行是半角,也按全角替换,即ファ替换成fua,而不是fa // 移除长音符号ー // 移除促音符号ッ半角(为了统一,现在按全角替换) var katakana = ['ア', 'イ', 'ウ', 'エ', 'オ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'ヵ', 'ヶ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ッ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ヤ', 'ユ', 'ヨ', 'ャ', 'ュ', 'ョ', 'ワ', 'ヲ', 'ヴ', 'ヮ', 'ヰ', 'ヱ', 'ン', 'ヷ', 'ヺ', 'ヸ', 'ヹ', 'ー', 'ガ', 'ギ', 'グ', 'ゲ', 'ゴ', 'ザ', 'ジ', 'ズ', 'ゼ', 'ゾ', 'ダ', 'ヂ', 'ヅ', 'デ', 'ド', 'バ', 'ビ', 'ブ', 'ベ', 'ボ', 'パ', 'ピ', 'プ', 'ペ', 'ポ']; // 罗马音 var romaji = ['a', 'i', 'u', 'e', 'o','a', 'i', 'u', 'e', 'o', 'ka', 'ki', 'ku', 'ke', 'ko', 'ka', 'ke', 'sa', 'shi', 'su', 'se', 'so', 'ta', 'chi', 'tsu', 'te', 'to', 'tsu', 'ha', 'hi', 'fu', 'he', 'ho', 'na', 'ni', 'nu', 'ne', 'no', 'ma', 'mi', 'mu', 'me', 'mo', 'ra', 'ri', 'ru', 're', 'ro', 'ya', 'yu', 'yo','ya', 'yu', 'yo', 'wa', 'wo', 'vu', 'wa', 'wi', 'we', 'n', 'ba', 'bo', 'bi', 'be', '', 'ga', 'gi', 'gu', 'ge', 'go', 'za', 'ji', 'zu', 'ze', 'zo', 'da', 'di', 'du', 'de', 'do', 'ba', 'bi', 'bu', 'be', 'bo', 'pa', 'pi', 'pu', 'pe', 'po']; // 假名跟罗马音映射表 var katakanaMap = {}; for (var i in romaji) { katakanaMap[katakana[i]] = romaji[i]; } // 假名正则表达式 var katakanaReg = new RegExp('(' + katakana.join('|') + ')+', 'g'); // 片假名转罗马音 function katakanaToRomajiHTML(m) { var sa = []; // 原文上一行显示罗马音 sa.push('', m, ''); for (var i = 0; i < m.length; i++) { sa.push(katakanaMap[m.charAt(i)]); } sa.push(''); return sa.join(''); } function katakanaToRomajiText(m) { var sa = []; for (var i = 0; i < m.length; i++) { sa.push(katakanaMap[m.charAt(i)]); } return sa.join(''); } // 全局替换元素内容。若使用文本模式,则只替换纯文本内容,不会清除内嵌元素的事件。但是纯文本模式没有注音或者悬浮查看原文效果 function replaceNodeValue(pa, reg, fn) { // 替换节点的内容,例如属性列表 for (var i = 0; i < pa.length; i++) { var str = pa[i].nodeValue; if (str.search(reg) !== -1) { pa[i].nodeValue = str.replace(reg, fn); } } } function replaceAttr(pa, reg, fn) { // 替换元素的属性列表(包括其子节点的) for (var i = 0; i < pa.length; i++) { if (pa[i].childNodes.length > 0) { replaceAttr(pa[i].childNodes, reg, fn); replaceNodeValue(pa[i].attributes, reg, fn); } else { if (pa[i].nodeName !== '#text') { replaceNodeValue(pa[i].attributes, reg, fn); } } } } function replaceHTML(pa, reg, fn, notMixed) { // 富文本替换,可以增加特殊表现,但会清除子元素事件 // 替换前,建议先替换属性,否则可能破坏标签结构 for (var i = 0; i < pa.length; i++) { var str = pa[i].innerHTML; if (str.search(reg) !== -1) { // 多次替换可能重复操作同一个元素,这时需要排除这部分元素 if (notMixed) { if (str.indexOf('__ka__') !== -1) { continue; } } pa[i].innerHTML = str.replace(reg, fn); } } } function replaceText(pa, reg, fn) { // 纯文本替换,只替换文本节点的内容,不会影响元素事件 for (var i = 0; i < pa.length; i++) { if (pa[i].childNodes.length > 0) { replaceText(pa[i].childNodes, reg, fn); } else { if (pa[i].nodeName === '#text') { var str = pa[i].nodeValue; if (str.search(reg) !== -1) { pa[i].nodeValue = str.replace(reg, fn); } } } } } // 将指定元素的假名转罗马音 // useText:false表示富文本模式,true表示纯文本模式; // notMixed:false表示不做混合检测,true表示做混合检测。内嵌标签很可能被前面操作替换,这时就不应该再替换 function kanaToRomaji(element, selector, useText, notMixed) { var qa = element.querySelectorAll(selector); if (qa.length === 0) return; replaceAttr(qa, katakanaReg, katakanaToRomajiText); if (useText) { replaceText(qa, katakanaReg, katakanaToRomajiText); } else { replaceHTML(qa, katakanaReg, katakanaToRomajiHTML, notMixed ? '__ka__' : null); } } function runKanaToRomaji(ele) { console.log('kana to romaji is starting!'); // 替换段落内容 kanaToRomaji(ele, 'p', false, false); // 替换链接内容 kanaToRomaji(ele, 'a', false, true); // 其它标签内容 kanaToRomaji(ele, '.chapter_title', false, true); kanaToRomaji(ele, '#novel_ex', false, true); kanaToRomaji(ele, '.ex', false, true); } // 是否替换假名的菜单按钮,点击可以切换 var katakanaMenuId; function addReplaceKatakanaMenu() { katakanaMenuId = GM_registerMenuCommand('片假名(' + (isReplaceKatakana ? '替换' : '不变') + ')', onReplaceKatakanaMenu); } function onReplaceKatakanaMenu() { isReplaceKatakana = !isReplaceKatakana; GM_setValue('katakana_flag', isReplaceKatakana); GM_unregisterMenuCommand(katakanaMenuId); addReplaceKatakanaMenu(); } addReplaceKatakanaMenu(); // 是否输入自动翻译的菜单按钮,点击可以切换 var inputTransMenuId; function addInputTransMenu() { inputTransMenuId = GM_registerMenuCommand('输入翻译(' + (isInputTrans ? '是' : '否') + ')', onInputTransMenu); } function onInputTransMenu() { isInputTrans = !isInputTrans; GM_setValue('input_trans', isInputTrans); GM_unregisterMenuCommand(inputTransMenuId); addInputTransMenu(); var ele = document.querySelector('#js_fanyi_input'); if (isInputTrans) { ele.addEventListener('input', getVei(ele).onInput); } else { ele.removeEventListener('input', getVei(ele).onInput); } } addInputTransMenu(); // 是否加载后自动翻译的菜单按钮,点击可以切换 var autoTransMenuId; function addAutoTransMenu() { autoTransMenuId = GM_registerMenuCommand('自动翻译(' + (isAutoTrans ? '是' : '否') + ')', onAutoTransMenu); } function onAutoTransMenu() { isAutoTrans = !isAutoTrans; GM_setValue('auto_trans', isAutoTrans); GM_unregisterMenuCommand(autoTransMenuId); addAutoTransMenu(); } addAutoTransMenu(); })();