// ==UserScript== // @name VNDB优先原文和中文化 // @namespace http://tampermonkey.net/ // @version 4.3.13 // @description 优先显示原文(title->value),以及中文化(mainMap[value]->value) // @author aotmd // @match https://vndb.org/* // @noframes // @license MIT // @run-at document-body // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @require https://greasyfork.org/scripts/445990-vndbtranslatorlib/code/VNDBTranslatorLib_.js?version=1298505 // @downloadURL none // ==/UserScript== /**-----------------------------业务逻辑部分[300行]----------------------------------*/ /** ---------------------------map处理---------------------------*/ let pathname = window.location.pathname; otherPageRules.forEach((item) => { //当regular是正则才执行 if (item.regular !== undefined && item.regular instanceof RegExp) { if (item.regular.test(pathname)) { //添加到主map,若存在重复项则覆盖主map Object.assign(mainMap, item.map); //添加特殊map Object.assign(specialMap, item.specialMap); //添加titleMap Object.assign(titleMap, item.titleMap); console.log(item.name + ',规则匹配:' + pathname + '->' + item.regular); } } }); /*object转Map, 正则new效率原因,先new出来*/ (function () { let tempMap = new Map(); let k = Object.getOwnPropertyNames(specialMap); for (let i = 0, len = k.length; i < len; i++) { try { tempMap.set(new RegExp(k[i]), specialMap[k[i]]); } catch (e) { console.log('"' + k[i] + '"不是一个合法正则表达式'); } } specialMap = tempMap; })(); /** ----------------------------END----------------------------*/ /** * 递归节点 * @param el 要处理的节点 * @param func 调用的函数 */ function 递归(el, func) { const nodeList = el.childNodes; /*先处理自己*/ 数据归一化(el,false); for (let i = 0; i < nodeList.length; i++) { const node = nodeList[i]; 数据归一化(node); } function 数据归一化(el,recursion=true) { if (el.nodeType === 1) { //为元素则递归 if (recursion){ 递归(el, func); } let attribute, value, flag = false; //为input且类型不为隐藏类型 if (el.nodeName === 'INPUT'&&el.type!=='hidden') { value = el.getAttribute('value'); attribute = 'value'; if (value == null || value.trim().length === 0) { value = el.getAttribute('placeholder'); attribute = 'placeholder'; } flag = true; } else if (el.nodeName === 'TEXTAREA') { value = el.getAttribute('placeholder'); attribute = 'placeholder'; flag = true; } else if (el.getAttribute('title')!==null&& el.title.length!==0) { /*过判断用*/ value = 'title用过判断value值'; attribute = 'title'; flag = true; } if (!flag) return; func(el, value, attribute); } else if (el.nodeType === 3) { //为文本节点则处理数据 func(el, el.nodeValue); } } } recordsList = []; let observerMap = new Map(); /** * dom修改事件,包括属性,内容,节点修改 * @param document 侦听对象 * @param func 执行函数,可选参数(records),表示更改的节点 */ function dom修改事件(document, func) { const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;//浏览器兼容 const config = {attributes: true, childList: true, characterData: true, subtree: true};//配置对象 const observer = new MutationObserver(function (records, itself) { recordsList.push(records); //进入后停止侦听 let flag = false; let obsArr = []; let selfIndex = -1; let doc = -1; //找到当前对象对应的value,和索引,以及k for (let key of observerMap.keys()) { let t = observerMap.get(key); for (let i = 0; i < t.length; i++) { if (itself === t[i][0]) { obsArr = t; selfIndex = i; doc = key; flag = true; break; } } if (flag) { break; } } if (selfIndex === -1) { console.error('没有找到obs的v'); return; } /*停止与之相同config的obs*/ for (let i = 0; i < obsArr.length; i++) { if (JSON.stringify(obsArr[i][1]) === JSON.stringify(obsArr[selfIndex][1])) { obsArr[i][0].disconnect() } } /*调用与之相同config的obs*/ try { for (let i = 0; i < obsArr.length; i++) { if (JSON.stringify(obsArr[i][1]) === JSON.stringify(obsArr[selfIndex][1])) { obsArr[i][2](records); } } } catch (e) { console.error('执行错误') } //启用与之相同config的obs for (let i = 0; i < obsArr.length; i++) { if (JSON.stringify(obsArr[i][1]) === JSON.stringify(obsArr[selfIndex][1])) { obsArr[i][0].observe(doc, obsArr[i][1]); } } }); if (observerMap.get(document) !== undefined) { let v = observerMap.get(document); v.push([observer, config, func]); observerMap.set(document, v); } else { observerMap.set(document, [[observer, config, func]]); } /*开始侦听*/ observer.observe(document, config); } (function () { /*立即执行*/ console.time('初始原文化 ,时间'); 递归(document.body, 原文化); console.timeEnd('初始原文化 ,时间'); console.time('初始字典翻译,时间'); 递归(document.body, 字典翻译); console.timeEnd('初始字典翻译,时间'); /*当body发生变化时执行*/ dom修改事件(document.body, (records) => { console.time('原文化 ,时间'); for (let i = 0, len = records.length; i < len; i++) { 递归(records[i].target, 原文化); } console.timeEnd('原文化 ,时间'); console.time('字典翻译,时间'); for (let i = 0, len = records.length; i < len; i++) { 递归(records[i].target, 字典翻译); } console.timeEnd('字典翻译,时间'); }); function 原文化(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); if (attribute === 'Text') { let title = node.parentNode.getAttribute("title"); if (内容判定(title, value)) { node.parentNode.setAttribute("title", value); node.nodeValue = title; // console.log(value+'->'+title) }else { //父父级内容交换,影响性能 //为搜索结果的大预览图进行原文化替换,#maincontent > form > div.mainbox.charbgrid > a let title = node.parentNode.parentNode?node.parentNode.parentNode.getAttribute("title"):null; if (内容判定(title, value)){ node.parentNode.parentNode.setAttribute("title", value); node.nodeValue = title; // console.log(value+'->'+title) } } } else { let title = node.getAttribute("title"); if (内容判定(title, value)) { //若为通常节点则正常设置属性 node.setAttribute('title', value); node.setAttribute(attribute, title); // console.log(value+'->'+title) } } /** * 显示的部分不为中文或日文,并且交换的部分为中文或日文 * 且不应只有空格和> (标签链接浏览器页vn匹配) * 并且value没有对应翻译值,title没有翻译过[通过查找' \t\n'标记判断] * @param title * @param value * @returns {boolean} */ function 内容判定(title, value) { return title != null && !/[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(value) && !/^[> -\\d]+$/.test(value) && /[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(title) && mainMap[value]===undefined && title.indexOf(' \t\n')===-1; } } function 字典翻译(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); /** titleMap翻译*/ if (attribute==='title'){ if(mainMap[value] === undefined &&node.nodeType === 1&&node.title ){ /*如果为节点类型,value没有翻译,且存在title*/ let flag=true; /*判断子节点文本,若文本为中文日文或匹配mainMap或与title相等,则执行后续操作*/ let nodelist=node.childNodes; for (let i=0;i'+newValue); /*替换后结束遍历*/ break; } } } } })(); /**-----------------------------开发用函数部分[350行]----------------------------------*/ /** 开启后通过控制台调用函数即可*/ let 开发者模式 = false; if (开发者模式) { /*exportMap已弃用*/ /** * 导出新的已被翻译的内容到控制台显示 *
即手动在网页上改文本,注意: *
先在要翻译的文本中间写入翻译后的内容 *
然后用del和backspace删除前后内容 *
开启编辑模式: *
document.body.contentEditable='true'; *
document.designMode='on'; */ exportMap = function () { let addMap = {}; 递归(document.body, 数据处理); /*导出到控制台处理*/ console.log(JSON.stringify(addMap)); function 数据处理(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); //没有在map中找到翻译 if (mainMap[value] === undefined) { //是中文、不是日文 if (/[\u4E00-\u9FA5]+/.test(value) && !/[ぁ-んァ-ヶ]+/.test(value)) { if (attribute === 'Text') { node = node.parentNode; } let title = node.getAttribute('title'); //如果title没有翻译,则记录 if (title !== null && mainMap[title] === undefined) { addMap[title] = value; } } } } }; /*** 记录所有满足条件的未翻译内容
缺点为找不到上下文*/ noMap = {}; /*** 记录所有满足条件的未翻译提示信息
缺点为找不到上下文*/ noTitleMap={}; /*** 用以复制value到title[已弃用] *
现用来导出未翻译的title和value,map->控制台 *
若出现新元素,请手动通过控制台重新调用 *
若干扰项太多,可以通过删除干扰元素,再重新调用 * */ copyToTitle = () => { //清空 noMap = {}; noTitleMap={}; 递归(document.body, 数据处理); console.log(JSON.stringify(noMap)); console.log(JSON.stringify(noTitleMap)); function 数据处理(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); //没有在map中找到翻译 if (mainMap[value] === undefined) { //1<长度<300,不为中文、日文,不是纯数字 if (1 < value.length && value.length < 300 && !/[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(value) && !/^[\d]+$/.test(value)) { //归一化处理 if (attribute === 'Text') { node = node.parentNode; } let title = node.getAttribute('title'); //title属性为中文或日文时不执行后续操作 if (title != null && /[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(title)) { return; } //未翻译的节点title if(title!= null&&titleMap[title]===undefined){ let flag=true; /*判断子节点文本,若文本为中文日文或匹配mainMap或与title相等,则不添加到未翻译title*/ let nodelist=node.childNodes; for (let i=0;ititle // if (title != null && title.trim() !== value) { // node.setAttribute('title', title + ' ' + value); // } else { // node.setAttribute('title', value); // } //设置没有翻译的map标记 noMap[value] = value.toLowerCase(); } } } }; /*立即执行*/ copyToTitle(); /** *统计不应该匹配,但可以匹配的k->v与正则,用以将局部map升级到主map * @type {{Object}} */ otherLog = GM_getValue('otherLog') || {}; console.log(otherLog); delotherLog = () => { GM_deleteValue('otherLog'); }; /** * 按降序显示otherLog数组 */ showotherLog=()=>{ //复制一份 let temp=JSON.parse(JSON.stringify(otherLog)); let k = Object.getOwnPropertyNames(temp); let otherLogList=[]; for (let i = 0, len = k.length; i < len; i++) { temp[k[i]].unshift(k[i]); otherLogList.push(temp[k[i]]); } /*排序*/ otherLogList.sort(function (obj1, obj2) { return obj2[1] - obj1[1]; }); console.log(otherLogList); let sb='匹配项\t匹配数\t匹配时机\t匹配结果\n'; for (let i=0,len=otherLogList.length;i { /*object转Map,将其他没有生效的map合起来*/ let otherMap = {}; let otherTitleMap={}; let otherSpecialMap = new Map(); otherPageRules.forEach((item) => { let pathname = window.location.pathname; if (item.regular !== undefined && item.regular instanceof RegExp && !item.regular.test(pathname)) { let k = Object.getOwnPropertyNames(item.specialMap); for (let i = 0, len = k.length; i < len; i++) { try { otherSpecialMap.set(new RegExp(k[i]), item.specialMap[k[i]]); } catch (e) { console.log('"' + k[i] + '"不是一个合法正则表达式'); } } Object.assign(otherMap, item.map); Object.assign(otherTitleMap, item.titleMap); } }); /*立即执行*/ console.time('初始其他规则,调试'); 递归(document.body, 未生效规则匹配测试); console.timeEnd('初始其他规则,调试'); /*当body发生变化时执行*/ dom修改事件(document.body, (records) => { console.time('其他规则,调试'); for (let i = 0, len = records.length; i < len; i++) { 递归(records[i].target, 未生效规则匹配测试); } console.timeEnd('其他规则,调试'); /*若不相等则更新并输出*/ if (JSON.stringify(otherLog) !== JSON.stringify(GM_getValue('otherLog') || {})) { GM_setValue('otherLog', otherLog); console.log(otherLog); } }); /** * 统计不应该匹配,但可以匹配的k->v与正则,用以将局部map升级到主map * @param key * @param value */ function otherLogAdd(key, value) { if (otherLog[key] === undefined) { otherLog[key] = [1, value[0], value[1]]; } else { let item = otherLog[key]; item[0]++; /*去重*/ let a1 = item[1].split('$$'); a1.push(value[0]); let mySet = new Set(a1); a1 = [...mySet]; item[1] = a1.join('$$'); a1 = item[2].split('$$'); a1.push(value[1]); mySet = new Set(a1); a1 = [...mySet]; item[2] = a1.join('$$'); } } function 未生效规则匹配测试(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); /*不被mainMap和specialMap匹配*/ /*由于执行顺序的原因,该判断基本没有意义*/ if (mainMap[value] !== undefined) { return; } /*因为正则涉及匹配可能太广,所以不排除*/ // for (let key of specialMap.keys()) { // if (key.test(value)) { // return; // } // } /*1<长度<300,不为中文、日文,不是纯数字,降低缩进*/ let f = true; if (1 < value.length && value.length < 300 && !/[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(value) && !/^[\d]+$/.test(value)) { f = false; } if (f) return; /*title翻译*/ /** titleMap翻译*/ if (attribute==='title'){ if(otherMap[value] === undefined &&node.nodeType === 1&&node.title ){ /*如果为节点类型,value没有翻译,且存在title*/ let flag=true; /*判断子节点文本,若文本为中文日文或匹配mainMap或与title相等,则执行后续操作*/ let nodelist=node.childNodes; for (let i=0;iotherMap匹配', otherMap[title]]); } } } return; } if (otherMap[value] !== undefined) { if (attribute === 'Text') { //若为文本节点则追加父节点title属性 let title = node.parentNode.getAttribute('title'); if (title != null && title.trim() !== value) { node.parentNode.setAttribute('title', title + ' ' + value); } else { node.parentNode.setAttribute('title', value); } node.nodeValue = otherMap[value]; otherLogAdd(value, ['otherMap匹配,Text', otherMap[value]]); } else { //若为通常节点则正常设置属性 node.setAttribute('title', value); node.setAttribute(attribute, otherMap[value]); otherLogAdd(value, ['otherMap匹配,节点', otherMap[value]]); } } else { //遍历specialMap,正则替换 for (let key of otherSpecialMap.keys()) { /*正则匹配,降低缩进*/ if (!key.test(value)) {continue;} let info = 'otherSpecialMap匹配,正则:' + key + ','; /*正则替换*/ let newValue = value.replace(key, otherSpecialMap.get(key)); /*若有循环替换符,则进行替换*/ let nvs = newValue.split('%%'); /*如果map的值没有中文,且带%%%%,则设置flag为true*/ let flag = false; if (!/[\u4E00-\u9FA5]+/.test(otherSpecialMap.get(key)) && nvs.length !== 1 && nvs.length % 2 === 1) { flag = true; } if (nvs.length !== 1 && nvs.length % 2 === 1) { for (let i = 1; i < nvs.length; i += 2) { /*转小写*/ let low = nvs[i].split('@@'); if (low.length === 3) { nvs[i] = low[1].toLowerCase(); } /*匹配otherMap*/ if (otherMap[nvs[i]] !== undefined) { nvs[i] = otherMap[nvs[i]]; info += '在otherMap找到%%%%(额外匹配),'; /*若找到map,则重新置flag为false*/ flag = false; } /*匹配匹配mainMap*/ if (mainMap[nvs[i]] !== undefined) { nvs[i] = mainMap[nvs[i]]; info += '在mainMap找到%%%%(额外匹配),'; /*若找到map,则重新置flag为false*/ flag = false; } } newValue = nvs.join('') } /*如果替换式没有中文,且%%%%也没有匹配,则跳过*/ if (flag) {continue;} if (attribute === 'Text') { //若为文本节点则追加父节点title属性 let title = node.parentNode.getAttribute('title'); if (title != null && title.trim() !== value) { node.parentNode.setAttribute('title', title + ' ' + value); } else { node.parentNode.setAttribute('title', value); } node.nodeValue = newValue; info += 'Text'; } else { //若为通常节点则正常设置属性 node.setAttribute('title', value); node.setAttribute(attribute, newValue); info += '节点'; } otherLogAdd(value, [info, newValue]); // console.log(value + '->' + newValue); /*替换后结束遍历*/ break; } } } })(); }