Greasy Fork is available in English.

文本通杀屏蔽工具

将带有指定词的文本整段屏蔽

// ==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;
    }
}