Profanity Filter

Simple filtering for profanity from website text. Not limited to static text, while avoiding performance impact.

目前為 2017-06-11 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Profanity Filter
  3. // @author adisib
  4. // @namespace namespace_adisib
  5. // @description Simple filtering for profanity from website text. Not limited to static text, while avoiding performance impact.
  6. // @version 2017.06.11
  7. // @include http://*
  8. // @include https://*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12.  
  13. (function() {
  14.  
  15. "use strict";
  16.  
  17.  
  18. // --- GLOBALS --------
  19.  
  20.  
  21. // Display performance and debugging information to the console.
  22. const DEBUG = false;
  23.  
  24.  
  25. // set replacement string
  26. const replaceString = "*bleep*";
  27.  
  28. // words to be filtered list
  29. // This should be ordered by most common first for performance (still TODO, but not important)
  30. // (also should probably be sanitized before dropping into regex)
  31. let words = ['fuck','shit','ass','damn','asshole','bullshit','bitch','piss','goddamn','crap','sh!t','bastard','dumbass','fag','motherfuck','nig','cunt','douche','douchebag','jackass','mothafuck','pissoff','shitfull','fuk','fuckme','fvck','fcuk','b!tch','phuq','phuk','phuck','fatass','faggot','dipshit','fagot','faggit','fagget','assfuck','buttfuck','asswipe','asskiss','assclown'];
  32.  
  33. // filters the words and any versions with optional endings
  34. // shouldn't run into issues with optional endings; a whitelist would be trivial to implement should it be required
  35. const wordsFilter = new RegExp("\\b(?:" + words.join("|") + ")[tgkp]??(?=(?:ing?(?:ess)??|ed|i??er|a)??(?:e??[syz])??\\b)", "gi");
  36.  
  37.  
  38. // --------------------
  39.  
  40.  
  41. // Initial slow filter pass that handles static text
  42. function filterStaticText()
  43. {
  44. let startTime, endTime;
  45. if (DEBUG)
  46. {
  47. startTime = performance.now();
  48. }
  49.  
  50. // Do title first because it is always visible
  51. if (wordsFilter.test(document.title))
  52. {
  53. document.title = document.title.replace(wordsFilter, replaceString);
  54. }
  55.  
  56. filterNodeTree(document.body);
  57.  
  58. if (DEBUG)
  59. {
  60. endTime = performance.now();
  61. console.log("PF | Static Text Run-Time (ms): " + (endTime - startTime).toString());
  62. }
  63. }
  64.  
  65.  
  66. // --------------------
  67.  
  68.  
  69. // filters dynamic text, and handles things such as AJAX Youtube comments
  70. function filterDynamicText()
  71. {
  72. let textMutationObserver = new MutationObserver(filterMutations);
  73. let TxMOInitOps = { characterData: true, childList: true, subtree: true };
  74. textMutationObserver.observe(document.body, TxMOInitOps);
  75.  
  76. let title = document.getElementsByTagName("title")[0];
  77. if (title)
  78. {
  79. let titleMutationObserver = new MutationObserver( function(mutations) { filterNode(title); } );
  80. let TiMOInitOps = { characterData: true, subtree: true };
  81. titleMutationObserver.observe(title, TiMOInitOps);
  82. }
  83. }
  84.  
  85.  
  86. // --------------------
  87.  
  88.  
  89. // Handler for mutation observer from filterDynamicText()
  90. function filterMutations(mutations)
  91. {
  92. let startTime, endTime;
  93. if (DEBUG)
  94. {
  95. startTime = performance.now();
  96. }
  97.  
  98. for (let i = 0; i < mutations.length; ++i)
  99. {
  100. let mutation = mutations[i];
  101.  
  102. if (mutation.type === "childList")
  103. {
  104. let nodes = mutation.addedNodes;
  105. for (let j = 0; j < nodes.length; ++j)
  106. {
  107. filterNodeTree(nodes[j]);
  108. }
  109. }
  110. else if (mutation.type === "characterData")
  111. {
  112. filterNode(mutation.target);
  113. }
  114. }
  115.  
  116. if (DEBUG)
  117. {
  118. endTime = performance.now();
  119. console.log("PF | Dynamic Text Run-Time (ms): " + (endTime - startTime).toString());
  120. }
  121. }
  122.  
  123.  
  124. // --------------------
  125.  
  126.  
  127. // Filters a textNode
  128. function filterNode(node)
  129. {
  130. if (wordsFilter.test(node.data) && !node.parentNode.isContentEditable)
  131. {
  132. node.data = node.data.replace(wordsFilter, replaceString);
  133. }
  134. }
  135.  
  136.  
  137. // --------------------
  138.  
  139.  
  140. // Filters all of the text from a node and its decendants
  141. function filterNodeTree(node)
  142. {
  143. if (!node || (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE))
  144. {
  145. return;
  146. }
  147.  
  148. if (node.nodeType === Node.TEXT_NODE)
  149. {
  150. filterNode(node);
  151. return; // text nodes don't have children
  152. }
  153.  
  154. let textNodes = document.evaluate(".//text()[string-length() > 2 and not(parent::script or parent::noscript or parent::code)]", node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  155.  
  156. const l = textNodes.snapshotLength;
  157. for (let i = 0; i < l; ++i)
  158. {
  159. filterNode(textNodes.snapshotItem(i));
  160. }
  161. }
  162.  
  163.  
  164. // --------------------
  165.  
  166.  
  167. // Runs the different filter types
  168. function filterPage()
  169. {
  170. filterStaticText();
  171. filterDynamicText();
  172. words = null;
  173. }
  174.  
  175.  
  176. // --- MAIN -----------
  177.  
  178. filterPage();
  179.  
  180. })();