// ==UserScript==
// @name VNDB优先原文和中文化
// @namespace http://tampermonkey.net/
// @version 4.3.1
// @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://gf.qytechs.cn/scripts/445990-vndbtranslatorlib/code/VNDBTranslatorLib.js?version=1058974
// ==/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;
if (el.nodeName === 'INPUT') {
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 {
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)
&& !/^[> ]+$/.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<nodelist.length;++i){
if (nodelist[i].nodeType===3&&
(/[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(nodelist[i].nodeValue)
||mainMap[nodelist[i].nodeValue] !== undefined
||node.title===nodelist[i].nodeValue)
){
flag=false;
break;
}
}
if (flag){/*加上翻译信息*/
if (titleMap[node.title] !== undefined) {
node.title=titleMap[node.title]+" \t\n"+node.title;
}else if (mainMap[node.title]!==undefined){
node.title=mainMap[node.title]+" \t\n"+node.title;
}
}
}
return;
}
/** mainMap翻译*/
if (mainMap[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 = mainMap[value];
} else {
//若为通常节点则正常设置属性
node.setAttribute('title', value);
node.setAttribute(attribute, mainMap[value])
}
}else {
/** specialMap正则翻译*/
//遍历specialMap,正则替换
for (let key of specialMap.keys()) {
/*正则匹配*/
if (key.test(value)) {
/*正则替换*/
let newValue = value.replace(key, specialMap.get(key));
/*若有循环替换符,则进行替换*/
let nvs = newValue.split('%%');
/*如果map的值没有中文,且带%%%%,则设置flag为true*/
let flag = false;
if (!/[\u4E00-\u9FA5]+/.test(specialMap.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();
}
/*匹配mainMap*/
if (mainMap[nvs[i]] !== undefined) {
nvs[i] = mainMap[nvs[i]];
/*若找到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;
} else {
//若为通常节点则正常设置属性
node.setAttribute('title', value);
node.setAttribute(attribute, newValue)
}
// console.log(value+'->'+newValue);
/*替换后结束遍历*/
break;
}
}
}
}
})();
/**-----------------------------开发用函数部分[350行]----------------------------------*/
/** 开启后通过控制台调用函数即可*/
let 开发者模式 = false;
if (开发者模式) {
/*exportMap已弃用*/
/**
* 导出新的已被翻译的内容到控制台显示
* <br>即手动在网页上改文本,注意:
* <br>先在要翻译的文本中间写入翻译后的内容
* <br>然后用del和backspace删除前后内容
* <br>开启编辑模式:
* <br>document.body.contentEditable='true';
* <br>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;
}
}
}
}
};
/*** 记录所有满足条件的未翻译内容<br>缺点为找不到上下文*/
noMap = {};
/*** 记录所有满足条件的未翻译提示信息<br>缺点为找不到上下文*/
noTitleMap={};
/*** <s>用以复制value到title[已弃用]</s>
* <br>现用来导出未翻译的title和value,map->控制台
* <br>若出现新元素,请手动通过控制台重新调用
* <br>若干扰项太多,可以通过删除干扰元素,再重新调用
* */
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;i<nodelist.length;++i){
if (nodelist[i].nodeType===3&&
(/[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(nodelist[i].nodeValue)
||mainMap[nodelist[i].nodeValue] !== undefined
||title===nodelist[i].nodeValue)
){
flag=false;
break;
}
}
/*如果存在title,且title没有定义在mainMap*/
/*那么添加到待翻译title信息*/
if (flag){
noTitleMap[title] = title.toLowerCase();
}
}
//复制value->title
// 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<len;i++){
sb+=otherLogList[i].join('\t')+'\n'
}
console.log(sb)
};
/*未生效规则匹配测试,用以筛选常用规则手动上移至主规则*/
(() => {
/*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;i<nodelist.length;++i){
if (nodelist[i].nodeType===3&&
(/[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(nodelist[i].nodeValue)
||otherMap[nodelist[i].nodeValue] !== undefined
||node.title===nodelist[i].nodeValue)
){
flag=false;
break;
}
}
if (flag){/*加上翻译信息*/
let title=node.title;
if (otherTitleMap[title] !== undefined) {
node.title=otherTitleMap[title]+" \t\n"+title;
otherLogAdd(title, ['otherTitleMap匹配', otherTitleMap[title]]);
}else if (otherMap[node.title]!==undefined){
node.title=otherMap[title]+" \t\n"+title;
otherLogAdd(title, ['otherTitleMap->otherMap匹配', 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;
}
}
}
})();
}