您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 V2EX 送码帖中,根据被评论区用户领取的激活码/邀请码,自动划掉主楼/附言中被提及的 Code。
当前为
- // ==UserScript==
- // @name V2EX Used Code Striker++
- // @namespace http://tampermonkey.net/
- // @version 1.3
- // @description 在 V2EX 送码帖中,根据被评论区用户领取的激活码/邀请码,自动划掉主楼/附言中被提及的 Code。
- // @author 与Gemini协作完成
- // @match https://www.v2ex.com/t/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_registerMenuCommand
- // @license MIT
- // ==/UserScript==
- (function() {
- 'use strict';
- // --- Configuration ---
- const STORAGE_KEY_KEYWORDS = 'v2ex_used_code_striker_keywords';
- const STORAGE_KEY_SHOW_USER = 'v2ex_used_code_striker_show_user';
- const defaultUsedKeywords = ['用', 'used', 'taken', '领', 'redeem', 'thx', '感谢'];
- // --- Load Settings ---
- const savedKeywordsString = GM_getValue(STORAGE_KEY_KEYWORDS, defaultUsedKeywords.join(','));
- const showUserInfoEnabled = GM_getValue(STORAGE_KEY_SHOW_USER, true); // Default to true (show user)
- let activeUsedKeywords = [];
- if (savedKeywordsString && savedKeywordsString.trim() !== '') {
- activeUsedKeywords = savedKeywordsString.split(',').map(kw => kw.trim()).filter(Boolean);
- }
- console.log('V2EX Used Code Striker: Active keywords:', activeUsedKeywords.length > 0 ? activeUsedKeywords : '(None - All comment codes considered used)');
- console.log('V2EX Used Code Striker: Show Username:', showUserInfoEnabled);
- // --- Regex & Style Setup ---
- const codeRegex = /(?:[A-Z0-9][-_]?){6,}/gi;
- const usedStyle = 'text-decoration: line-through; color: grey;';
- const userInfoStyle = 'font-size: smaller; margin-left: 5px; color: #999; text-decoration: none;'; // Style for the user link
- const markedClass = 'v2ex-used-code-marked'; // Class for the strikethrough span
- const userInfoClass = 'v2ex-code-claimant'; // Class for the user link anchor
- let keywordRegexCombinedTest = (text) => false; // Default test function
- // Build keyword regex only if there are active keywords
- if (activeUsedKeywords.length > 0) {
- const wordCharRegex = /^[a-zA-Z0-9_]+$/;
- const englishKeywords = activeUsedKeywords.filter(kw => wordCharRegex.test(kw));
- const nonWordBoundaryKeywords = activeUsedKeywords.filter(kw => !wordCharRegex.test(kw));
- const regexParts = [];
- if (englishKeywords.length > 0) {
- const englishPattern = `\\b(${englishKeywords.join('|')})\\b`;
- const englishRegex = new RegExp(englishPattern, 'i');
- regexParts.push((text) => englishRegex.test(text));
- // console.log("V2EX Used Code Striker: English Keyword Regex:", englishRegex);
- }
- if (nonWordBoundaryKeywords.length > 0) {
- const escapedNonWordKeywords = nonWordBoundaryKeywords.map(kw =>
- kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- );
- const nonWordPattern = `(${escapedNonWordKeywords.join('|')})`;
- const nonWordRegex = new RegExp(nonWordPattern, 'i');
- regexParts.push((text) => nonWordRegex.test(text));
- // console.log("V2EX Used Code Striker: Non-Word-Boundary Keyword Regex:", nonWordRegex);
- }
- if (regexParts.length > 0) {
- keywordRegexCombinedTest = (text) => {
- for (const testFn of regexParts) {
- if (testFn(text)) return true;
- }
- return false;
- };
- }
- }
- // --- Menu Commands ---
- GM_registerMenuCommand('配置 V2EX 划掉Code关键词', () => {
- const currentKeywords = GM_getValue(STORAGE_KEY_KEYWORDS, defaultUsedKeywords.join(','));
- const newKeywordsString = prompt(
- '请输入评论中表示Code已使用的关键词,用英文逗号 (,) 分隔。\n\n' +
- '留空则表示评论中出现的所有Code都会被认为已使用。\n\n' +
- '当前配置:',
- currentKeywords
- );
- if (newKeywordsString !== null) { // Prompt wasn't cancelled
- const cleanedKeywords = newKeywordsString.trim();
- GM_setValue(STORAGE_KEY_KEYWORDS, cleanedKeywords);
- alert(
- '关键词已更新。\n' +
- `新配置: ${cleanedKeywords || '(空 - 所有评论Code都将被标记)'}\n\n` +
- '请刷新页面以应用更改。'
- );
- }
- });
- GM_registerMenuCommand(`切换显示/隐藏使用者信息 (${showUserInfoEnabled ? '当前: 显示' : '当前: 隐藏'})`, () => {
- const currentState = GM_getValue(STORAGE_KEY_SHOW_USER, true);
- const newState = !currentState;
- GM_setValue(STORAGE_KEY_SHOW_USER, newState);
- alert(
- `使用者信息显示已切换为: ${newState ? '显示' : '隐藏'}\n\n` +
- '请刷新页面以应用更改。'
- );
- });
- // --- Helper Function: findTextNodes (Unchanged) ---
- function findTextNodes(element, textNodes) {
- if (!element) return;
- for (const node of element.childNodes) {
- if (node.nodeType === Node.TEXT_NODE) {
- if (node.nodeValue.trim().length > 0) {
- textNodes.push(node);
- }
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- // Avoid recursing into already marked spans or user links
- if (!(node.tagName === 'SPAN' && node.classList.contains(markedClass)) &&
- !(node.tagName === 'A' && node.classList.contains(userInfoClass)))
- {
- if (node.tagName !== 'A' && node.tagName !== 'CODE') { // Avoid recursing into normal links/code blocks? Check if needed.
- findTextNodes(node, textNodes);
- } else {
- findTextNodes(node, textNodes); // Search inside A and CODE for text nodes too
- }
- }
- }
- }
- }
- // --- Main Logic ---
- console.log('V2EX Used Code Striker: Script running...');
- // 1. Extract used Codes and Claimant Info from comments
- const claimedCodeInfo = new Map(); // Map<string, { username: string, profileUrl: string }>
- const commentElements = document.querySelectorAll('div.cell[id^="r_"]'); // Select the whole comment cell
- console.log(`V2EX Used Code Striker: Found ${commentElements.length} comment cells.`);
- const keywordsAreActive = activeUsedKeywords.length > 0;
- commentElements.forEach((commentCell, index) => {
- const replyContentEl = commentCell.querySelector('.reply_content');
- const userLinkEl = commentCell.querySelector('strong > a[href^="/member/"]');
- if (!replyContentEl || !userLinkEl) {
- // console.warn(`V2EX Used Code Striker: Skipping comment cell ${index + 1}, missing content or user link.`);
- return; // Skip if structure is unexpected
- }
- const commentText = replyContentEl.textContent;
- const username = userLinkEl.textContent;
- const profileUrl = userLinkEl.href;
- const potentialCodes = commentText.match(codeRegex);
- if (potentialCodes) {
- let commentMatchesCriteria = false;
- if (!keywordsAreActive) {
- // Setting is empty: consider all codes in comments as used
- commentMatchesCriteria = true;
- } else {
- // Keywords are defined: check if comment contains keywords
- if (keywordRegexCombinedTest(commentText)) {
- commentMatchesCriteria = true;
- }
- }
- if (commentMatchesCriteria) {
- potentialCodes.forEach(code => {
- const codeUpper = code.toUpperCase();
- // Only store the *first* user claiming a specific code
- if (!claimedCodeInfo.has(codeUpper)) {
- console.log(`V2EX Used Code Striker: Found potential used code "${code}" by user "${username}" in comment ${index + 1}`);
- claimedCodeInfo.set(codeUpper, { username, profileUrl });
- }
- });
- }
- }
- });
- console.log(`V2EX Used Code Striker: Extracted info for ${claimedCodeInfo.size} unique potential used codes based on config:`, claimedCodeInfo);
- if (claimedCodeInfo.size === 0) {
- console.log('V2EX Used Code Striker: No potential used codes found in comments matching criteria. Exiting.');
- return;
- }
- // 2. Find and mark Codes in main post and supplements
- const contentAreas = [
- document.querySelector('.topic_content'), // Main post content
- ...document.querySelectorAll('.subtle .topic_content') // Supplement content (inside .markdown_body)
- ].filter(el => el); // Filter out nulls if no supplements
- console.log(`V2EX Used Code Striker: Found ${contentAreas.length} content areas to scan.`);
- contentAreas.forEach((area, areaIndex) => {
- const textNodes = [];
- findTextNodes(area, textNodes);
- textNodes.forEach(node => {
- // Check if the node is already inside a marked element (double check)
- if (node.parentNode && (node.parentNode.classList.contains(markedClass) || node.parentNode.classList.contains(userInfoClass))) {
- return;
- }
- const nodeText = node.nodeValue;
- let match;
- let lastIndex = 0;
- const newNodeContainer = document.createDocumentFragment();
- const regex = new RegExp(codeRegex.source, 'gi'); // Create new regex instance for each node
- regex.lastIndex = 0; // Reset lastIndex
- while ((match = regex.exec(nodeText)) !== null) {
- const matchedCode = match[0];
- const matchedCodeUpper = matchedCode.toUpperCase();
- if (claimedCodeInfo.has(matchedCodeUpper)) {
- const claimInfo = claimedCodeInfo.get(matchedCodeUpper);
- // Add text before the match
- if (match.index > lastIndex) {
- newNodeContainer.appendChild(document.createTextNode(nodeText.substring(lastIndex, match.index)));
- }
- // Create the strikethrough span for the code
- const span = document.createElement('span');
- span.textContent = matchedCode;
- span.style.cssText = usedStyle;
- span.title = `Code "${matchedCode}" likely used by ${claimInfo.username}`;
- span.classList.add(markedClass);
- newNodeContainer.appendChild(span);
- // Optionally, add the user info link
- if (showUserInfoEnabled && claimInfo) {
- const userLink = document.createElement('a');
- userLink.href = claimInfo.profileUrl;
- userLink.textContent = ` (@${claimInfo.username})`;
- userLink.style.cssText = userInfoStyle;
- userLink.classList.add(userInfoClass);
- userLink.target = '_blank'; // Open in new tab
- userLink.title = `View profile of ${claimInfo.username}`;
- newNodeContainer.appendChild(userLink);
- }
- lastIndex = regex.lastIndex;
- } else {
- // If code is not in the claimed map, ensure loop continues correctly.
- // regex.lastIndex is automatically advanced by exec().
- }
- }
- // Add any remaining text after the last match (or the whole text if no matches)
- if (lastIndex < nodeText.length) {
- newNodeContainer.appendChild(document.createTextNode(nodeText.substring(lastIndex)));
- }
- // Replace the original text node only if modifications were made
- if (newNodeContainer.hasChildNodes() && lastIndex > 0) { // lastIndex > 0 implies at least one match was processed
- node.parentNode.replaceChild(newNodeContainer, node);
- }
- });
- });
- console.log('V2EX Used Code Striker: Script finished.');
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址