Greasy Fork

Shined up real nice.

Fanfiction.net Unwanted Result Filter

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

After trying this script, you can ask a question about it, review it, or report it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
// ==UserScript==
// @name Fanfiction.net Unwanted Result Filter
// @namespace   http://www.ficfan.org/
// @description Make up for how limited Fanfiction.net's result filtering is
//              compared to sites like Twisting the Hellmouth.
// @copyright   2014-2015, Stephan Sokolow (http://www.ssokolow.com/)
// @license     MIT; http://www.opensource.org/licenses/mit-license.php
// @version     0.1.9.1
//
// @require     http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.2.min.js
// @require     https://openuserjs.org/src/libs/sizzle/GM_config.js
//
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_log
// @noframes
//
// @compatible  firefox Tested regularly under Greasemonkey.
// @compatible  chrome  Tested occasionally under Tampermonkey.
//
// @match       *://www.fanfiction.net/*
// ==/UserScript==
(function($) {

  /* NOTE TO USERS OF VERSION 0.0.1:
   * Sorry for erasing any modifications you made to the filter settings.
   *
   * I didn't put much thought into version 0.0.1 and, as a result, it was
   * impossible to release an update without erasing modifications.
   *
   * I'm now storing settings via GM_setValue, so it should never happen again
   * and there's a proper configuration GUI available at
   *   Greasemonkey > User Script Commands... > Configure Result Filter...
   */

  // ----==== Configuration ====----

  var IGNORED_OPACITY = 0.3;

  var fieldDefs = {
    'filter_slash': {
      'section': ['Slash Filter',
        'Hide stories with slash based on warnings in story descriptions'],
        'label': 'Enabled',
        'labelPos': 'right',
        'type': 'checkbox',
        'default': true,
    },
    'hide_slash': {
        'label': 'No Placeholder',
        'labelPos': 'right',
        'type': 'checkbox',
        'default': false,
    },
    'slash_pat': {
      'label': 'Slash is...',
      'title': 'A regular expression which matches slash in descriptions',
      'type': 'text',
      'size': 255,
      'default': ".*(slash|yaoi)([.,!:) -]|$)"
    },
    'not_slash_pat': {
      'label': '...but not...',
      'title': 'A regular expression which matches "not slash" and so on',
      'type': 'text',
      'size': 255,
      'default': ".*(fem|not?[ ]+)(slash|yaoi)([.,!: ]|$)"
    },
    'filter_cats': {
      'section': ['Unwanted Category Filter',
        'Hide unwanted fandoms in author pages and "All Crossovers" searches'],
        'label': 'Enabled',
        'labelPos': 'right',
        'type': 'checkbox',
        'default': true,
    },
    'hide_cats': {
        'label': 'No Placeholder',
        'labelPos': 'right',
        'type': 'checkbox',
        'default': false,
    },
    'unwanted_cats': {
      'label': 'Unwanted categories (One per line, blank lines and lines ' +
        'beginning with # will be ignored):',
        'type': 'textarea',
        'size': 100,
        'default': [
          "# --== Never wanted ==-- ",
          "Invader Zim",
          "Supernatural",
          "Twilight",
          "",
          "# --== Not right now ==-- ",
          "Harry Potter & Avengers",
          "Naruto",
        ].join("\n"),
    },
    'unwanted_cats_escape': {
      'label': 'Lines are literal strings (uncheck for regular expressions)',
      'labelPos': 'right',
      'title': 'NOTE: Leading/trailing whitespace is always ignored and ' +
               'newlines always have OR behaviour.',
      'type': 'checkbox',
      'default': true,
    },
    'unwanted_cats_commute': {
      'label': 'Automatically generate "B & A" lines from "A & B" lines',
      'labelPos': 'right',
      'title': "WARNING: This will break regexes with & inside () or []",
      'type': 'checkbox',
      'default': true,
    },
    'filter_manual': {
      'section': ['Manual Filter',
        'Hide an arbitrary list of story IDs'],
        'label': 'Enabled',
        'labelPos': 'right',
        'type': 'checkbox',
        'default': true,
    },
    'hide_manual': {
        'label': 'No Placeholder',
        'labelPos': 'right',
        'type': 'checkbox',
        'default': false,
    },
    'manual_reason': {
      'label': 'Reason to display in placeholders:',
      'type': 'text',
      'size': 255,
      'default': "Already Read"
    },
    'unwanted_manual': {
      'label': 'Unwanted story IDs (One per line, blank lines and lines ' +
        'beginning with # will be ignored):',
        'type': 'textarea',
        'size': 100,
        'default': "",
    },
    // TODO: Ideas for future filters:
    // - Genre (allowing more than one whitelist/blacklist entry)
    // TODO: Ideas for filters requiring an in-page UI:
    // - sort orders not already offered (eg. faves/follows on authors faves)
    // - min/max words as a freeform range entry
    // - filter sets which can be toggled
  };

  var frame = $('<div>').appendTo('body')[0];
  var config_params = {
    'id': 'ffnet_result_filter',
    'title': 'Fanfiction.net Unwanted Result Filter',
    'fields': fieldDefs,
    'css': ('#ffnet_result_filter ' + [
      // Match Fanfiction.net styling more closely
      ".section_header { background-color: #339 !important; }",
      ".section_desc { background-color: #f6f7ee !important; border: none; " +
      "  padding: 1px; }",
      ".config_header { font-size: 16pt; }",
      ".field_label { font-size: 13px; font-weight: normal; }",
      // Layout adjustments for using a non-iframe container
      "label { display: inline; }",
      ".section_header_holder { padding: 15px; margin-bottom: 2em; }",
      ".modal-footer { margin-top: -2em; }",
      ".saveclose_buttons.btn { margin: 0 0 0 5px !important; }",
      ".reset_holder { padding-right: 12px; }",
      "input[type=checkbox] { margin: 2px 4px 2px; }",
      // Make the panel sanely scrollable
      "#ffnet_result_filter_wrapper { " +
        " display: flex; flex-direction: column; height: 100%; }\n" +
      "#ffnrfilter_contentbox { " +
        " flex: 1 1 auto; min-height: 0px; overflow-y: scroll; }\n" +
      // Form layout fixes
      "input[type=text], textarea " +
      "  { width: calc(100% - 1.1em); resize: vertical; }\n" +
      "#ffnet_result_filter_filter_manual_var, " +
      "#ffnet_result_filter_filter_slash_var, " +
      "#ffnet_result_filter_filter_cats_var, " +
      "#ffnet_result_filter_hide_manual_var, " +
      "#ffnet_result_filter_hide_slash_var, " +
      "#ffnet_result_filter_hide_cats_var, " +
      "#ffnet_result_filter_slash_pat_var, " +
      "#ffnet_result_filter_not_slash_pat_var " +
      "    { display: inline-block; margin-right: 1em !important; } " +
      "#ffnet_result_filter_field_slash_pat, " +
      "#ffnet_result_filter_field_not_slash_pat " +
      "    { max-width: 20em; } " +
      "#ffnet_result_filter_field_unwanted_cats { min-height: 10em; }"
      ].join('\n#ffnet_result_filter ')),
    'events': {
      'open': function(doc) {
        // Reconcile GM_config and Bootstrap CSS
        $(this.frame).css({
          'z-index': 1050,
          'top': '50px',
          'height': 'calc(99% - 100px)',
          'border-color': '#d4d4d4',
        }).addClass('modal fade in');
        $('button', this.frame).addClass('btn');
        $('.reset_holder').addClass('btn pull-left');
        $('#ffnet_result_filter_buttons_holder').addClass('modal-footer');
        $('#ffnet_result_filter_slash_pat_var').before("<br>");
        var header = $('.config_header').addClass('modal-header');
        // Move the content into a wrapper DIV for styling
        $('<div>', {id: 'ffnrfilter_contentbox'})
          .insertAfter(header)
          .append($('.section_header_holder').detach());
        // Add a clickable backdrop for the panel
        $("<div>", {'id': 'gm_modal_back'}).click(function() {
          GM_config.close();
        }).addClass('modal-backdrop fade in').appendTo('body');
      },
      'close': function() {
        // Plumb the added backdrop into the close handler
        $('#gm_modal_back').remove();
      }
    },
    'frame': frame,
  };

  // ----==== Functions ====----

  /// Escape string for literal meaning inside a regular expression
  /// Source: http://stackoverflow.com/a/3561711/435253
  var re_escape = function(s) {
    return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  };

  /// Use with Array.filter() to remove comments and blank lines
  var rows_filter = function(elem, idx, arr) {
    elem = elem.trim()
    if (elem.charAt(0) == '#' || !elem) {
      return false;
    }
    return true;
  };

  /// Parse an array from a newline-delimited string containing # comments
  var parse_lines = function(s) {
    return s.split("\n").map(function(e, i, a) {
      return e.trim();
    }).filter(rows_filter);
  };

  /// Parse a usable list of category patterns from a raw string
  var parse_cats_list = function(s, escape, commute) {
    // Parse the config
    var cats_raw = parse_lines(s);
    if (escape) { cats_raw = cats_raw.map(re_escape) }
    if (commute) {
      var cats_out = [];
      for (var i = 0, len = cats_raw.length; i < len; i++) {
        var line = cats_raw[i];
        cats_out.push(line);
        var parts = line.split(' & ');
        if (parts.length > 1) {
          cats_out.push(parts[1] + ' & ' + parts[0]);
        }
      }
    }
    return cats_out;
  };

  // Parse a usable list of story IDs from a raw string
  var parse_id_list = function(s) {
    return parse_lines(s).filter(rows_filter);
  };

  /// Hide a story entry in a way it can be recovered
  var hide_entry = function(node) {
    $(node).addClass('filtered').hide();
  };

  /// Hide a story entry and add a clickable placeholder
  var add_placeholder = function(node, reason) {
    var $story = $(node);
    var $placeholder = $story.clone();

    $placeholder.html("Click to Show (" + reason + ")").css({
      minHeight: 0,
      maxHeight: '1em',
      color: 'lightgray',
      textAlign: 'center',
      cursor: 'pointer',
    }).click($story, function(e) {
      $(this).slideUp();
      e.data.css('min-height', 0).slideDown();
    }).addClass('filter_placeholder').insertBefore($story);
    hide_entry($story);
  };

  /// Code which must be re-run to reapply filters after changing the config
  var initialize_filters_and_apply = function() {
    // Parse the config
    var bad_cats = parse_cats_list(
      GM_config.get('unwanted_cats'),
      GM_config.get('unwanted_cats_escape'),
      GM_config.get('unwanted_cats_commute')
    );

    if (GM_config.get('filter_manual')) {
      var manual_story_ids = parse_id_list(GM_config.get('unwanted_manual'));
    } else {
      var manual_story_ids = [];
    }
    var story_link_re = new RegExp("\/s\/(\\d+)\/");

    // Generate RegExp objects from the parsed config
    var slash_re = new RegExp(GM_config.get('slash_pat'), 'i');
    var not_slash_re = new RegExp(GM_config.get('not_slash_pat'), 'i');
    var cats_re = new RegExp("(?:^|- )(.*(" + bad_cats.join('|') + ').*) - Rated:.*');
    var cat_link_re = new RegExp(bad_cats.join('|'));

    // Clean up after any previous run
    $(".filter_placeholder").remove();
    $(".filtered").show();
    $(".ignored").css('opacity', 1).show().removeClass('ignored');

    var results = $(".z-list");
    for (var i = 0, reslen = results.length; i < reslen; i++) {
      var story = results[i];
      var meta_row = $('.xgray', story).text();
      var description = $('.z-padtop', story).contents()[0].data;

      // TODO: Redesign to collapse runs of hidden entries
      // TODO: Show a comma-separated list of reasons something was hidden
      var reason = null;
      if (manual_story_ids.length > 0) {
        var story_url = $('a.stitle', story).attr('href');
        var story_id = story_link_re.exec(story_url)[1];
        if (story_id && manual_story_ids.indexOf(story_id) != -1) {
          if (GM_config.get('hide_manual')) {
            hide_entry(story);
          } else {
            add_placeholder(story, GM_config.get('manual_reason'));
          }
          continue;
        }
      }

      if (GM_config.get('filter_slash') && slash_re.test(description)
          && !not_slash_re.test(description)) {
        if (GM_config.get('hide_slash')) {
          hide_entry(story);
        } else {
          add_placeholder(story, "Slash");
        }
        continue;
      }

      if (GM_config.get('filter_cats')) {
        var matches = meta_row.match(cats_re);
        if (matches && matches.length > 0) {
          if (GM_config.get('hide_cats')) {
            hide_entry(story);
          } else {
            add_placeholder(story, matches[1]);
          }
          continue;
        }
      }
    }

    if (GM_config.get('filter_cats')) {
      var hide_cats = GM_config.get('hide_cats');
      $('#list_output a').each(function() {
        $this = $(this);
        // "Browse" wraps the title and entry count in a <div> that lets us
        // easily fade both while "Community" puts them directly in the <td>
        // which defines the column.
        var $parent = $this.parent('div');
        if ($parent.length > 0) { $this = $parent; }

        if (cat_link_re.test($this.text())) {
          $this.addClass('ignored');
          if (hide_cats) {
            $this.hide();
          } else {
            $this.css('opacity', IGNORED_OPACITY);
          }
        }
      });
    }
  };

  // ----==== Begin Main Program ====----

  // Stuff which either must or need only be called once
  GM_config.init(config_params);
  GM_config.onSave = initialize_filters_and_apply;
  GM_registerMenuCommand("Configure Result Filter...",
                         function() { GM_config.open(); }, 'C');

  // Clear out ad box which misaligns "Hidden" message if it's first result
  $($('#content_wrapper_inner ins.adsbygoogle').parent()[0]).remove();

  initialize_filters_and_apply();

}).call(this, jQuery);