Greasy Fork 支持简体中文。

WaniKanify

Firefox version of chedkid's excellent Chrome app

// ==UserScript==
// @name        WaniKanify
// @namespace   wanikani
// @description Firefox version of chedkid's excellent Chrome app
// @include     *
// @version     1.4.0
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @require     http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js
// ==/UserScript==

// Current format version of the vocab database
var FORMAT_VER = 2;

// Lock to ensure only one download at once
var downloading = false;

/* Main */
window.addEventListener ("load", function () {

    GM_registerMenuCommand("Wanikanify: Run", tryRun);
    GM_registerMenuCommand("Wanikanify: Refresh vocabulary", tryRefreshVocabulary);
    GM_registerMenuCommand("Wanikanify: Enable auto-run", promptAutoRun);
    GM_registerMenuCommand("Wanikanify: Set API key", promptApiKey);

    if (GM_getValue("autoRun") == 1)
        run(false);

}, false);

/**
 * User interface
 */

/* Run script */
function tryRun() {
    var apiKey = getApiKey();
    if (apiKey != undefined)
        run(false);
}

/* Refresh vocabulary */
function tryRefreshVocabulary() {
    var apiKey = getApiKey();
    if (apiKey != undefined)
        downloadVocab();
}

/* Specifiy whether to run automatically */
function promptAutoRun() {
    var autoRun = parseInt(window.prompt("Enter 1 to enable auto-run and 0 to disable", GM_getValue("autoRun") ? 1 : 0));
    GM_setValue("autoRun", autoRun);
}

/* Specify the WaniKani API key */
function promptApiKey() {
    var apiKey;

    while (true) {
        apiKey = GM_getValue("apiKey");
        apiKey = window.prompt("Please enter your API key", apiKey ? apiKey : "");

        if (apiKey && !/^[a-fA-F0-9]{32}$/.test(apiKey))
            alert("That was not a valid API key, please try again");
        else
            break;
    }

    GM_setValue("apiKey", apiKey);
    GM_setValue("mustRefresh", 1);
}

/**
 * Runtime code
 */

function getApiKey() {
    var apiKey = GM_getValue("apiKey");

    if (apiKey == undefined)
        promptApiKey();

    return apiKey;
}

function run(refreshFirst) {
    // Ignore current vocab download
    downloading = false;

    // Get the current timestamp (in minutes)
    var currentTime = Math.floor(new Date().getTime() / 60000);

    // Get the time and the vocab format from when the vocab list was last refreshed
    var refreshTime = GM_getValue("refreshTime");
    var refreshFormat = GM_getValue("formatVer");

    // Hotfix: Allow script to enforce refresh
    var mustRefresh = GM_getValue("mustRefresh") == 1;
    GM_setValue("mustRefresh", 0);

    // See if we should to refresh. If a week has passed, it's probably a good idea anyway
    var shouldRefresh = mustRefresh || refreshFirst || (GM_getValue("vocab", "") == "") || (refreshFormat != FORMAT_VER)
    || (refreshTime == undefined) || (currentTime - refreshTime >= 10080);

    // No refresh needed, simply replace vocab...
    if (!shouldRefresh)
        replaceVocab();
    // Update vocab list if last refresh was too long ago (and replace afterwards), or old version
    else
        downloadVocab(true);
}

