将带有指定词的文本整段屏蔽
// ==UserScript== // @name 文本通杀屏蔽工具 // @namespace http://tampermonkey.net/ // @version 1.0.5 // @description 将带有指定词的文本整段屏蔽 // @author aotmd // @include /.*/ // @license MIT // @run-at document-body // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== let console = { ...window.console }; console.time=()=>{}; console.timeEnd=()=>{}; /**-----------------------------用户自定义部分[50行]----------------------------------*/ let setting={ //设置屏蔽样式,[true|false],true为开启,false为关闭: 黑幕遮罩:false, 打码屏蔽:true, //鼠标移到目标上后持续指定毫秒后再移出则取消屏蔽. 屏蔽恢复时间:1500, //黑幕遮罩启用后,右上角有黑幕开关进行开关,是否记忆开关状态 记忆黑幕开关状态:false, }; /*通用主文本屏蔽词,作用在全局,示例:let mainArray = ["砖家建议","震惊",];*/ let mainArray = []; /** * title屏蔽词 * @type Array */ let titleArray=[]; /** 特殊全局map,用以替换变动的文本节点[正则], * value出现的%%$1%%为需要继续屏蔽值 * vlaue出现的%%@@$1@@%%将$1转小写,然后继续屏蔽值 * */ let specialMap = { /*转小写再匹配map,范围太广不使用*/ // "^([a-zA-Z -]+)$":"%%@@$1@@%%", // /** 游戏详情页,评分统计 /v\d+ */ // "^(\\d+) vote[s]? total, average ([\\d.]+) \\(([a-zA-Z -]+)\\)$": "总共$1票, 平均分$2 (%%$3%%)", // /** 讨论 */ // "^discussions \\((\\d+)\\)$": "讨论 ($1)", // // /**上移->个人页相关 评分说明(下拉列表,选择分数时)*/ // "^(\\d+) \\(([a-zA-Z -]+)\\)$": "$1 (%%$2%%)", }; /*额外map,作用在指定页面*/ let otherPageRules = [ { name:'绯月', regular:/bbs\.kfpromax\.com/i, Array:["答辩","难蚌","自嗨","垃圾","精日"], titleArray:[], specialMap:{}, }, { name:'百度百科女装屏蔽实例', regular:/baike.baidu.com\/item\/%E5%A5%B3%E8%A3%85/i, Array:["女装",], titleArray:["女装",], specialMap:{}, }, { name:'bilibili', regular:/bilibili\.com/i, Array:["震惊","一小伙","毕业","停播","抑郁","玉玉","流量密码","贵物","答辩","难蚌","自嗨","垃圾","精日","吃饱了","抽象"], titleArray:["震惊","一小伙","毕业"], specialMap:{}, }, { name:'bgm', /*要屏蔽的页面,使用正则匹配*/ regular:/bgm\.tv/i, /*屏蔽的词的数组*/ Array:["小丑","答辩","粪作","垃圾"], /*屏蔽的title的词的数组*/ titleArray:[], /*正则匹配要屏蔽的词的map*/ specialMap:{}, }, { name:'规则说明', /*要屏蔽的页面,使用正则匹配*/ regular:/.*/i, /*屏蔽的词的数组*/ Array:[], /*屏蔽的title的词的数组*/ titleArray:[], /*正则匹配要屏蔽的词的map*/ specialMap:{}, }, ]; /**-----------------------------用户自定义部分结束----------------------------------*/ /**-----------------------------业务逻辑部分[450行]----------------------------------*/ /** ---------------------------map处理---------------------------*/ let href = window.location.href; otherPageRules.forEach((item) => { //当regular是正则才执行 if (item.regular !== undefined && item.regular instanceof RegExp) { if (item.regular.test(href)) { //添加到主map,若存在重复项则覆盖主map mainArray=mainArray.concat(item.Array); //添加特殊map Object.assign(specialMap, item.specialMap); //添加titleMap titleArray=titleArray.concat(item.titleArray); console.log(item.name + ',规则匹配:' + href + '->' + item.regular); } } }); //去重 mainArray=Array.from(new Set(mainArray)); titleArray=Array.from(new Set(titleArray)); /*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 array * @param value * @returns {boolean|*} */ function 包含判断(array, value) { let len=array.length; for (let i=0;i<len;i++){ if (value.includes(array[i])){ return array[i]; } } return false; } /** * 递归节点 * @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.getAttribute('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); } } } /** 与下方函数结合*/ let observerMap=new Map(); /** * 修改后的函数,在触发事件后会对其他相同效果的obs排队依次触发 * 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) { //进入后停止侦听 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); } /** * 鼠标悬停指定时间后执行 * @param node 元素 * @param timeout 指定时间ms * @param func 执行函数. * @param clearEven 执行一次后移除事件. */ function 鼠标悬停指定时间后执行(node, timeout, func,clearEven=true) { let clear; node.addEventListener('mouseover', f1); node.addEventListener('mouseout', f2); function f1() { // 注册移过事件处理函数 clear = setTimeout(() => { func(); if (clearEven){ node.removeEventListener("mouseover", f1); node.removeEventListener("mouseout", f2); } }, timeout); } function f2() { // 注册移出事件处理函数 clearTimeout(clear); } } /**--------------------------一般函数结束--------------------------*/ (function () { /*当body发生变化时执行*/ dom修改事件(document.body, (records) => { 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(); /** titleMap翻译*/ if (attribute==='title'){ if(node.nodeType === 1&&node.title){ /*如果为节点类型,且存在title*/ let f1=包含判断(titleArray,node.title); if (!!f1&&node.getAttribute('titleFlag')===null&&node.getAttribute('mainFlag')===null) { node.setAttribute('titleFlag', 'true'); let temp=node.title; node.title="屏蔽词:"+f1; // 使用方法 鼠标悬停指定时间后执行(node, setting.屏蔽恢复时间-100,()=>{ node.setAttribute('title',temp); }); } } return; } /** mainMap翻译*/ let f1=包含判断(mainArray,value); if (!!f1) { if (attribute === 'Text') { //若为文本节点则为父节点设置遮罩 if (setting.黑幕遮罩){ node.parentNode.classList.add('maskedStatement'); } if (setting.打码屏蔽){ if (node.parentNode.getAttribute('mainFlag')===null){ let temp=node.nodeValue,nodeTemp=node.parentNode; node.parentNode.title="屏蔽词:"+f1; node.parentNode.style.wordBreak="break-word"; node.nodeValue=Array(node.nodeValue.length+1).join('▇'); 鼠标悬停指定时间后执行(node.parentNode,setting.屏蔽恢复时间,()=>{ nodeTemp.setAttribute('mainFlag', 'true'); //不知什么原因,这里有时赋值没有体现在页面上 node.nodeValue=temp; //重新附加一个用父找子然后更改的. let len=nodeTemp.childNodes.length; for (let i=0;i<len;i++){ if (nodeTemp.childNodes[i].nodeValue&&nodeTemp.childNodes[i].nodeValue.includes('▇')){ nodeTemp.childNodes[i].nodeValue=temp; } } try{ nodeTemp.removeAttribute('title'); node.parentNode.removeAttribute('title'); }catch (e) {console.error(e)} }) } } } else { //若为通常节点则正常设置遮罩 if (setting.黑幕遮罩){ node.classList.add('maskedStatement'); } if (setting.打码屏蔽){ if (node.getAttribute('mainFlag')===null){ node.setAttribute('mainFlag', 'true'); let temp=value; node.title="屏蔽词:"+f1; node.setAttribute(attribute, Array(value.length+1).join('▇')); 鼠标悬停指定时间后执行(node,setting.屏蔽恢复时间,()=>{ node.removeAttribute('title'); node.setAttribute(attribute,temp); }) } } } }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 (!!包含判断(mainArray,nvs[i])) { /*若找到map,则重新置flag为false*/ flag = false; } } newValue = nvs.join('') } if (flag) {/*如果替换式没有中文,且%%%%也没有匹配,则跳过*/ continue; } let f1=key; if (attribute === 'Text') { //若为文本节点则为父节点设置遮罩 if (setting.黑幕遮罩){ node.parentNode.classList.add('maskedStatement'); } if (setting.打码屏蔽){ if (node.parentNode.getAttribute('mainFlag')===null){ let temp=node.nodeValue,nodeTemp=node.parentNode; node.parentNode.title="屏蔽词:"+f1; node.parentNode.style.wordBreak="break-word"; node.nodeValue=Array(node.nodeValue.length+1).join('▇'); 鼠标悬停指定时间后执行(node.parentNode,setting.屏蔽恢复时间,()=>{ nodeTemp.setAttribute('mainFlag', 'true'); //不知什么原因,这里有时赋值没有体现在页面上 node.nodeValue=temp; //重新附加一个用父找子然后更改的. let len=nodeTemp.childNodes.length; for (let i=0;i<len;i++){ if (nodeTemp.childNodes[i].nodeValue&&nodeTemp.childNodes[i].nodeValue.includes('▇')){ nodeTemp.childNodes[i].nodeValue=temp; } } try{ nodeTemp.removeAttribute('title'); node.parentNode.removeAttribute('title'); }catch (e) {console.error(e)} }) } } } else { //若为通常节点则正常设置遮罩 if (setting.黑幕遮罩){ node.classList.add('maskedStatement'); } if (setting.打码屏蔽){ if (node.getAttribute('mainFlag')===null){ node.setAttribute('mainFlag', 'true'); let temp=value; node.title="屏蔽词:"+f1; node.setAttribute(attribute, Array(value.length+1).join('▇')); 鼠标悬停指定时间后执行(node,setting.屏蔽恢复时间,()=>{ node.removeAttribute('title'); node.setAttribute(attribute,temp); }) } } } break; } } } } })(); /**-----------------------------黑幕控制----------------------------------*/ if (setting.黑幕遮罩) { //屏蔽样式: let styleElement = addStyle(` .maskedStatement:hover,maskedStatement:active/*, .maskedStatement a:hover,maskedStatement a:active, a .maskedStatement:hover,a maskedStatement:active */ { color: white!important; transition:color 0.3s linear; } .maskedStatement/*, .maskedStatement a, a .maskedStatement*/ { color: #252525!important; text-shadow: none; background-color: #252525!important; } `); let a1 = document.createElement('button'); a1.className = "gt1 button2"; document.body.appendChild(a1); let flag=true; if(setting.记忆黑幕开关状态) { if (GM_getValue('flag') !== undefined) { flag = GM_getValue('flag') } else { GM_setValue('flag', true); flag = true; } } if (flag) { a1.innerText = "关闭黑幕"; } else { a1.innerText = "开启黑幕"; styleElement.setAttribute("type", 'text'); } a1.onclick = function () { flag = !flag; if (flag) { styleElement.setAttribute("type", 'text/css'); a1.innerText = "关闭黑幕"; } else { styleElement.setAttribute("type", 'text'); a1.innerText = "开启黑幕"; } if(setting.记忆黑幕开关状态) { GM_setValue('flag', flag); } }; addStyle(`.gt1 { padding: 5px 5px; font-size: 14px; color: snow; position: fixed; border-radius: 4px; right: 5px; top: 5%; z-index: 999999; text-align: center; text-decoration: none; display: inline-block; margin: 4px 2px; -webkit-transition-duration: 0.4s;/* Safari */ transition-duration: 0.4s; cursor: pointer; background-color: #4CAF50; border: 2px solid #4CAF50; padding: 0px; font-size: 12px; opacity: 0.2; right: -40px; } .gt1:hover { background-color: white; color: black; font-size: 14px; padding: 5px 10px; -webkit-transition: 0.5s; opacity: 1; margin: -3px 2px; right: 5px; } `); /** * 添加css样式 * @param rules css字符串 */ function addStyle(rules) { let styleElement = document.createElement('style'); styleElement["type"] = 'text/css'; document.getElementsByTagName('head')[0].appendChild(styleElement); styleElement.appendChild(document.createTextNode(rules)); return styleElement; } }