Anime1 filter

Removes links to objectionable adult content.

// ==UserScript==
// @name        Anime1 filter
// @version     0.3.2
// @description Removes links to objectionable adult content.
// @match       *.anime1.com/*
// @copyright   2012-2014, Chaim-Leib Halbert
// @namespace https://greasyfork.org/users/910
// ==/UserScript==

// Documentation: https://github.com/chaimleib/anime1-filter

/* ### Utilities ### */
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

var DEBUG = true;

function log() {
    if (DEBUG)
        console.log.apply(console, arguments);
}

function warn() {
    console.warn.appy(console, arguments);
}

// whether val is in arr
function inArray(val, arr) {
    // apparently, a decrementing while loop is the fastest way to iterate
    var i = arr.length;
    while(i--) {
        if (val === arr[i]) return true;
    }
    return false;
}

// used for NodeLists, which are not quite like arrays, and have no forEach()
// method.
// https://developer.mozilla.org/en-US/docs/Web/API/NodeList
function forEach(arr, func) {
    var i = arr.length;
    while (i--) func(arr[i], i, arr);
}


/* ### Main logic ### */
// Lower-case list of terms to filter
var censor_list = [
    'ecchi',
    'harem',
    'shoujo ai',
    'shonen ai',
    'shounen ai',
    //'romance',
];

// is str in the blacklist?
function should_censor(str) {
    return inArray(str.toLowerCase(), censor_list);
}

// called when jQ is ready
function main(jQ) {
    // watch for dynamically-loaded billboards. takes an array of
    // MutationRecords.
    // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
    function billboards_were_added(mutations) {
        // I only care about addedNodes, not removedNodes, so filter them
        mutations = mutations.filter(function (el) {
            return el.addedNodes.length;
        });
        if (!mutations.length)
            return;
        forEach(mutations, function (m) {
            forEach(m.addedNodes, checkNode);
        });
    }

    // check each changed node for a billboard and censor it. Takes a Node.
    // https://developer.mozilla.org/en-US/docs/Web/API/Node
    function checkNode(n) {
        if (!n.classList)
            return;

        // I have to check for 'an-text' class first, because if I were
        // to check for the enclosing 'an-box' class, it would break.
        // Apparently, Anime1.com first loads up empty 'an-box'es, then
        // fills them with images and data. I need to make sure the 'an-text'
        // elements are present before I can tell whether to censor the
        // anime or not.
        if (inArray('an-text', n.classList)) {
            check_billboard(n.parentNode);
        }
    }

    // remove billboard if it has a blacklisted genre
    function check_billboard(bb) {
        if (should_censor_billboard(bb)) {
            remove_billboard(bb);
        }
    }

    // whether the billboard is for an anime in a blacklisted genre
    function should_censor_billboard(bb) {
        var genres = jQ(bb).find('div.dgenres a');
        for (var i=0; i<genres.length; i++) {
            var genre = genres[i].innerHTML;
            if (should_censor(genre)) return true;
        }
        return false;
    }

    // remove the billboard from the DOM
    function remove_billboard(bb) {
        var name = get_billboard_name(bb);
        bb.parentNode.removeChild(bb);
        log('!!Removed: ' + name);
    }

    // extract the name of the billboard's anime
    function get_billboard_name(bb) {
        var heads = jQ(bb).find('h2');
        var name;
        if (!heads.length) {
            warn('could not read bb name!');
            warn(bb);
            name = "???";
        }
        else {
            name = heads[0].innerHTML;
        }
        return name;
    }

    // filters the list of billboards on initial document load
    function start_check_billboards() {
        var bbs = jQ('div.an-box');
        if (!bbs.length) {
            log('no bbs found!');
            observer.disconnect();
            return;
        }
        else {
            log('found ' + bbs.length + ' billboards.');
        }
        forEach(bbs, check_billboard);
    }



    /* actually set up the observer to watch for changes */
    var observer = new MutationObserver(billboards_were_added);
    // define what element should be observed by the observer
    // and what types of mutations trigger the callback
    observer.observe(document.body, {
      subtree: true,
      childList: true,
    });


    // filters the genre links list (/content/genre)
    function start_check_genre_links() {
        var genre_ary = jQ('.popularity-by-genre > ul a');
        if (genre_ary.length === 0) {
            log('no genres found!');
            return;
        }

        for (var i=0; i<genre_ary.length; i++) {
            var a = genre_ary[i];
            var li = a.parentNode;
            if (should_censor(a.innerHTML)) {
                li.parentNode.removeChild(li);
            }
        }
    }

    start_check_genre_links();
    start_check_billboards();
}


/* ### Bootstrapper ### */
// wait for jQuery, then call main()
function addJQuery(callback) {
    document.addEventListener('load', function() {
        var script = document.createElement("script");
        script.setAttribute("src", "//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js");
        script.textContent = "(" + callback.toString() + ")(jQuery);";
        //script.textContent = "window.jQ=$;(" + callback.toString() + ")();";
        document.body.appendChild(script);
    }, false);
}

// Make sure jQ is loaded before calling main()
if (jQuery !== undefined) {
    log('Skipping delay for jQ')
    log(main.toString());
    main(jQuery);
} else {
    log('Waiting for jQuery...');
    addJQuery(main);
}