Fanfiction.net Unwanted Result Filter

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

目前為 2015-11-28 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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.0
//
// @require     http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.2.min.js
// @require     https://greasyfork.org/scripts/2855-gm-config/code/GM_config.js?version=33973
//
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_log
// @noframes
//
// @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 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, blanks lines and lines ' +
        'beginning with # will be ignored):',
        'title': 'One entry per line',
        '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,
    }
  };

  var config_params = {
    'id': 'ffnet_result_filter',
    'title': 'Fanfiction.net Unwanted Result Filter',
    'fields': fieldDefs,
    'css': (
      // Match Fanfiction.net styling
      ".section_header { background-color: #339 !important; }\n" +
      ".section_desc { background-color: #f6f7ee !important; }\n" +
      // Form layout fixes
      "input[type=text], textarea { width: 99%; resize: vertical; }\n" +
      "input[type=checkbox] { vertical-align: middle; }\n" +
      "#ffnet_result_filter_filter_slash_var, " +
      "#ffnet_result_filter_filter_cats_var, " +
      "#ffnet_result_filter_hide_slash_var, " +
      "#ffnet_result_filter_hide_cats_var " +
      "    { display: inline-block; margin-right: 1em !important; } " +
      "#ffnet_result_filter_field_unwanted_cats { min-height: 10em; }"),
    'events': {
      'open': function(doc) {
        $(this.frame).css('border-color', '#d4d4d4');
        $("<div>", {'id': 'gm_modal_back'}).css({
          position: 'fixed',
          top: 0,
          left: 0,
          width: '100%',
          height: '100%',
          background: 'black',
          opacity: 0.15,
        }).click(function() {
          GM_config.close();
        }).appendTo('body');
      },
      'close': function() {
        $('#gm_modal_back').remove();
      }
    }
  };

  // ----==== 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;
  }

  /// 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')
    );

    // 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:.*');


    // Clean up after any previous run
    $(".filter_placeholder").remove();
    $(".filtered").show();

    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
      var reason = null;
      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");
        }
      } else 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]);
          }
        }
      }
    }
  };

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

  // Stuff which either must or need only be called once
  GM_config.init(config_params);
  GM_config.onChange = 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);