您需要先安装一个扩展,例如 篡改猴、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.'); })();