Profanity Filter

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

  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 2018.10.10
  7. // @include http://*
  8. // @include https://*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13.  
  14. "use strict";
  15.  
  16.  
  17. // --- SETTINGS --------
  18.  
  19.  
  20. // The string that replaces offending words.
  21. const replaceString = "*bleep*";
  22.  
  23. // If useCustomWords is true, then customWords is used as the word list and the default list will not be used. Otherwise, it uses a pre-compiled version of the default list for performance.
  24. // The words list does not have to include endings like plurals or "ing", as they will always be handled.
  25. // The default list is: ['fuck','shit','ass','damn','asshole','bullshit','bitch','piss','goddamn','crap','sh!t','bastard','dumbass','fag','motherfuck','nigger','cunt','douche','douchebag','jackass','mothafuck','pissoff','shitfull','fuk','fuckme','fucktard','fvck','fcuk','b!tch','phuq','phuk','phuck','fatass','faggot','dipshit','fagot','faggit','fagget','assfuck','buttfuck','asswipe','asskiss','assclown']
  26. // This should be ordered by most common first for performance, and must only contain alpha-numeric (unless you sanitize for regex)
  27. const useCustomWords = false;
  28. const customWords = [];
  29.  
  30. // Display performance and debugging information to the console.
  31. const DEBUG = false;
  32.  
  33.  
  34. // --------------------
  35.  
  36.  
  37. let wordString = useCustomWords ? "\\b(?:" + customWords.join("|") + ")[tgkp]??(?=(?:ing?(?:ess)??|ed|i??er|a)??(?:e??[syz])??\\b)" : "\\b(?:(?:f(?:u(?:ck(?:me|tard)??|k)|a(?:g(?:(?:g[eio]|o)t)??|tass)|(?:cu|vc)k)|b(?:u(?:llshit|ttfuck)|[!i]tch|astard)|ass(?:(?:hol|wip)e|clown|fuck|kiss)??|d(?:amn|umbass|ouche(?:bag)??|ipshit)|p(?:hu(?:c?k|q)|iss(?:off)??)|sh(?:it(?:full)??|!t)|moth(?:er|a)fuck|c(?:rap|unt)|goddamn|jackass|nigg))[tgkp]??(?=(?:ing?(?:ess)??|ed|i??er|a)??(?:e??[syz])??\\b)";
  38. const wordsFilter = new RegExp(wordString, "gi");
  39. wordString = null;
  40.  
  41. const findText = document.createExpression(".//text()[string-length() > 2 and not(parent::script or parent::code)]", null);
  42.  
  43.  
  44. // Initial slow filter pass that handles static text
  45. function filterStaticText()
  46. {
  47. let startTime, endTime;
  48. if (DEBUG)
  49. {
  50. startTime = performance.now();
  51. }
  52.  
  53. // Do title first because it is always visible
  54. if (wordsFilter.test(document.title))
  55. {
  56. document.title = document.title.replace(wordsFilter, replaceString);
  57. }
  58.  
  59. filterNodeTree(document.body);
  60.  
  61. if (DEBUG)
  62. {
  63. endTime = performance.now();
  64. console.log("PF | Static Text Run-Time (ms): " + (endTime - startTime).toString());
  65. }
  66. }
  67.  
  68.  
  69. // --------------------
  70.  
  71.  
  72. // filters dynamic text, and handles things such as AJAX Youtube comments
  73. function filterDynamicText()
  74. {
  75. let textMutationObserver = new MutationObserver(filterMutations);
  76. let TxMOInitOps = { characterData: true, childList: true, subtree: true };
  77. textMutationObserver.observe(document.body, TxMOInitOps);
  78.  
  79. let title = document.getElementsByTagName("title")[0];
  80. if (title)
  81. {
  82. let titleMutationObserver = new MutationObserver( function(mutations) { filterNode(title); } );
  83. let TiMOInitOps = { characterData: true, subtree: true };
  84. titleMutationObserver.observe(title, TiMOInitOps);
  85. }
  86. }
  87.  
  88.  
  89. // --------------------
  90.  
  91.  
  92. // Handler for mutation observer from filterDynamicText()
  93. function filterMutations(mutations)
  94. {
  95. let startTime, endTime;
  96. if (DEBUG)
  97. {
  98. startTime = performance.now();
  99. }
  100.  
  101. for (let i = 0; i < mutations.length; ++i)
  102. {
  103. let mutation = mutations[i];
  104.  
  105. if (mutation.type === "childList")
  106. {
  107. let nodes = mutation.addedNodes;
  108. for (let j = 0; j < nodes.length; ++j)
  109. {
  110. filterNodeTree(nodes[j]);
  111. }
  112. }
  113. else if (mutation.type === "characterData" && !mutation.target.parentNode.isContentEditable)
  114. {
  115. filterNode(mutation.target);
  116. }
  117. }
  118.  
  119. if (DEBUG)
  120. {
  121. endTime = performance.now();
  122. console.log("PF | Dynamic Text Run-Time (ms): " + (endTime - startTime).toString());
  123. }
  124. }
  125.  
  126.  
  127. // --------------------
  128.  
  129.  
  130. // Filters a textNode
  131. function filterNode(node)
  132. {
  133. if (wordsFilter.test(node.data))
  134. {
  135. node.data = node.data.replace(wordsFilter, replaceString);
  136. }
  137. }
  138.  
  139.  
  140. // --------------------
  141.  
  142.  
  143. // Filters all of the text from a node and its decendants
  144. function filterNodeTree(node)
  145. {
  146. if (!node || (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE))
  147. {
  148. return;
  149. }
  150.  
  151. if (node.nodeType === Node.TEXT_NODE)
  152. {
  153. filterNode(node);
  154. return; // text nodes don't have children
  155. }
  156.  
  157. let textNodes = findText.evaluate(node, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  158.  
  159. const l = textNodes.snapshotLength;
  160. for (let i = 0; i < l; ++i)
  161. {
  162. filterNode(textNodes.snapshotItem(i));
  163. }
  164. }
  165.  
  166.  
  167. // --------------------
  168.  
  169.  
  170. // Runs the different filter types
  171. function filterPage()
  172. {
  173. filterStaticText();
  174. filterDynamicText();
  175. }
  176.  
  177.  
  178. // --- MAIN -----------
  179.  
  180. filterPage();
  181.  
  182. })();