// ==UserScript==
// @name highlight-keywords
// @namespace https://github.com/mudssky/highlight-keywords
// @version 2.0.4
// @author mudssky
// @description 高亮关键词,可设置关键词的样式,支持正则匹配
// @license MIT
// @icon https://vitejs.dev/logo.svg
// @homepage https://github.com/mudssky/highlight-keywords
// @homepageURL https://github.com/mudssky/highlight-keywords
// @supportURL https://github.com/mudssky/highlight-keywords/issues
// @match *://*/*
// @exclude *://element-plus*
// @require https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js
// @require data:application/javascript,window.Vue%3DVue%3B
// @require https://cdn.jsdelivr.net/npm/element-plus@2.3.14/dist/index.full.min.js
// @resource element-plus/dist/index.css https://cdn.jsdelivr.net/npm/element-plus@2.3.14/dist/index.css
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @grant GM_setValue
// @run-at document-end
// ==/UserScript==
(t=>{if(typeof GM_addStyle=="function"){GM_addStyle(t);return}const r=document.createElement("style");r.textContent=t,document.head.append(r)})(" .dialog-footer button[data-v-aead4c22]:first-child{margin-right:10px}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.min-h-\\[400px\\]{min-height:400px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)} ");
(function (vue, ElementPlus) {
'use strict';
var zhCn = {
name: "zh-cn",
el: {
colorpicker: {
confirm: "确定",
clear: "清空"
},
datepicker: {
now: "此刻",
today: "今天",
cancel: "取消",
clear: "清空",
confirm: "确定",
selectDate: "选择日期",
selectTime: "选择时间",
startDate: "开始日期",
startTime: "开始时间",
endDate: "结束日期",
endTime: "结束时间",
prevYear: "前一年",
nextYear: "后一年",
prevMonth: "上个月",
nextMonth: "下个月",
year: "年",
month1: "1 月",
month2: "2 月",
month3: "3 月",
month4: "4 月",
month5: "5 月",
month6: "6 月",
month7: "7 月",
month8: "8 月",
month9: "9 月",
month10: "10 月",
month11: "11 月",
month12: "12 月",
weeks: {
sun: "日",
mon: "一",
tue: "二",
wed: "三",
thu: "四",
fri: "五",
sat: "六"
},
months: {
jan: "一月",
feb: "二月",
mar: "三月",
apr: "四月",
may: "五月",
jun: "六月",
jul: "七月",
aug: "八月",
sep: "九月",
oct: "十月",
nov: "十一月",
dec: "十二月"
}
},
select: {
loading: "加载中",
noMatch: "无匹配数据",
noData: "无数据",
placeholder: "请选择"
},
cascader: {
noMatch: "无匹配数据",
loading: "加载中",
placeholder: "请选择",
noData: "暂无数据"
},
pagination: {
goto: "前往",
pagesize: "条/页",
total: "共 {total} 条",
pageClassifier: "页",
page: "页",
prev: "上一页",
next: "下一页",
currentPage: "第 {pager} 页",
prevPages: "向前 {pager} 页",
nextPages: "向后 {pager} 页",
deprecationWarning: "你使用了一些已被废弃的用法,请参考 el-pagination 的官方文档"
},
messagebox: {
title: "提示",
confirm: "确定",
cancel: "取消",
error: "输入的数据不合法!"
},
upload: {
deleteTip: "按 delete 键可删除",
delete: "删除",
preview: "查看图片",
continue: "继续上传"
},
table: {
emptyText: "暂无数据",
confirmFilter: "筛选",
resetFilter: "重置",
clearFilter: "全部",
sumText: "合计"
},
tree: {
emptyText: "暂无数据"
},
transfer: {
noMatch: "无匹配数据",
noData: "无数据",
titles: ["列表 1", "列表 2"],
filterPlaceholder: "请输入搜索内容",
noCheckedFormat: "共 {total} 项",
hasCheckedFormat: "已选 {checked}/{total} 项"
},
image: {
error: "加载失败"
},
pageHeader: {
title: "返回"
},
popconfirm: {
confirmButtonText: "确定",
cancelButtonText: "取消"
}
}
};
var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
var _GM_setClipboard = /* @__PURE__ */ (() => typeof GM_setClipboard != "undefined" ? GM_setClipboard : void 0)();
var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
function highlightKeyword(node, pattern, index) {
var _a;
let exposeCount = 0;
if (node.nodeType === Node.TEXT_NODE && node instanceof Text) {
const matchResult = node.data.match(pattern);
if (matchResult) {
const highlightEl = document.createElement("span");
highlightEl.dataset.highlight = "yes";
highlightEl.dataset.highlightMatch = matchResult[0];
if (index ?? false) {
highlightEl.dataset.highlightIndex = index;
}
const matchNode = node.splitText((matchResult == null ? void 0 : matchResult.index) ?? 0);
matchNode.splitText(matchResult[0].length);
var highlightTextNode = document.createTextNode(matchNode.data);
highlightEl.appendChild(highlightTextNode);
(_a = matchNode.parentNode) == null ? void 0 : _a.replaceChild(highlightEl, matchNode);
exposeCount++;
}
} else if (node.nodeType === Node.ELEMENT_NODE && !/script|style/.test(node.tagName.toLowerCase())) {
if (node.dataset.highlight === "yes") {
if ((index ?? null) === null) {
return;
}
if (node.dataset.highlightIndex === (index ?? "").toString()) {
return;
}
}
let childNodes = node.childNodes;
for (var i = 0; i < childNodes.length; i++) {
highlightKeyword(childNodes[i], pattern, index);
}
}
return exposeCount;
}
function closeHighlight(pattern, index = null) {
var highlightNodeList = document.querySelectorAll("[data-highlight=yes]");
for (var n = 0; n < highlightNodeList.length; n++) {
const dataset = highlightNodeList[n].dataset;
if (index === null || dataset.highlightIndex !== index.toString()) {
return;
}
if (pattern.test(dataset.highlightMatch)) {
var parentNode = highlightNodeList[n].parentNode;
var childNodes = highlightNodeList[n].childNodes;
var childNodesLen = childNodes.length;
var nextSibling = highlightNodeList[n].nextSibling;
for (var k = 0; k < childNodesLen; k++) {
parentNode.insertBefore(childNodes[0], nextSibling);
}
var flagNode = document.createTextNode("");
parentNode.replaceChild(flagNode, highlightNodeList[n]);
parentNode.normalize();
}
}
}
function cleanKeywords(keywords) {
let wordMatchString = "";
const words = [...keywords];
words.forEach((item) => {
let transformString = item.replace(/[.[*?+^$|()/]|\]|\\/g, "\\$&");
wordMatchString += `|(${transformString})`;
});
wordMatchString = wordMatchString.substring(1);
const wholePattern = new RegExp(`^${wordMatchString}$`, "i");
const pattern = new RegExp(wordMatchString, "i");
return [pattern, wholePattern];
}
const configName = "hightlight-config";
const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
__name: "index",
setup(__props) {
const ruleFormRef = vue.ref();
const dialogVisible = vue.ref(false);
const ruleList = vue.ref([]);
const pageState = vue.reactive({
globalStyle: void 0
});
const form = vue.reactive({
configJson: "",
defaultHightlightStyle: "background:gold;color:black;",
highlightStyle: "background:gold;color:black;",
placeholder: `//示例:
[
{
"keywords": ["成年コミック"],
"matchUrl": "sukebei.nyaa.si",
},
]
`
});
const matchedRuleList = vue.computed(() => {
return ruleList.value.filter((rule) => {
var urlPattern = new RegExp(rule.matchUrl);
return urlPattern.test(window.location.href);
});
});
const matchedKeywords = vue.computed(() => {
const keywordsLists = matchedRuleList.value.map((item) => {
return item.keywords;
});
return [...new Set(keywordsLists.flat())];
});
function generateHighlightStyle(styleText) {
return `[data-highlight="yes"]{${styleText}}`;
}
function loadGlobalStyle() {
let style2 = document.createElement("style");
style2.textContent = generateHighlightStyle(form.defaultHightlightStyle);
document.head.appendChild(style2);
pageState.globalStyle = style2;
}
function updateHighlightStyle(styleText) {
if (pageState.globalStyle) {
pageState.globalStyle.textContent = generateHighlightStyle(styleText);
}
}
function handleCopyJson() {
_GM_setClipboard(form.configJson, "text");
}
function loadRuleList() {
const vv = _GM_getValue(configName, []);
ruleList.value = vv;
form.configJson = JSON.stringify(ruleList.value);
}
function handleOpenPanel() {
dialogVisible.value = true;
}
function handleClose() {
dialogVisible.value = false;
}
function validateConfig(configList) {
const res = [false, "配置项格式不对"];
if (!Array.isArray(configList)) {
return res;
}
if (configList.some((item) => {
return typeof item !== "object";
})) {
return res;
}
for (const property of ["keywords", "matchUrl"]) {
if (configList.some((item) => {
return !((item == null ? void 0 : item[property]) ?? false);
})) {
res[1] = `${property} 属性是必须的`;
return res;
}
}
for (const item of configList) {
if (typeof item.matchUrl !== "string") {
res[1] = "matchUrl类型错误";
return res;
}
if (!Array.isArray(item.keywords)) {
res[1] = "keywords类型错误";
return res;
}
for (const keyword of item.keywords) {
if (typeof keyword !== "string") {
res[1] = "keywords类型错误";
return res;
}
if (keyword.trim() === "") {
console.log("空字符串");
res[1] = "keywords不能为空";
return res;
}
}
}
return [true, res[1]];
}
async function handleUpdateConfig() {
var _a;
await ((_a = ruleFormRef.value) == null ? void 0 : _a.validate((valid2, fields) => {
if (valid2) {
console.log("submit!");
} else {
console.log("error submit!", fields);
}
}));
let list;
try {
list = JSON.parse(form.configJson);
} catch (error) {
ElementPlus.ElMessage({
type: "warning",
message: "json解析错误"
});
return;
}
const [valid, errorMessage] = validateConfig(list);
if (!valid) {
ElementPlus.ElMessage({
type: "warning",
message: errorMessage
});
return;
}
ruleList.value = list;
updateHighlightStyle(form.highlightStyle);
_GM_setValue(configName, list);
highlightMatchedKeywords();
ElementPlus.ElMessage({
type: "success",
message: "配置更新成功"
});
handleClose();
}
function highlightMatchedKeywords() {
if (matchedKeywords.value.length < 1) {
return;
}
const [pattern, _] = cleanKeywords(matchedKeywords.value);
closeHighlight(document.body, pattern);
highlightKeyword(document.body, pattern);
}
vue.onMounted(() => {
loadRuleList();
if (matchedKeywords.value.length > 0) {
loadGlobalStyle();
highlightMatchedKeywords();
}
_GM_registerMenuCommand("打开配置面板", handleOpenPanel);
});
return (_ctx, _cache) => {
const _component_el_input = vue.resolveComponent("el-input");
const _component_el_form_item = vue.resolveComponent("el-form-item");
const _component_el_form = vue.resolveComponent("el-form");
const _component_el_button = vue.resolveComponent("el-button");
const _component_el_space = vue.resolveComponent("el-space");
const _component_el_row = vue.resolveComponent("el-row");
const _component_el_dialog = vue.resolveComponent("el-dialog");
return vue.openBlock(), vue.createBlock(_component_el_dialog, {
modelValue: dialogVisible.value,
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => dialogVisible.value = $event),
title: "配置面板",
width: "30%",
"before-close": handleClose,
class: "min-h-[400px]"
}, {
default: vue.withCtx(() => [
vue.createVNode(_component_el_form, {
model: form,
ref_key: "ruleFormRef",
ref: ruleFormRef
}, {
default: vue.withCtx(() => [
vue.createVNode(_component_el_form_item, {
label: "高亮样式",
prop: "highlightStyle",
rules: [
{
required: true,
whitespace: true,
message: "请输入高亮样式",
trigger: "change"
}
]
}, {
default: vue.withCtx(() => [
vue.createVNode(_component_el_input, {
modelValue: form.highlightStyle,
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => form.highlightStyle = $event)
}, null, 8, ["modelValue"])
]),
_: 1
}),
vue.createVNode(_component_el_form_item, { label: "配置" }, {
default: vue.withCtx(() => [
vue.createVNode(_component_el_input, {
modelValue: form.configJson,
"onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => form.configJson = $event),
placeholder: form.placeholder,
type: "textarea",
autosize: { minRows: 5, maxRows: 10 }
}, null, 8, ["modelValue", "placeholder"])
]),
_: 1
})
]),
_: 1
}, 8, ["model"]),
vue.createVNode(_component_el_row, { justify: "end" }, {
default: vue.withCtx(() => [
vue.createVNode(_component_el_space, null, {
default: vue.withCtx(() => [
vue.createVNode(_component_el_button, { onClick: handleCopyJson }, {
default: vue.withCtx(() => [
vue.createTextVNode("复制json")
]),
_: 1
}),
vue.createVNode(_component_el_button, {
onClick: handleUpdateConfig,
type: "primary"
}, {
default: vue.withCtx(() => [
vue.createTextVNode("更新配置")
]),
_: 1
})
]),
_: 1
})
]),
_: 1
})
]),
_: 1
}, 8, ["modelValue"]);
};
}
});
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
const app$1 = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-aead4c22"]]);
const cssLoader = (e) => {
const t = GM_getResourceText(e);
return GM_addStyle(t), t;
};
cssLoader("element-plus/dist/index.css");
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
__name: "App",
setup(__props) {
const config = vue.reactive({
zIndex: 100
});
return (_ctx, _cache) => {
return vue.openBlock(), vue.createBlock(vue.unref(ElementPlus.ElConfigProvider), {
locale: vue.unref(zhCn),
zIndex: config.zIndex
}, {
default: vue.withCtx(() => [
vue.createVNode(app$1)
]),
_: 1
}, 8, ["locale", "zIndex"]);
};
}
});
const app = vue.createApp(_sfc_main);
const appContainer = (() => {
const app2 = document.createElement("div");
document.documentElement.append(app2);
return app2;
})();
app.use(ElementPlus);
app.mount(appContainer);
})(Vue, ElementPlus);