function downloadVocab(runAfter, wasManual) {
    if (downloading) {
        console.log("Attempted to download WaniKani data while already downloading");
        return;
    }

    var apiKey = GM_getValue("apiKey");
    if (apiKey == undefined)
        return;

    console.log("Downloading new vocab data...");
    downloading = true;

    GM_xmlhttpRequest({
        method: "GET",
        url: "http://www.wanikani.com/api/v1.2/user/" + apiKey + "/vocabulary/",
        onerror: function (response) {
            alert("Error while downloading WaniKani data. Please try again later.");
            downloading = false;
        },
        onload: function (response) {
            var json;

            try {
                json = JSON.parse(response.responseText);
            } catch (e) {
                alert("Unable to process WaniKani data. Please try again later.", e);
            }

            if (json) {
                if ("error" in json) {
                    alert("Error from WaniKani: " + json.error.message);
                    downloading = false;
                    return;
                }

                GM_setValue("refreshTime", Math.floor(new Date().getTime() / 60000));

                // Create the vocab map that will be stored
                var vocabMap = {};
                // Loop through all received words
                var vocabList = json.requested_information.general;

                for (var i in vocabList) {
                    var vocab = vocabList[i];

                    // Skip words not yet learned
                    if (!vocab.user_specific)
                        continue;

                    // Split multiple spellings
                    var meanings = vocab.meaning.split(", ");

                    // Conjugate verbs
                    var conjugations = [];
                    for (var m in meanings) {
                        var word = meanings[m];

                        // If a verb...
                        if (/^to /.test(word))
                        {
                            // Remove leading 'to'
                            meanings[m] = word.substr(3);

                            // Remove 'e' suffix for conjugations
                            if (word.slice(-1) == "e")
                                word = word.slice(0, -1);

                            if (!/ /.test(word))
                                conjugations.push(word + "ed", word + "es", word + "en", word + "es", word + "s", word + "ing");
                        }

                        // Not a verb, try plural
                        else if (word.length >= 3 && word.slice(-1) != "s")
                            conjugations.push(word + "s");
                    }
                    meanings.push.apply(meanings, conjugations);

                    // After updating the meanings,
                    for (var m in meanings) {
                        vocabMap[meanings[m]] = vocab.character;
                    }
                }

                String.prototype.hashCode = function() {
                    var hash = 0, i, char;
                    if (this.length == 0) return hash;
                    for (i = 0, l = this.length; i < l; i++) {
                        char  = this.charCodeAt(i);
                        hash  = ((hash<<5)-hash)+char;
                        hash |= 0; // Convert to 32bit integer
                    }
                    return hash;
                };

                // Update the new vocab list in the storage
                vocabJson = JSON.stringify(vocabMap);
                GM_setValue("vocab", vocabJson);
                if(!runAfter) {
                    if (vocabJson.hashCode() != GM_getValue("vocabHash")) {
                        alert("Successfully updated vocab!");
                        GM_setValue("vocabHash", vocabJson.hashCode());
                    }
                    // Only inform the user that the vocab is up to date if he manually requested to update it
                    else {
                        alert("Vocab already up to date");
                    }
                }
                GM_setValue("formatVer", FORMAT_VER);

                // Unlock download lock
                downloading = false;

                // If wanted, immediately run the script
                if (runAfter)
                    replaceVocab(vocabMap);
            }
        }
    });
}

function replaceVocab(vocabMap) {
    // No vocab map given, try to parse from stored JSON
    if (!vocabMap) {
        try {
            vocabMap = JSON.parse(GM_getValue("vocab"), {});
            if (!vocabMap || (vocabMap && jQuery.isEmptyObject(vocabMap)))
                throw 1;
        } catch (e) {
            alert("Error while parsing the vocab list; deleting it now. Please try again.");
            GM_setValue("vocab", "");
            return;
        }
    }

    console.log(vocabMap);
    console.log("Replacing vocab...");

    var replaceCallback = function(str) {
        if (vocabMap.hasOwnProperty(str.toLowerCase())) {
            var translation = vocabMap[str.toLowerCase()];
            return '<span class="wanikanified" title="' + str + '" data-en="' + str + '" data-jp="' + translation +
                '" onClick="var t = this.getAttribute(\'title\'); this.setAttribute(\'title\', this.innerHTML); this.innerHTML = t;">' + translation + '<\/span>';
        }

        // Couldn't replace anything, leave as is
        return str;
    };

    var nodes = $("body *:not(noscript):not(script):not(style)");

    // Very naive attempt at replacing vocab consisting of multiple words first
    nodes.replaceText(/\b(\S+?\s+\S+?\s+\S+?\s+\S+?)\b/g, replaceCallback);
    nodes.replaceText(/\b(\S+?\s+\S+?\s+\S+?)\b/g, replaceCallback);
    nodes.replaceText(/\b(\S+?\s+\S+?)\b/g, replaceCallback);
    nodes.replaceText(/\b(\S+?)\b/g, replaceCallback);

    console.log("Vocab replaced!"); 
}

/*
 * jQuery replaceText - v1.1 - 11/21/2009
 * http://benalman.com/projects/jquery-replacetext-plugin/
 * 
 * Copyright (c) 2009 "Cowboy" Ben Alman
 * Dual licensed under the MIT and GPL licenses.
 * http://benalman.com/about/license/
 */
(function($){$.fn.replaceText=function(b,a,c){return this.each(function(){var f=this.firstChild,g,e,d=[];if(f){do{if(f.nodeType===3){g=f.nodeValue;e=g.replace(b,a);if(e!==g){if(!c&&/</.test(e)){$(f).before(e);d.push(f)}else{f.nodeValue=e}}}}while(f=f.nextSibling)}d.length&&$(d).remove()})}})(jQuery);