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