AO3 关键词检测&折叠 | AO3 Keyword Detection & Collapse

在指定位置检测关键词,折叠对应内容,并显示相应代替语句。v0.6支持开关大小写匹配、支持关键词正则表达式。

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