V2EX Used Code Striker++

在 V2EX 送码帖中,根据被评论区用户领取的激活码/邀请码,自动划掉主楼/附言中被提及的 Code。

当前为 2025-04-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name V2EX Used Code Striker++
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @description 在 V2EX 送码帖中,根据被评论区用户领取的激活码/邀请码,自动划掉主楼/附言中被提及的 Code。
  6. // @author 与Gemini协作完成
  7. // @match https://www.v2ex.com/t/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // --- Configuration ---
  19. const STORAGE_KEY_KEYWORDS = 'v2ex_used_code_striker_keywords';
  20. const STORAGE_KEY_SHOW_USER = 'v2ex_used_code_striker_show_user';
  21. const defaultUsedKeywords = ['用', 'used', 'taken', '领', 'redeem', 'thx', '感谢'];
  22.  
  23. // --- Load Settings ---
  24. const savedKeywordsString = GM_getValue(STORAGE_KEY_KEYWORDS, defaultUsedKeywords.join(','));
  25. const showUserInfoEnabled = GM_getValue(STORAGE_KEY_SHOW_USER, true); // Default to true (show user)
  26.  
  27. let activeUsedKeywords = [];
  28. if (savedKeywordsString && savedKeywordsString.trim() !== '') {
  29. activeUsedKeywords = savedKeywordsString.split(',').map(kw => kw.trim()).filter(Boolean);
  30. }
  31.  
  32. console.log('V2EX Used Code Striker: Active keywords:', activeUsedKeywords.length > 0 ? activeUsedKeywords : '(None - All comment codes considered used)');
  33. console.log('V2EX Used Code Striker: Show Username:', showUserInfoEnabled);
  34.  
  35. // --- Regex & Style Setup ---
  36. const codeRegex = /(?:[A-Z0-9][-_]?){6,}/gi;
  37. const usedStyle = 'text-decoration: line-through; color: grey;';
  38. const userInfoStyle = 'font-size: smaller; margin-left: 5px; color: #999; text-decoration: none;'; // Style for the user link
  39. const markedClass = 'v2ex-used-code-marked'; // Class for the strikethrough span
  40. const userInfoClass = 'v2ex-code-claimant'; // Class for the user link anchor
  41.  
  42. let keywordRegexCombinedTest = (text) => false; // Default test function
  43.  
  44. // Build keyword regex only if there are active keywords
  45. if (activeUsedKeywords.length > 0) {
  46. const wordCharRegex = /^[a-zA-Z0-9_]+$/;
  47. const englishKeywords = activeUsedKeywords.filter(kw => wordCharRegex.test(kw));
  48. const nonWordBoundaryKeywords = activeUsedKeywords.filter(kw => !wordCharRegex.test(kw));
  49. const regexParts = [];
  50.  
  51. if (englishKeywords.length > 0) {
  52. const englishPattern = `\\b(${englishKeywords.join('|')})\\b`;
  53. const englishRegex = new RegExp(englishPattern, 'i');
  54. regexParts.push((text) => englishRegex.test(text));
  55. // console.log("V2EX Used Code Striker: English Keyword Regex:", englishRegex);
  56. }
  57.  
  58. if (nonWordBoundaryKeywords.length > 0) {
  59. const escapedNonWordKeywords = nonWordBoundaryKeywords.map(kw =>
  60. kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  61. );
  62. const nonWordPattern = `(${escapedNonWordKeywords.join('|')})`;
  63. const nonWordRegex = new RegExp(nonWordPattern, 'i');
  64. regexParts.push((text) => nonWordRegex.test(text));
  65. // console.log("V2EX Used Code Striker: Non-Word-Boundary Keyword Regex:", nonWordRegex);
  66. }
  67.  
  68. if (regexParts.length > 0) {
  69. keywordRegexCombinedTest = (text) => {
  70. for (const testFn of regexParts) {
  71. if (testFn(text)) return true;
  72. }
  73. return false;
  74. };
  75. }
  76. }
  77.  
  78. // --- Menu Commands ---
  79. GM_registerMenuCommand('配置 V2EX 划掉Code关键词', () => {
  80. const currentKeywords = GM_getValue(STORAGE_KEY_KEYWORDS, defaultUsedKeywords.join(','));
  81. const newKeywordsString = prompt(
  82. '请输入评论中表示Code已使用的关键词,用英文逗号 (,) 分隔。\n\n' +
  83. '留空则表示评论中出现的所有Code都会被认为已使用。\n\n' +
  84. '当前配置:',
  85. currentKeywords
  86. );
  87.  
  88. if (newKeywordsString !== null) { // Prompt wasn't cancelled
  89. const cleanedKeywords = newKeywordsString.trim();
  90. GM_setValue(STORAGE_KEY_KEYWORDS, cleanedKeywords);
  91. alert(
  92. '关键词已更新。\n' +
  93. `新配置: ${cleanedKeywords || '(空 - 所有评论Code都将被标记)'}\n\n` +
  94. '请刷新页面以应用更改。'
  95. );
  96. }
  97. });
  98.  
  99. GM_registerMenuCommand(`切换显示/隐藏使用者信息 (${showUserInfoEnabled ? '当前: 显示' : '当前: 隐藏'})`, () => {
  100. const currentState = GM_getValue(STORAGE_KEY_SHOW_USER, true);
  101. const newState = !currentState;
  102. GM_setValue(STORAGE_KEY_SHOW_USER, newState);
  103. alert(
  104. `使用者信息显示已切换为: ${newState ? '显示' : '隐藏'}\n\n` +
  105. '请刷新页面以应用更改。'
  106. );
  107. });
  108.  
  109.  
  110. // --- Helper Function: findTextNodes (Unchanged) ---
  111. function findTextNodes(element, textNodes) {
  112. if (!element) return;
  113. for (const node of element.childNodes) {
  114. if (node.nodeType === Node.TEXT_NODE) {
  115. if (node.nodeValue.trim().length > 0) {
  116. textNodes.push(node);
  117. }
  118. } else if (node.nodeType === Node.ELEMENT_NODE) {
  119. // Avoid recursing into already marked spans or user links
  120. if (!(node.tagName === 'SPAN' && node.classList.contains(markedClass)) &&
  121. !(node.tagName === 'A' && node.classList.contains(userInfoClass)))
  122. {
  123. if (node.tagName !== 'A' && node.tagName !== 'CODE') { // Avoid recursing into normal links/code blocks? Check if needed.
  124. findTextNodes(node, textNodes);
  125. } else {
  126. findTextNodes(node, textNodes); // Search inside A and CODE for text nodes too
  127. }
  128. }
  129. }
  130. }
  131. }
  132.  
  133. // --- Main Logic ---
  134. console.log('V2EX Used Code Striker: Script running...');
  135.  
  136. // 1. Extract used Codes and Claimant Info from comments
  137. const claimedCodeInfo = new Map(); // Map<string, { username: string, profileUrl: string }>
  138. const commentElements = document.querySelectorAll('div.cell[id^="r_"]'); // Select the whole comment cell
  139. console.log(`V2EX Used Code Striker: Found ${commentElements.length} comment cells.`);
  140.  
  141. const keywordsAreActive = activeUsedKeywords.length > 0;
  142.  
  143. commentElements.forEach((commentCell, index) => {
  144. const replyContentEl = commentCell.querySelector('.reply_content');
  145. const userLinkEl = commentCell.querySelector('strong > a[href^="/member/"]');
  146.  
  147. if (!replyContentEl || !userLinkEl) {
  148. // console.warn(`V2EX Used Code Striker: Skipping comment cell ${index + 1}, missing content or user link.`);
  149. return; // Skip if structure is unexpected
  150. }
  151.  
  152. const commentText = replyContentEl.textContent;
  153. const username = userLinkEl.textContent;
  154. const profileUrl = userLinkEl.href;
  155.  
  156. const potentialCodes = commentText.match(codeRegex);
  157.  
  158. if (potentialCodes) {
  159. let commentMatchesCriteria = false;
  160. if (!keywordsAreActive) {
  161. // Setting is empty: consider all codes in comments as used
  162. commentMatchesCriteria = true;
  163. } else {
  164. // Keywords are defined: check if comment contains keywords
  165. if (keywordRegexCombinedTest(commentText)) {
  166. commentMatchesCriteria = true;
  167. }
  168. }
  169.  
  170. if (commentMatchesCriteria) {
  171. potentialCodes.forEach(code => {
  172. const codeUpper = code.toUpperCase();
  173. // Only store the *first* user claiming a specific code
  174. if (!claimedCodeInfo.has(codeUpper)) {
  175. console.log(`V2EX Used Code Striker: Found potential used code "${code}" by user "${username}" in comment ${index + 1}`);
  176. claimedCodeInfo.set(codeUpper, { username, profileUrl });
  177. }
  178. });
  179. }
  180. }
  181. });
  182.  
  183. console.log(`V2EX Used Code Striker: Extracted info for ${claimedCodeInfo.size} unique potential used codes based on config:`, claimedCodeInfo);
  184.  
  185. if (claimedCodeInfo.size === 0) {
  186. console.log('V2EX Used Code Striker: No potential used codes found in comments matching criteria. Exiting.');
  187. return;
  188. }
  189.  
  190. // 2. Find and mark Codes in main post and supplements
  191. const contentAreas = [
  192. document.querySelector('.topic_content'), // Main post content
  193. ...document.querySelectorAll('.subtle .topic_content') // Supplement content (inside .markdown_body)
  194. ].filter(el => el); // Filter out nulls if no supplements
  195.  
  196. console.log(`V2EX Used Code Striker: Found ${contentAreas.length} content areas to scan.`);
  197.  
  198. contentAreas.forEach((area, areaIndex) => {
  199. const textNodes = [];
  200. findTextNodes(area, textNodes);
  201.  
  202. textNodes.forEach(node => {
  203. // Check if the node is already inside a marked element (double check)
  204. if (node.parentNode && (node.parentNode.classList.contains(markedClass) || node.parentNode.classList.contains(userInfoClass))) {
  205. return;
  206. }
  207.  
  208. const nodeText = node.nodeValue;
  209. let match;
  210. let lastIndex = 0;
  211. const newNodeContainer = document.createDocumentFragment();
  212. const regex = new RegExp(codeRegex.source, 'gi'); // Create new regex instance for each node
  213. regex.lastIndex = 0; // Reset lastIndex
  214.  
  215. while ((match = regex.exec(nodeText)) !== null) {
  216. const matchedCode = match[0];
  217. const matchedCodeUpper = matchedCode.toUpperCase();
  218.  
  219. if (claimedCodeInfo.has(matchedCodeUpper)) {
  220. const claimInfo = claimedCodeInfo.get(matchedCodeUpper);
  221.  
  222. // Add text before the match
  223. if (match.index > lastIndex) {
  224. newNodeContainer.appendChild(document.createTextNode(nodeText.substring(lastIndex, match.index)));
  225. }
  226.  
  227. // Create the strikethrough span for the code
  228. const span = document.createElement('span');
  229. span.textContent = matchedCode;
  230. span.style.cssText = usedStyle;
  231. span.title = `Code "${matchedCode}" likely used by ${claimInfo.username}`;
  232. span.classList.add(markedClass);
  233. newNodeContainer.appendChild(span);
  234.  
  235. // Optionally, add the user info link
  236. if (showUserInfoEnabled && claimInfo) {
  237. const userLink = document.createElement('a');
  238. userLink.href = claimInfo.profileUrl;
  239. userLink.textContent = ` (@${claimInfo.username})`;
  240. userLink.style.cssText = userInfoStyle;
  241. userLink.classList.add(userInfoClass);
  242. userLink.target = '_blank'; // Open in new tab
  243. userLink.title = `View profile of ${claimInfo.username}`;
  244. newNodeContainer.appendChild(userLink);
  245. }
  246.  
  247. lastIndex = regex.lastIndex;
  248.  
  249. } else {
  250. // If code is not in the claimed map, ensure loop continues correctly.
  251. // regex.lastIndex is automatically advanced by exec().
  252. }
  253. }
  254.  
  255. // Add any remaining text after the last match (or the whole text if no matches)
  256. if (lastIndex < nodeText.length) {
  257. newNodeContainer.appendChild(document.createTextNode(nodeText.substring(lastIndex)));
  258. }
  259.  
  260. // Replace the original text node only if modifications were made
  261. if (newNodeContainer.hasChildNodes() && lastIndex > 0) { // lastIndex > 0 implies at least one match was processed
  262. node.parentNode.replaceChild(newNodeContainer, node);
  263. }
  264. });
  265. });
  266.  
  267. console.log('V2EX Used Code Striker: Script finished.');
  268.  
  269. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址