Fanfiction.net Unwanted Result Filter

Make up for how limited Fanfiction.net's result filtering is

当前为 2015-11-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Fanfiction.net Unwanted Result Filter
  3. // @namespace http://www.ficfan.org/
  4. // @description Make up for how limited Fanfiction.net's result filtering is
  5. // compared to sites like Twisting the Hellmouth.
  6. // @copyright 2014-2015, Stephan Sokolow (http://www.ssokolow.com/)
  7. // @license MIT; http://www.opensource.org/licenses/mit-license.php
  8. // @version 0.1.0
  9. //
  10. // @require http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.2.min.js
  11. // @require https://greasyfork.org/scripts/2855-gm-config/code/GM_config.js?version=33973
  12. //
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_log
  17. // @noframes
  18. //
  19. // @match *://www.fanfiction.net/*
  20. // ==/UserScript==
  21. (function($) {
  22.  
  23. /* NOTE TO USERS OF VERSION 0.0.1:
  24. * Sorry for erasing any modifications you made to the filter settings.
  25. *
  26. * I didn't put much thought into version 0.0.1 and, as a result, it was
  27. * impossible to release an update without erasing modifications.
  28. *
  29. * I'm now storing settings via GM_setValue, so it should never happen again
  30. * and there's a proper configuration GUI available at
  31. * Greasemonkey > User Script Commands... > Configure Result Filter...
  32. */
  33.  
  34. // ----==== Configuration ====----
  35.  
  36. var fieldDefs = {
  37. 'filter_slash': {
  38. 'section': ['Slash Filter',
  39. 'Hide stories with slash based on warnings in story descriptions'],
  40. 'label': 'Enabled',
  41. 'labelPos': 'right',
  42. 'type': 'checkbox',
  43. 'default': true,
  44. },
  45. 'hide_slash': {
  46. 'label': 'No Placeholder',
  47. 'labelPos': 'right',
  48. 'type': 'checkbox',
  49. 'default': false,
  50. },
  51. 'slash_pat': {
  52. 'label': 'Slash is...',
  53. 'title': 'A regular expression which matches slash in descriptions',
  54. 'type': 'text',
  55. 'size': 255,
  56. 'default': ".*(slash|yaoi)([.,!: ]|$)"
  57. },
  58. 'not_slash_pat': {
  59. 'label': '...but not...',
  60. 'title': 'A regular expression which matches "not slash" and so on',
  61. 'type': 'text',
  62. 'size': 255,
  63. 'default': ".*(fem|not?[ ]+)(slash|yaoi)([.,!: ]|$)"
  64. },
  65. 'filter_cats': {
  66. 'section': ['Unwanted Category Filter',
  67. 'Hide unwanted fandoms in author pages and "All Crossovers" searches'],
  68. 'label': 'Enabled',
  69. 'labelPos': 'right',
  70. 'type': 'checkbox',
  71. 'default': true,
  72. },
  73. 'hide_cats': {
  74. 'label': 'No Placeholder',
  75. 'labelPos': 'right',
  76. 'type': 'checkbox',
  77. 'default': false,
  78. },
  79. 'unwanted_cats': {
  80. 'label': 'Unwanted categories (One per line, blanks lines and lines ' +
  81. 'beginning with # will be ignored):',
  82. 'title': 'One entry per line',
  83. 'type': 'textarea',
  84. 'size': 100,
  85. 'default': [
  86. "# --== Never wanted ==-- ",
  87. "Invader Zim",
  88. "Supernatural",
  89. "Twilight",
  90. "",
  91. "# --== Not right now ==-- ",
  92. "Harry Potter & Avengers",
  93. "Naruto",
  94. ].join("\n"),
  95. },
  96. 'unwanted_cats_escape': {
  97. 'label': 'Lines are literal strings (uncheck for regular expressions)',
  98. 'labelPos': 'right',
  99. 'title': 'NOTE: Leading/trailing whitespace is always ignored and ' +
  100. 'newlines always have OR behaviour.',
  101. 'type': 'checkbox',
  102. 'default': true,
  103. },
  104. 'unwanted_cats_commute': {
  105. 'label': 'Automatically generate "B & A" lines from "A & B" lines',
  106. 'labelPos': 'right',
  107. 'title': "WARNING: This will break regexes with & inside () or []",
  108. 'type': 'checkbox',
  109. 'default': true,
  110. }
  111. };
  112.  
  113. var config_params = {
  114. 'id': 'ffnet_result_filter',
  115. 'title': 'Fanfiction.net Unwanted Result Filter',
  116. 'fields': fieldDefs,
  117. 'css': (
  118. // Match Fanfiction.net styling
  119. ".section_header { background-color: #339 !important; }\n" +
  120. ".section_desc { background-color: #f6f7ee !important; }\n" +
  121. // Form layout fixes
  122. "input[type=text], textarea { width: 99%; resize: vertical; }\n" +
  123. "input[type=checkbox] { vertical-align: middle; }\n" +
  124. "#ffnet_result_filter_filter_slash_var, " +
  125. "#ffnet_result_filter_filter_cats_var, " +
  126. "#ffnet_result_filter_hide_slash_var, " +
  127. "#ffnet_result_filter_hide_cats_var " +
  128. " { display: inline-block; margin-right: 1em !important; } " +
  129. "#ffnet_result_filter_field_unwanted_cats { min-height: 10em; }"),
  130. 'events': {
  131. 'open': function(doc) {
  132. $(this.frame).css('border-color', '#d4d4d4');
  133. $("<div>", {'id': 'gm_modal_back'}).css({
  134. position: 'fixed',
  135. top: 0,
  136. left: 0,
  137. width: '100%',
  138. height: '100%',
  139. background: 'black',
  140. opacity: 0.15,
  141. }).click(function() {
  142. GM_config.close();
  143. }).appendTo('body');
  144. },
  145. 'close': function() {
  146. $('#gm_modal_back').remove();
  147. }
  148. }
  149. };
  150.  
  151. // ----==== Functions ====----
  152.  
  153. /// Escape string for literal meaning inside a regular expression
  154. /// Source: http://stackoverflow.com/a/3561711/435253
  155. var re_escape = function(s) {
  156. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  157. };
  158.  
  159. /// Use with Array.filter() to remove comments and blank lines
  160. var rows_filter = function(elem, idx, arr) {
  161. elem = elem.trim()
  162. if (elem.charAt(0) == '#' || !elem) {
  163. return false;
  164. }
  165. return true;
  166. };
  167.  
  168. /// Parse an array from a newline-delimited string containing # comments
  169. var parse_lines = function(s) {
  170. return s.split("\n").map(function(e, i, a) {
  171. return e.trim();
  172. }).filter(rows_filter);
  173. };
  174.  
  175. /// Parse a usable list of category patterns from a raw string
  176. var parse_cats_list = function(s, escape, commute) {
  177. // Parse the config
  178. var cats_raw = parse_lines(s);
  179. if (escape) { cats_raw = cats_raw.map(re_escape) }
  180. if (commute) {
  181. var cats_out = [];
  182. for (var i = 0, len = cats_raw.length; i < len; i++) {
  183. var line = cats_raw[i];
  184. cats_out.push(line);
  185. var parts = line.split(' & ');
  186. if (parts.length > 1) {
  187. cats_out.push(parts[1] + ' & ' + parts[0]);
  188. }
  189. }
  190. }
  191. return cats_out;
  192. }
  193.  
  194. /// Hide a story entry in a way it can be recovered
  195. var hide_entry = function(node) {
  196. $(node).addClass('filtered').hide();
  197. };
  198.  
  199. /// Hide a story entry and add a clickable placeholder
  200. var add_placeholder = function(node, reason) {
  201. var $story = $(node);
  202. var $placeholder = $story.clone();
  203.  
  204. $placeholder.html("Click to Show (" + reason + ")").css({
  205. minHeight: 0,
  206. maxHeight: '1em',
  207. color: 'lightgray',
  208. textAlign: 'center',
  209. cursor: 'pointer',
  210. }).click($story, function(e) {
  211. $(this).slideUp();
  212. e.data.css('min-height', 0).slideDown();
  213. }).addClass('filter_placeholder').insertBefore($story);
  214. hide_entry($story);
  215. };
  216.  
  217. /// Code which must be re-run to reapply filters after changing the config
  218. var initialize_filters_and_apply = function() {
  219. // Parse the config
  220. var bad_cats = parse_cats_list(
  221. GM_config.get('unwanted_cats'),
  222. GM_config.get('unwanted_cats_escape'),
  223. GM_config.get('unwanted_cats_commute')
  224. );
  225.  
  226. // Generate RegExp objects from the parsed config
  227. var slash_re = new RegExp(GM_config.get('slash_pat'), 'i');
  228. var not_slash_re = new RegExp(GM_config.get('not_slash_pat'), 'i');
  229. var cats_re = new RegExp(".*(" + bad_cats.join('|') + ').*Rated:.*');
  230.  
  231.  
  232. // Clean up after any previous run
  233. $(".filter_placeholder").remove();
  234. $(".filtered").show();
  235.  
  236. var results = $(".z-list");
  237. for (var i = 0, reslen = results.length; i < reslen; i++) {
  238. var story = results[i];
  239. var meta_row = $('.xgray', story).text();
  240. var description = $('.z-padtop', story).contents()[0].data;
  241.  
  242. // TODO: Redesign to collapse runs of hidden entries
  243. var reason = null;
  244. if (GM_config.get('filter_slash') && slash_re.test(description)
  245. && !not_slash_re.test(description)) {
  246. if (GM_config.get('hide_slash')) {
  247. hide_entry(story);
  248. } else {
  249. add_placeholder(story, "Slash");
  250. }
  251. } else if (GM_config.get('filter_cats')) {
  252. var matches = meta_row.match(cats_re);
  253. if (matches && matches.length > 0) {
  254. if (GM_config.get('hide_cats')) {
  255. hide_entry(story);
  256. } else {
  257. add_placeholder(story, matches[1]);
  258. }
  259. }
  260. }
  261. }
  262. };
  263.  
  264. // ----==== Begin Main Program ====----
  265.  
  266. // Stuff which either must or need only be called once
  267. GM_config.init(config_params);
  268. GM_config.onChange = initialize_filters_and_apply;
  269. GM_registerMenuCommand("Configure Result Filter...",
  270. function() { GM_config.open(); }, 'C');
  271.  
  272. // Clear out ad box which misaligns "Hidden" message if it's first result
  273. $($('#content_wrapper_inner ins.adsbygoogle').parent()[0]).remove();
  274.  
  275. initialize_filters_and_apply();
  276.  
  277. }).call(this, jQuery);