// ==UserScript==
// @name AO3 关键词检测&折叠 | AO3 Keyword Detection & Collapse
// @author ghostupload
// @namespace urovom@游快贴贴
// @version 0.6
// @description 在指定位置检测关键词,折叠对应内容,并显示相应代替语句。v0.6支持开关大小写匹配、支持关键词正则表达式。
// @match https://archiveofourown.org/*
// @icon https://vi.ag925.top/download/ao3_content_filter_64x.ico
// @license MIT
// @grant none
// @AO3publish https://archiveofourown.org/chapters/148721791
// ==/UserScript==
(function() {
'use strict';
// !!!请自定义修改此处折叠规则!!!
// 自定义:检测位置(type)、关键词(keywords)、提示词(replace)、提示词颜色(color)、大小写检测(caseCheck)。
// 检测位置:
// 用户名(author)、作品标题(title)、所有标签(tag)、角色标签(character)、关系标签(relationship)、作品摘要(summary)。
// 其中用户名为精准匹配,可设置白名单
// 关键词:
// 支持部分正则表达式,如'Original \\w+ Character' 将匹配任何 'Original ... Character'
// 大小写检测:
// caseCheck设置为true则匹配大小写,设置为false则无视大小写进行匹配。
// ===============【规则】===============
const filterRules = [
{ type:'author', keywords:['用户名A','用户名B'], replace:'已屏蔽用户', color:'#FF0000', caseCheck:'true' },
{ type:'title', keywords:['甲乙','乙甲'], replace:'甲乙甲', color:'#AA3333', caseCheck:'true' },
{ type:'tag', keywords:['Everyone loves'], replace:'无视大小写的万人迷tag!', color:'#AA00AA', caseCheck:'false' },
{ type:'character', keywords:['Original Character','Original \\w+ Character'], replace:'任何原创角色', color:'#3333AA', caseCheck:'false' },
{ type:'relationship', keywords:['Original Character','Original \\w+ Character'], replace:'任何原创角色参与CP', color:'#AA3333', caseCheck:'false' },
{ type:'summary', keywords:['甲乙','乙甲'], replace:'甲乙甲', color:'#AA3333', caseCheck:'true' },
{ type:'author', keywords:['ghostupload'], replace:'一只野生的插件作者', color:'#238080', caseCheck:'true' },
];
// ==========================================
// =============【用户名白名单】=============
// 默认跳过检测
const excludeAuthors = ['ghostupload', 'ghostuploader'];
// ==========================================
function filterContent() {
const works = document.querySelectorAll('.blurb');
works.forEach(work => {
//检查用户名是否在白名单中
const authorLink = work.querySelector('a[rel="author"]');
const authorHref = authorLink ? authorLink.getAttribute('href') : '';
if (excludeAuthors.some(author => authorHref.includes(`/${author}/`))) {
return;
}
let replaceTexts = [];
let found = new Set();
const originalContent = {
header: work.querySelector('.header.module')?.outerHTML || '',
tags: work.querySelector('.tags.commas')?.outerHTML || '',
summary: work.querySelector('.userstuff.summary')?.outerHTML || '',
stats: work.querySelector('.stats')?.outerHTML || '',
};
// 读取语言信息
const languageElement = work.querySelector('.stats .language + dd.language');
const languageText = languageElement ? languageElement.innerText : '';
const languageDisplay = createLanguageInfo('Language: ', languageText, '#AAAAAA');
filterRules.forEach(rule => {
let keywords = rule.keywords;
if (rule.type !== 'author') {
rule.keywords = rule.keywords.map(keyword => {
if (rule.caseCheck === 'false') {
keyword = new RegExp(keyword, 'i');
} else {
keyword = new RegExp(keyword);
}
return keyword;
});
}
if (found.has(rule.replace)) return;
switch (rule.type) {
case 'author': {
if (authorHref && rule.keywords.some(keyword => authorHref.includes(`/${keyword}/`))) {
replaceTexts.push(createReplaceText('Author: ', rule.replace, rule.color));
found.add(rule.replace);
}
break;
}
case 'title': {
const titleText = work.querySelector('h4.heading a');
if (titleText && rule.keywords.some(keyword => keyword.test(titleText.innerText))) {
replaceTexts.push(createReplaceText('Title: ', rule.replace, rule.color));
found.add(rule.replace);
return;
}
break;
}
case 'tag': {
const tagsAll = work.querySelectorAll('.tags .tag');
for (const tag of tagsAll) {
if (rule.keywords.some(keyword => keyword.test(tag.innerText))) {
replaceTexts.push(createReplaceText('Tags: ', rule.replace, rule.color));
found.add(rule.replace);
return;
}
}
break;
}
case 'character': {
const tagsChara = work.querySelectorAll('.tags .characters .tag');
for (const tag of tagsChara) {
if (rule.keywords.some(keyword => keyword.test(tag.innerText))) {
replaceTexts.push(createReplaceText('Characters: ', rule.replace, rule.color));
found.add(rule.replace);
return;
}
}
break;
}
case 'relationship': {
const tagsRelation = work.querySelectorAll('.tags .relationships .tag');
for (const tag of tagsRelation) {
if (rule.keywords.some(keyword => keyword.test(tag.innerText))) {
replaceTexts.push(createReplaceText('Relationships: ', rule.replace, rule.color));
found.add(rule.replace);
return;
}
}
break;
}
case 'summary': {
const summaryText = work.querySelector('.userstuff.summary')?.innerText;
if (summaryText && rule.keywords.some(keyword => keyword.test(summaryText))) {
replaceTexts.push(createReplaceText('Summary: ', rule.replace, rule.color));
found.add(rule.replace);
return;
}
break;
}
}
});
if (replaceTexts.length > 0) {
work.innerHTML = '';
replaceTexts.forEach(text => {
work.appendChild(text);
});
// 添加语言信息
if (languageText) {
work.appendChild(languageDisplay);
}
// 添加按钮
const buttonContainer = document.createElement('div');
buttonContainer.style.margin = '1em 0.5em 0.5em';
const moreButton = document.createElement('span');
moreButton.innerText = 'more';
moreButton.style.color = '#CCCCCC';
moreButton.style.fontWeight = 'bold';
moreButton.style.cursor = 'pointer';
moreButton.style.display = 'block';
buttonContainer.appendChild(moreButton);
const buttonGroup = document.createElement('div');
buttonGroup.style.display = 'none';
buttonGroup.style.marginTop = '0.5em';
const buttons = [
{ text: 'Header', content: originalContent.header },
{ text: 'Tags', content: originalContent.tags },
{ text: 'Summary', content: originalContent.summary },
{ text: 'Stats', content: originalContent.stats },
{ text: 'Show All', content: originalContent.header + originalContent.tags + originalContent.summary + originalContent.stats },
];
buttons.forEach(buttonInfo => {
const button = document.createElement('button');
button.innerText = buttonInfo.text;
button.style.width = '5em';
button.style.margin = '0.2em';
button.style.border = '1px solid #808080';
button.style.padding = '2px';
button.style.cursor = 'pointer';
button.style.borderRadius = '0';
button.style.background = '#F6F6FF';
button.addEventListener('click', () => {
const existingContent = work.querySelector('.original-content');
if (existingContent && existingContent.innerHTML === buttonInfo.content) {
existingContent.remove();
} else {
if (existingContent) {
existingContent.remove();
}
if (buttonInfo.content) {
const contentDiv = document.createElement('div');
contentDiv.className = 'original-content';
contentDiv.innerHTML = buttonInfo.content;
work.appendChild(contentDiv);
}
}
});
buttonGroup.appendChild(button);
});
moreButton.addEventListener('click', () => {
if (buttonGroup.style.display === 'none') {
buttonGroup.style.display = 'block';
} else {
buttonGroup.style.display = 'none';
const existingContents = work.querySelectorAll('.original-content');
existingContents.forEach(content => content.remove());
}
});
buttonContainer.appendChild(buttonGroup);
work.appendChild(buttonContainer);
}
});
}
// 生成提示句
function createReplaceText(prefix, replace, color) {
const p = document.createElement('p');
p.style.margin = '1em 0 0.5em';
p.style.fontWeight = 'bold';
const prefixSpan = document.createElement('span');
prefixSpan.style.color = color;
prefixSpan.innerText = prefix;
const replaceSpan = document.createElement('span');
replaceSpan.style.color = color;
replaceSpan.innerText = replace;
p.appendChild(prefixSpan);
p.appendChild(replaceSpan);
return p;
}
// 显示语言
function createLanguageInfo(prefix, content, color) {
const p = document.createElement('p');
p.style.margin = '1em 0 0.5em';
p.style.fontWeight = 'bold';
p.style.color = color;
p.innerText = prefix + content;
return p;
}
// 当页面加载时执行过滤函数
window.addEventListener('load', filterContent);
})();