- // ==UserScript==
- // @name 网页高亮关键字
- // @namespace http://tampermonkey.net/
- // @version 1.1.2.64
- // @description 对网页上的文字进行高亮显示,如果对你有帮助,可以随意修改使用
- // @author You
- // @include *
- // @grant GM_addStyle
- // @grant GM_registerMenuCommand
- // @grant GM_setValue
- // @grant GM_getValue
- // @require https://cdn.jsdelivr.net/npm/vue@2.6.1/dist/vue.min.js
- // @icon https://github.com/ChinaGodMan/UserScripts/raw/main/docs/icon/Scripts%20Icons/icons8-mark-96.png
- // @supportURL https://github.com/ChinaGodMan/UserScripts/issues
- // @homepageURL https://github.com/ChinaGodMan/UserScripts
- // @license MIT
-
- // ==/UserScript==
- (function () {
-
-
- // 初始化
- function initialize() {
- let defaultWords = {
- "key_123": {
- limit: ["baidu"],
- "info": "汉字测试",
- "words": ["抖音", "快手", "网页", "平台", "的", "最", "一", "个", "多", "服务", "大"],
- "color": "#85d228",
- "textcolor": "#3467eb"
-
-
- },
- "key_124": {
- limit: [],
- "info": "数字测试",
- "words": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
- "color": "#48c790",
- "textcolor": "#3467eb"
-
- },
- "key_3379656389": {
- limit: [],
- "info": "字母测试",
- "words": ["a", "b", "c", "d", "e", "f", "t", "y", "u", "i", "o", "k", "j", "h", "g", "s", "z", "x", "v", "n", "m"],
- "color": "#e33544",
- "textcolor": "#3467eb"
- },
- "key_4947181948": {
- limit: [],
- "info": "相同的字可以显示各个分组的标题",
- "words": ["的", "最", "一", "个", "多", "服务", "大"],
- "color": "#6e7bdd",
- "textcolor": "#e33544"
- }
- }
- // 设置关键字默认值
- if (!GM_getValue("key")) { GM_setValue("key", defaultWords) }
- if (Object.keys(GM_getValue("key")).length == 0) { GM_setValue("key", defaultWords) }
- // GM_setValue("key",this.defaultWords);
-
- let cache = GM_getValue("key")
- Object.keys(cache).forEach(key => {
- let defult = {
- limit: [],
- info: "",
- words: [],
- color: "#85d228"
- }
- Object.keys(defult).forEach((key2) => {
- if (!cache[key][key2]) {
- console.log(defult[key2])
- cache[key][key2] = defult[key2]
- }
- })
- })
-
- GM_setValue("key", cache)
- }
- /**
- * @description: 遍历找出所有文本节点
- * @param {*} node
- * @return {*} 节点map
- */
- function textMap(node) {
- // 存储文本节点
- let nodeMap = new Map()
-
- const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, (textNode) => {
- if (textNode.parentElement.nodeName === "SCRIPT" |
- textNode.parentElement.nodeName === "script" |
- textNode.parentElement.nodeName === "style" |
- textNode.parentElement.nodeName === "STYLE" |
- textNode.parentElement.className === "mt_highlight" |
- document.querySelector("#mt_seting_box").contains(textNode)
- ) {
- return NodeFilter.FILTER_SKIP
- }
-
- if (textNode.data.length < 20) {
- return textNode.data.replace(/[\n \t]/g, "").length ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
- }
-
- return NodeFilter.FILTER_ACCEPT
- })
-
- while ((textNode = walker.nextNode())) {
- nodeMap.set(textNode, textNode.data)
- }
-
- return nodeMap
- }
-
- // 高亮
- class HIGHTLIGHT {
-
- // 需要高亮的关键字
- /**通过规则新建关键字列表,解决一个关键字会存在多个分类中
- * 将{
- * key1{
- * words:[word1,word2]
- * },
- * key2{
- * words:[word3,word4]
- * }
- * }
- * 转换为map{
- * word1:key1
- * word2:key1
- * word4:key2
- * word3:key2
- * }
- * @description:
- * @return {map}map{
- *
- * classesKey=>分类标签,类型string
- *
- * infoList=>提示词,数组["汉字","字符"]
- *
- * }
- */
- static words() {
-
- // 转换
- let newWords = new Map
- Object.keys(GM_getValue("key")).forEach(classesKey => {
-
- let info = GM_getValue("key")[classesKey].info
- let words = GM_getValue("key")[classesKey].words
- let color = GM_getValue("key")[classesKey].color
- let limit = GM_getValue("key")[classesKey].limit
- let textcolor = GM_getValue("key")[classesKey].textcolor
-
-
- words.forEach(word => {
- let infoList = []
-
- // 检测是否被多个类目包含,被多个类目包含的关键字会有对应类目的信息
- if (newWords.get(word + "")) {
- infoList = newWords.get(word + "").infoList
- infoList.push(info)
- } else {
- infoList = [info]
- }
-
- newWords.set(word + "", {
- classesKey,
- infoList: infoList,
- textcolor,
- color,
- limit
- })
- })
- })
- return newWords
- }
-
- // 检测正则
- static reg() {
- let url = window.location.href
- let doIt = false
- let wordsList = []
- let words = this.words()
- words.forEach((value, word) => {
- // console.log(value.limit);
- // 过滤不匹配的
- if (value.limit.length == 0 || url.match(new RegExp(`${value.limit.join("|")}`, "g"))) {
- // 添加要筛选的关键字
- wordsList.push(word)
- }
- })
- // 过滤后还需不需要检测
- wordsList.length ? doIt = true : doIt = false
- // console.log(doIt,wordsList);
- return {
- rule: new RegExp(`(${wordsList.join("|")})`, "g"),
- doIt
- }
- }
-
-
- // 高亮css
- static highlightStyle = `
- .mt_highlight{
- background-color: rgb(255, 21, 21);
- border-radius: 2px;
- box-shadow: 0px 0px 1px 1px rgba(0, 0, 0,0.1);
- cursor: pointer;
- color: white;
-
- padding: 1px 1px;
- }
- `
-
- /**
- * @description: 返回需要被高亮的节点map{textNode,未来会被修改成目标的值}
- * @param {map} nodeMap
- * @return {void}
- */
- static highlight(nodeMap) {
- let words = this.words()
- let reg = this.reg()
- // 没有要高亮的关键字时不执行
-
- if (words.size && reg.doIt) {
- nodeMap.forEach((value, node) => {
- // 正则检测是否符合规则
- let newInnerHTML = value.replace(reg.rule, (word) => {
- let classesKey = words.get(word).classesKey
- let infoList = words.get(word).infoList
- let color = words.get(word).color
- let textcolor = words.get(word).textcolor
-
- // 返回新节点模板
- // return `<span class="mt_highlight" classesKey="${classesKey}" title="${infoList.join("\n")}" style="background: ${color};">${word}</span>`
-
- return `<span class="mt_highlight" classesKey="${classesKey}" title="${infoList.join("\n")}" style="background: ${color}; color:${textcolor};">${word}</span>`
- })
- // 是否检测出了
- if (value != newInnerHTML) {
- // 节点替换
- let newNode = document.createElement("span")
- newNode.innerHTML = newInnerHTML
- node.parentElement.replaceChild(newNode, node)
- // 点击复制
- newNode.addEventListener("click", (e) => {
- navigator.clipboard.writeText(e.target.innerText)
- })
- }
- })
- }
-
- }
-
- }
-
- /**
- * @description: 动态检测新节点,并执行高亮
- * @return {*}
- */
- function watch() {
- // 选择需要观察变动的节点
- const targetNode = document.body
-
- // 观察器的配置(需要观察什么变动)
- const config = { attributes: false, childList: true, subtree: true, characterData: true }
-
- // 当观察到变动时执行的回调函数
- const callback = function (mutationsList, observer) {
- let nodeMap = new Map
- setTimeout(() => {
- mutationsList.forEach(node => { nodeMap.set(node.target) })
- nodeMap.forEach((value, node) => {
- doOnce(node)
- })
- }, 1)
- }
-
- // 创建一个观察器实例并传入回调函数
- const observer = new MutationObserver(callback)
-
- // 以上述配置开始观察目标节点
- observer.observe(targetNode, config)
- }
-
-
- // gui
- class GUI {
- // 模板
- static setingTemplate = String.raw`
- <div class="seting_box" v-show="showSeting">
- <!-- 顶部选项 -->
- <div class="option_box">
- <div @click="config_in_add">导入添加</div>
- <div @click="config_in">导入覆盖</div>
-
- <input type="file" class="config_file" accept=".json" @change="file_read($event)">
-
- <div @click="config_out">导出配置文件</div>
- <div @click="refresh">刷新</div>
- <div class="close_seting" @click="close_seting">关闭</div>
- </div>
-
- <!-- 规则视图 -->
- <div class="rule_list_box" v-for="(value,key) in rule">
-
- <!-- 展示视图 -->
- <div class="show_box" v-show="!edit[key]" >
-
- <!-- 左边 -->
- <div class="show_left">
-
- <!-- 网站作用域 -->
- <div class="words_box" @click="editOn(key)">
- <span v-for="(word) in value.limit" :style="{'background': value.color, 'color': value.textcolor}">
- {{word}}
- </span>
- <!-- 没有限制 -->
- <span v-if="! value.limit.length" :style="{'background': value.color, 'color': value.textcolor}">
- 不限制
- </span>
- </div>
-
- <!-- 类目 -->
- <div class="info_box" @click="editOn(key)" :style="{'background': value.color, 'color': value.textcolor}">
- {{value.info}}
- </div>
- <!-- 关键字 -->
- <div class="words_box" @click="editOn(key)">
- <span v-for="(word) in value.words" :style="{'background': value.color, 'color': value.textcolor}">
- {{word}}
- </span>
- </div>
- </div>
-
- <!-- 分割线 -->
- <div class="line"></div>
-
- <!-- 修改颜色和删除 -->
- <div class="rule_set_box">
- <div class="color_box">
- <input type="color"
- :colorKey="key"
- v-model="value.color"
- @change="colorChange(key,value.color,value.textcolor)"
- >
- </div>
-
- <div class="textcolor_box">
- <input type="color"
- :colorKey="key"
- v-model="value.textcolor"
- @change="colorChange(key,value.color,value.textcolor)"
- >
- </div>
- <div class="del" @click.stop="del_key(key)">删除</div>
- </div>
- </div>
-
- <!-- 编辑视图 -->
- <div class="eidt_box" v-show="edit[key]">
- <div class="eidt_left">
- <!-- 修改作用域 -->
- <textarea :limit_key="key" :value="value.limit.toString().replace(/,/g,' ')"></textarea>
- <!-- 修改类目信息 -->
- <textarea :info_key="key" :value="value.info"></textarea>
- <!-- 修改关键字 -->
- <textarea :words_key="key" :value="value.words.toString().replace(/,/g,' ')"></textarea>
- </div>
-
- <!-- 分割线 -->
- <div class="line"></div>
-
- <!-- 确定 取消 -->
- <div class="eidt_right">
- <div class="del" @click="editOff(key)">取消</div>
- <div class="del" @click="ruleUpdate(key)">确定</div>
- </div>
-
- </div>
-
- </div>
-
- <!-- 添加新规则 -->
- <div class="add" @click="add_key">+</div>
-
- </div>
- `
- // 模板css
- static setingStyle = `
- body {
- --mian_width: 480px;
- --mian_color: #189fd8;
- --radius: 5px;
- --info_color: #eaeaea;
- --font_color: #676767;
- }
- .seting_box {
- width: 500px;
- max-height: 800px;
- overflow: auto;
- background: white;
- border-radius: 5px;
- position: fixed;
- transform: translate(-50%, 0);
- top: 50px;
- left: 50%;
- border: 1px solid rgba(0, 0, 0, 0.1);
- padding: 15px 5px;
- flex-direction: column;
- align-items: center;
- z-index: 10000;
- display: flex;
- box-shadow: 0 1px 5px 5px rgba(0, 0, 0, 0.1);
- }
- .option_box {
- width: var(--mian_width);
- display: flex;
- justify-content: space-between;
- }
- .option_box div {
- display: flex;
- height: 20px;
- align-items: center;
- padding: 5px 10px;
- background: var(--mian_color);
- color: white;
- border-radius: var(--radius);
- cursor: pointer;
- }
- .rule_list_box {
- width: var(--mian_width);
- border-radius: var(--radius);
- margin-top: 10px;
- padding: 5px 0px;
- box-shadow: 0 0 5px 0px #e2e2e2;
- cursor: pointer;
- }
- .rule_list_box .show_box {
- display: flex;
- justify-content: space-between;
- }
- .rule_list_box .show_box .show_left {
- width: 410px;
- }
- .rule_list_box .show_box .show_left > div {
- margin-top: 5px;
- }
- .rule_list_box .show_box .show_left > div:nth-child(1) {
- margin-top: 0px;
- }
- .rule_list_box .show_box .show_left .info_box {
- margin-left: 5px;
- margin-right: 5px;
- padding: 2px 5px;
- min-height: 22px;
- color: white;
- border-radius: var(--radius);
- background-color: var(--mian_color);
- display: flex;
- align-items: center;
- }
- .rule_list_box .show_box .show_left .words_box {
- margin-top: 0px;
- /* border: 1px solid black; */
- min-height: 20px;
- display: flex;
- flex-wrap: wrap;
- }
- .rule_list_box .show_box .show_left .words_box span {
- background-color: var(--info_color);
- color: white;
- padding: 2px 5px;
- border-radius: var(--radius);
- margin-left: 5px;
- margin-top: 5px;
- display: flex;
- align-items: center;
- height: 20px;
- }
- .rule_list_box .show_box .rule_set_box {
- display: flex;
- flex-direction: column;
- justify-content: space-around;
- padding: 0px 5px;
- }
- .rule_list_box .show_box .rule_set_box .color_box .textcolor_box {
- overflow: hidden;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .rule_list_box .show_box .rule_set_box .color_box .textcolor_box input {
- width: 50px;
- height: 25px;
- border-radius: var(--radius) !important;
- padding: 0px;
- }
- .rule_list_box .eidt_box {
- padding: 0px 5px;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
- .rule_list_box .eidt_box .eidt_left {
- width: 400px;
- }
- .rule_list_box .eidt_box .eidt_left textarea {
- width: 100% !important;
- min-height: 30px !important;
- border: none;
- outline: none;
- color: var(--font_color);
- background-color: var(--info_color);
- border-radius: var(--radius);
- margin-top: 5px;
- padding: 5px;
- }
- .rule_list_box .eidt_box .eidt_left textarea:nth-child(1) {
- margin-top: 0px;
- }
- .rule_list_box .eidt_box .eidt_right {
- display: flex;
- flex-direction: column;
- justify-content: space-around;
- }
- .rule_list_box .line {
- width: 1px;
- background-color: rgba(0, 0, 0, 0.1);
- }
- .rule_list_box .del {
- background-color: var(--mian_color);
- color: white;
- border-radius: var(--radius);
- padding: 0px 10px;
- font-size: 15px;
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer;
- }
- .add {
- width: var(--mian_width);
- height: 30px;
- background: #189fd8;
- color: white;
- display: flex;
- justify-content: center;
- border-radius: 5px;
- padding: 5px 0px;
- margin-top: 10px;
- align-items: center;
- font-size: 35px;
- font-weight: 100;
- cursor: pointer;
- }
- .bt {
- display: flex;
- align-items: center;
- justify-content: space-around;
- }
- input {
- border: none;
- padding: 0px;
- border-radius: 5px;
- box-shadow: none;
- }
- .config_file {
- display: none;
- }
- @media (max-width: 768px) {
-
- .option_box {
- width: 95%;
-
- }
- .option_box div {
- padding: 5px;
- font-size: 14px; /* 修改按钮字体大小 */
- }
-
- .rule_list_box {
- width: 95%;
- }
- .rule_list_box .show_box .show_left {
- width: 70%;
- }
- .rule_list_box .eidt_box .eidt_left {
- width: 70%;
- }
- .rule_list_box .option_box div {
- padding: 3px; /* 修改按钮内边距 */
- font-size: 12px; /* 修改按钮字体大小 */
-
- }
- .seting_box {
- width: 90%; /* 容器宽度为视口宽度的90% */
- max-height: 80vh; /* 最大高度为视口高度的80% */
- top: 10%; /* 顶部距离为视口高度的10% */
- bottom: 10%; /* 底部距离为视口高度的10% */
- transform: translate(-50%, 0); /* 居中显示 */
- left: 50%; /* 水平居中 */
- z-index: 10000; /* 设置一个较高的层叠顺序 */
- }
-
- }
-
-
- `
- // 开发用
- static devCss() {
- GM_xmlhttpRequest({
- method: "get",
- url: "http://127.0.0.1:1145",
- responseType: "blob",
- onload: (res) => {
- // console.log(res.responseText);
- GM_addStyle(res.responseText)
- },
- onerror: (error => {
- console.log("该页无法链接")
- })
- })
- }
-
- static create() {
- // 获取根节点
- let seting_box = document.querySelector("#mt_seting_box")
- seting_box.innerHTML = this.setingTemplate
-
-
- // 创建根节点样式
- GM_addStyle(this.setingStyle)
- // this.devCss()
-
- // 创建响应式ui
- const mt_Vue = new Vue({
- el: "#mt_seting_box",
- data() {
- return {
- rule: GM_getValue("key"),
- edit: this.addEdit(GM_getValue("key")),
- showSeting: false,
- config_add: false
- }
- },
-
- watch: {
- showSeting(n) {
- // console.log(22333);
- }
- },
-
- methods: {
- // 关闭设置
- close_seting() {
- this.showSeting = false
- },
-
- // 开启设置
- open_seting() {
- this.showSeting = true
- console.log(2233)
- },
- // 添加属性开关
- addEdit(rules) {
- let a = {}
- Object.keys(rules).forEach(key => {
- a[key] = false
- })
- return a
- },
-
- // 打开编辑
- editOn(key) {
- this.edit[key] = true
- },
-
- // 关闭编辑
- editOff(key) {
- this.edit[key] = false
- },
-
- // 颜色更新
- colorChange(key, color, textcolor) {
- document.querySelectorAll(`.mt_highlight[classesKey="${key}"]`).forEach(node => {
- node.style.background = color
- node.style.color = textcolor
- })
- // 保存到油猴中
- GM_setValue("key", this.rule)
- },
-
- // 更新规则
- ruleUpdate(key) {
- let newInfo = document.querySelector(`textarea[info_key=${key}]`).value
- let newWords = (document.querySelector(`textarea[words_key=${key}]`).value.split(" "))
- let newLimit = (document.querySelector(`textarea[limit_key=${key}]`).value.split(" "))
-
- // 去除空格
- newWords = Array.from(new Set(newWords))
- .filter(word => { return word != " " & word.length > 0 })
- newLimit = Array.from(new Set(newLimit))
- .filter(word => { return word != " " & word.length > 0 })
- // console.log(newInfo,newWords);
- this.rule[key].info = newInfo
- this.rule[key].words = newWords
- this.rule[key].limit = newLimit
-
- this.editOff(key)
-
- // 保存到油猴中
- GM_setValue("key", this.rule)
- },
-
- // 添加新规则
- add_key() {
- let key = "key_" + Math.floor(Math.random() * 10000000000)
- this.$set(this.rule, key, {
- info: "",
- words: [],
- color: "#dc6c75",
- textcolor: "#3467eb",
- limit: []
- })
-
- this.$set(this.edit, key, false)
-
- // 保存到油猴中
- GM_setValue("key", this.rule)
- console.log(2233)
- },
-
- // 删除规则
- del_key(key) {
- let ready = confirm("确认删除,该操作不可恢复")
-
- if (ready && Object.keys(this.rule).length > 1) {
- this.$delete(this.rule, key)
- this.$delete(this.edit, key)
- } else if (ready && Object.keys(this.rule).length < 2) {
- alert("至少保留一个")
- }
-
- // 保存到油猴中
- GM_setValue("key", this.rule)
- },
-
- // 复制到粘贴板
- copy() {
- navigator.clipboard.writeText(JSON.stringify(this.rules))
- },
-
- // 获取配置覆盖
- config_in() {
- document.querySelector(".config_file").click()
- this.config_add = false
- },
- // 获取配置添加
- config_in_add() {
- document.querySelector(".config_file").click()
- this.config_add = true
- },
-
- // 解析配置
- importFileJSON(ev) {
- return new Promise((resolve, reject) => {
- const fileDom = ev.target,
- file = fileDom.files[0]
-
- // 格式判断
- if (file.type !== 'application/json') {
- reject('仅允许上传json文件')
- }
- // 检验是否支持FileRender
- if (typeof FileReader === 'undefined') {
- reject('当前浏览器不支持FileReader')
- }
-
- // 执行后清空input的值,防止下次选择同一个文件不会触发onchange事件
- ev.target.value = ''
-
- // 执行读取json数据操作
- const reader = new FileReader()
- reader.readAsText(file) // 读取的结果还有其他读取方式,我认为text最为方便
-
- reader.onerror = (err) => {
- reject('json文件解析失败', err)
- }
-
- reader.onload = () => {
- const resultData = reader.result
- if (resultData) {
- try {
- const importData = JSON.parse(resultData)
- resolve(importData)
- } catch (error) {
- reject('读取数据解析失败', error)
- }
- } else {
- reject('读取数据解析失败', error)
- }
- }
- })
- },
-
- // 保存配置到本地
- file_read(e) {
- this.importFileJSON(e).then(res => {
- // 合并还是覆盖
- if (this.config_add) {
- let cache = {}
- Object.keys(GM_getValue("key")).forEach(key => {
- cache["key_" + Math.floor(Math.random() * 10000000000)] = GM_getValue("key")[key]
- })
- cache = { ...cache, ...res }
- console.log(cache)
-
- GM_setValue("key", cache)
- } else {
- GM_setValue("key", res)
- }
- initialize()
- this.rule = GM_getValue("key")
- this.edit = this.addEdit(res)
- })
- },
-
- // 导出配置
- config_out() {
- function exportJson(name, data) {
- let blob = new Blob([data]) // 创建 blob 对象
- let link = document.createElement("a")
- link.href = URL.createObjectURL(blob) // 创建一个 URL 对象并传给 a 的 href
- link.download = name // 设置下载的默认文件名
- link.click()
- }
-
- exportJson("mt_hight_light_config.json", JSON.stringify(this.rule))
-
- },
-
- // 刷新
- refresh() {
- location.reload()
- },
-
- },
-
- mounted() {
- GM_registerMenuCommand("打开设置", this.open_seting)
- // 点击其他区域关闭设置
- document.body.addEventListener("click", (e) => {
- // 检查是否是移动设备
- if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
- if (!document.querySelector("#mt_seting_box").contains(e.target)) {
- this.close_seting()
- }
- }
- })
-
- },
- })
-
- }
- }
-
-
- ///////////////////////////////////////////////////////////
- // vue根节点
- let seting_box = document.createElement("div") // 创建一个节点
- seting_box.setAttribute("id", "mt_seting_box") // 设置一个属性
- document.body.appendChild(seting_box)
-
- GM_addStyle(HIGHTLIGHT.highlightStyle)
-
- // 初始化数据
- initialize()
- console.log(GM_getValue("key"))
-
- // 静态页面的检测
- let nodeMap = textMap(document.body)
- // console.log(nodeMap);
- HIGHTLIGHT.highlight(nodeMap)
- nodeMap.clear()
-
- // 减少节点的重复检测
- let doOnce = ((wait) => {
- let timer = null
- // 存储动态更新的节点
- let elMap = new Map
-
- return (el) => {
- // 添加节点
- elMap.set(el)
- if (!timer) {
- timer = setTimeout(() => {
- // console.log(elMap);
- elMap.forEach((value, el) => {
- setTimeout(() => {
-
- let nodeMap = textMap(el)
- HIGHTLIGHT.highlight(nodeMap)
- nodeMap = null
-
- }, 1)
- })
- elMap.clear()
- timer = null
- }, wait)
- }
- }
- })(100)
- // 动态更新内容的检测
- watch()
-
-
- // 创建ui
- GUI.create()
- }
- )()