WaniKani Phonetic-Semantic Composition Rebirth

Adds information to Wanikani about kanji that use Phonetic-Semantic Composition.

// ==UserScript==
// @name        WaniKani Phonetic-Semantic Composition Rebirth
// @namespace   wk_phon_rebirth
// @include     http://www.wanikani.com/kanji/*
// @include     http://www.wanikani.com/review/session*
// @include     http://www.wanikani.com/lesson/session*
// @include     https://www.wanikani.com/kanji/*
// @include     https://www.wanikani.com/review/session*
// @include     https://www.wanikani.com/lesson/session*
// @author      acm
// @description Adds information to Wanikani about kanji that use Phonetic-Semantic Composition.
// @version     1.1.1
// @license     GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @grant       GM_addStyle
// @grant       unsafeWindow
// @require     https://greasyfork.org/scripts/34328-wanikani-phonetic-semantic-composition-original-database/code/Wanikani%20Phonetic-Semantic%20Composition%20Original%20Database.js
// ==/UserScript==

/*
 *  ====  Wanikani  Phonetic-Semantic Composition  ====
 *    ==             by ruipgpinheiro              ==
 *     =           modifications by acm            =
 *
 *  It seems that many kanji were created using a process called phonetic-semantic
 *  composition. This process joins two (or more) kanji (radicals), one (or more of them)
 *  usually called the bushu or dictionary section header establishes the meaning of the
 *  kanji, and another one, the phonetic component that establishes the (on'yomi) sound.
 *
 *  This means that a lot of kanji have a built-in mnemonic that I haven't seen being
 *  referred to in Wanikani, and so it's quite useful to know some of them, especially
 *  when having trouble with a specific reading!
 *
 *
 *
 *  For example (using non-Wanikani kanji names):
 *
 *  反・はん "to rebel" ("anti" by Wanikani mnemonics)
 *
 *  飯・はん "rice"
 *  版・はん "print"
 *  板・はん "a board"
 *  坂・はん "slope"
 *  販・はん "sale"
 *  叛・はん "to betray"
 *
 *  As you can notice, these kanji all use the first one as a phonetic component, placing it
 *  to the right of the semantic component (mostly, phonetic components are drawn right-most).
 *  Due to the evolution of the language, many such kanji have since then slightly changed
 *  pronunciation (仮・か "temporary"), but knowing this information can be a major help.
 *
 *  This script imports a database of over 100 phonetic components with over 400 regular Kanji
 *  that use their on'yomi reading onto Wanikani. This means that over a fourth of Wanikani's
 *  Kanji should be included in here somewhere and have a "built-in mnemonic" of sorts.
 *  Depending on how you study, it could be a huge help (or no help at all - you decide what's
 *  best for your brain). The information will be shown on the Kanji info page, during reviews
 *  (if you check the details for a Kanji) and during lessons, provided the relevant Kanji is
 *  included in the database.
 *
 *  Note that the database used in this script was automatically generated from a PDF file,
 *  and even though I tried to check it for mistakes, it is possible that it contains an error or two.
 *  This userscript contains the whole Kanji table from Hiroko Townsend's Thesis about phonetic
 *  components, which means the script's database includes 143 different phonetic components
 *  encompassing 417 regular kanji (kanji that use the on'yomi reading from the phonetic component)
 *  and 210 irregular ones (kanji that use a different reading, though with - supposedly - similar
 *  roots). Some of these Kanji aren't available on Wanikani, though, even though they'll be shown
 *  by the userscript as they are part of its database.
 *
 */

/*
 *  ====  LICENSE INFORMATION  ====
 *
 *  This script contains a database of phonetic components adapted under Fair Use
 *  (for nonprofit educational purposes) from
 *    Phonetic Components in Japanese Characters
 *      by Hiroko Townsend
 *      Master of Arts in Linguistics, San Diego State University, 2011
 *  Obtain a complete copy of the Thesis at http://sdsu-dspace.calstate.edu/bitstream/handle/10211.10/1203/Townsend_Hiroko.pdf
 *  Thank you Hiroko for the very useful thesis!
 *
 *
 *
 *	This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/*
 *	=== Changelog ===
 *	1.1.1 (21 October 2017)
 *	- Snatched some code from WaniKani Stroke Order userscript:
 *	-- Use of built-in MutationObserver instead of custom code
 *	-- A single function to extract a kanji instead of 3
 *
 *	1.1.0 (19 October 2017)
 *	- Cleaned up code
 *	- Fixed an issue with GreaseMonkey and onClick events in Firefox
 *
 *  1.0.5 (11 March 2014)
 *  - Relicensed under the GPLv3.
 *
 *  1.0.4 (23 January 2014)
 *  - Now supports the HTTPS protocol.
 *
 *  1.0.3 (24 November 2013)
 *  - Corrected 症, which has the wrong reading in the thesis used for creating the DB.
 *    It's now listed as irregular with the reading しょう, even though its phonetic component can also (rarely) be read the same way.
 *
 *  1.0.2 (24 November 2013)
 *  - Fixed a bug in the code that automatically generated the DB, which would misread phonetic components with a single,
 *    irregular kanji, like 刃, and put them inside the DB entry of the previous phonetic component.
 *    Therefore, the DB was regenerated from scratch. Updated the DB count in the description accordingly.
 *
 *  1.0.1 (23 November 2013)
 *  - Kanji links now open in a new tab, to fix a bug where clicking them would just restart the current reviews/lessons session.
 *
 *  1.0.0 (22 November 2013)
 *  - First release.
 */

/*
 * Debug Settings
 */
var debugLogEnabled = false;
var debugAlwaysUseFirstDBEntry = false;
var scriptShortName = "WKPSC";

scriptLog = debugLogEnabled ?
    function(msg)
    {
        if (typeof msg === "string")
            console.log(scriptShortName + ": " + msg);
        else
            console.log(msg);
    } :
    function() {};

/*
 * Global Variables/Objects/Classes
 */
// Stores the current Wanikani page we're on
var PageEnum = Object.freeze({ unknown:0, kanji:1, reviews:2, lessons:3 });
var curPage = PageEnum.unknown;

// Use the jQuery of the page itself to access storage
$ = unsafeWindow.$;

/*
 * Database Functions
 *
 * Searches the DB for a Kanji
 * If found, returns an object of the form {entry, regular, irregular}, where:
 *   entry - reference to the DB entry containing the Kanji
 *   type - 'regular' or 'irregular', specifies in which DB sub-array the kanji was found
 *   kanji - Kanji found (same as the input kanji)
 * If not found, returns null
 */
function searchDBForKanji(kanji)
{
    for (var i = 0; i < database.length; i++)
    {
        var cur = database[i];

        if (kanji == cur.phonetic)
            return {entry:cur, type:"phonetic", kanji:cur.phonetic};

        if ("regular" in cur)
        {
            for(var j = 0; j < cur.regular.length; j++)
            {
                if (kanji == cur.regular[j])
                    return {entry:cur, type:"regular", kanji:cur.regular[j]};
            }
        }

        if ("irregular" in cur)
        {
            for (var k = 0; k < cur.irregular.length; k++)
            {
                if (kanji == cur.irregular[k][0])
                    return {entry:cur, type:"irregular", kanji:cur.irregular[k][0], irregular:cur.irregular[k]};
            }
        }
    }

    return null;
}

/*
 * Injected Elements and Related Functions
 */
// Toggles the "More Information" button
function WKPSC_moreInformation_onClick(e)
{
    var obj = e.target || e.srcElement;
    var elem = obj.nextSibling;

    if (elem.getAttribute('class') == "WKPSC-more-information-hidden")
    {
        obj.innerHTML = 'Less Information <i class="icon-chevron-up">';
        elem.setAttribute('class', "WKPSC-more-information-show");
    }
    else
    {
        obj.innerHTML = 'More Information <i class="icon-chevron-down">';
        elem.setAttribute('class', "WKPSC-more-information-hidden");
    }
}

// Generates HTML for the injected Element
function generateHTML(dbEntry)
{
    var html;
    var regularText;

    // Detect whether mostly regular or not
    var regularCount = 0;
    var irregularCount = 0;

    if ("regular" in dbEntry.entry)
        regularCount = dbEntry.entry.regular.length;
    if ("irregular" in dbEntry.entry)
        irregularCount = dbEntry.entry.irregular.length;

    if (regularCount >= irregularCount)
    {
        if (irregularCount === 0)
            regularText = "completely regular";
        else
            regularText = "mostly regular";
    }
    else
    {
        if (regularCount === 0)
            regularText = "completely irregular";
        else
            regularText = "mostly irregular";
    }

    var totalCount = regularCount + irregularCount;

    // Generate correct HTML from templates
    var htmlTemplateThisKanji = '<span rel="tooltip" class="kanji-highlight" data-original-title="This Kanji" lang="ja">{0}</span>';
    html = htmlTemplateThisKanji.format(dbEntry.kanji);

    var htmlTemplatePhonetic = '';

    if(dbEntry.type == "phonetic")
    {
        htmlTemplatePhonetic = ' is a <b>{1}</b> phonetic component used in {2} Kanji to represent the <i>on\'yomi</i> reading <text class="WKPSC-hiragana">{3}</text>.';
        html += htmlTemplatePhonetic.format(dbEntry.entry.phonetic, regularText, totalCount, dbEntry.entry.reading);
    }
    else
    {
        if(dbEntry.entry.phonetic == "obsolete")
        {
            htmlTemplateNonPhonetic = ' was formed using phonetic-semantic composition. However, with the passing of time, the phonetic component used became obsolete as a Kanji, so the radical cannot be shown here. Nevertheless, this kanji contains a <b>{1}</b> phonetic component, also used in {2} other Kanji to represent the <i>on\'yomi</i> reading <text class="WKPSC-hiragana">{3}</text>. You should be able to compare Kanji beloging to the same set to figure out what it looks like.';
        }
        else
        {
            htmlTemplateNonPhonetic = ' was formed using phonetic-semantic composition. Therefore it contains the <b>{1}</b> phonetic component <a href="http://www.wanikani.com/kanji/{0}" target="_blank"><span rel="tooltip" class="kanji-highlight" data-original-title="Phonetic Component" lang="ja">{0}</span></a>, also used in {2} other Kanji to represent the <i>on\'yomi</i> reading <text class="WKPSC-hiragana">{3}</text>.';
        }

        html += htmlTemplateNonPhonetic.format(dbEntry.entry.phonetic, regularText, totalCount-1, dbEntry.entry.reading);
    }

    if (dbEntry.type == "irregular")
        html += ' <u>Make sure to note that this Kanji is irregular!</u>';

    var htmlTemplateMoreInformation = '</p><span id="WKPSC_info_btn" class="WKPSC-more-information-button WKPSC-more-information-button-margin">More Information <i class="icon-chevron-down"></i></span><span class="WKPSC-more-information-hidden">This series of phonetic-semantically composed Kanji consists of the ';
    html += htmlTemplateMoreInformation;

    var entryLength = 0;
    var cur = "";
    var i = 0;

    // Generate the list of 'regular' Kanji from templates, if any exist
    if ("regular" in dbEntry.entry)
    {
        var regularKanjiTemplate = '<a href="http://www.wanikani.com/kanji/{0}" target="_blank"><span rel="tooltip" class="kanji-highlight" data-original-title="Kanji" lang="ja">{0}</span></a>';
        var regIrregKanjiJoiningTemplate = '<br> There are also the following ';
        var regularTemplate = '<span rel="tooltip" class="reading-highlight" data-original-title="Same On\'yomi Reading">regular</span> Kanji ';

        html += regularTemplate;

        entryLength = dbEntry.entry.regular.length;
        for (i = 0; i < entryLength; i++)
        {
            cur = dbEntry.entry.regular[i];

            if (i > 0 && i < entryLength-1)
                html += ", ";
            else if (i == entryLength-1 && i > 0)
                html += " and ";

            html += regularKanjiTemplate.format(cur);
        }
        html += '.';

        if ("irregular" in dbEntry.entry)
            html += regIrregKanjiJoiningTemplate;
    }

    // Generate the table of 'irregular' Kanji from templates, if any exist
    if ("irregular" in dbEntry.entry)
    {
        var irregularKanjiTemplate = '<span rel="tooltip" class="reading-highlight" data-original-title="Similar On\'yomi Reading (shared historical roots)">irregular</span> Kanji:<table style="text-align:center; line-height:1.7" align="center" width="200px"><td class="span6"><h3>Kanji</h3></td><td class="span6"><h3>Reading</h3></td></tr>{0}</table>';
        var rowTemplate = '<tr><td><a href="http://www.wanikani.com/kanji/{0}" target="_blank"><span class="kanji-highlight" lang="ja">{0}</span></a></td><td class="WKPSC-hiragana" lang="ja">{1}</td></tr>';

        var tableHTML = "";
        entryLength = dbEntry.entry.irregular.length;

        for (i = 0; i < entryLength; i++)
        {
            cur = dbEntry.entry.irregular[i];

            tableHTML += rowTemplate.format(cur[0], cur[1]);
        }

        html += irregularKanjiTemplate.format(tableHTML);
    }

    // Close the remaining tag and return
    html += '</span>';

    return html;
}

// Create the element to be injected, set its id, class and HTML content
function createHTMLElement(dbEntry)
{
    var elmnt;

    if (curPage == PageEnum.kanji)
        elmnt = document.createElement('aside');
    else
        elmnt = document.createElement('blockquote');

    elmnt.setAttribute('id', 'WKPSC-extra-information');
    elmnt.setAttribute('class', 'additional-info');
    elmnt.innerHTML = '<h3><i class="icon-info-sign"></i> Phonetic-Semantic Composition</h3><p>' + generateHTML(dbEntry) + '</p>';

    return elmnt;
}

// Stores the old element, since we might have to clean it up when in the lessons module
var oldElement = null;

// Detects current Kanji, searches DB, and if a match is found, creates and injects the corresponding HTML Element
function addElement(node)
{
    // If required (lessons module), clean up the previously created element
    if (!isEmpty(oldElement) && !isEmpty(oldElement.parentNode))
        oldElement.parentNode.removeChild(oldElement);
    oldElement = null;

    // Find the current kanji

    var kanji;

    if (debugAlwaysUseFirstDBEntry)
        kanji = database[0].phonetic;
    else
    {
        kanji = getKanji();

        if (kanji === null)
        {
            scriptLog("Unable to extract the current kanji!");
            return;
        }
    }

    scriptLog(kanji);

    // Check whether the current kanji is in the database
    var dbEntry = searchDBForKanji(kanji);

    if (isEmpty(dbEntry)) {
        scriptLog("Kanji not in DB. Ignoring.");
        return;
    }
    scriptLog(dbEntry);

    // Create custom element
    var newElmnt = createHTMLElement(dbEntry);

    // Insert element
    switch(curPage)
    {
        case PageEnum.kanji:
            $('section#note-reading').before(newElmnt);
            break;
        case PageEnum.reviews:
            $('section#item-info-reading-mnemonic').append(newElmnt);
            break;
        case PageEnum.lessons:
            $('div#supplement-kan-reading div:contains("Reading Mnemonic") blockquote:last').after(newElmnt);
            oldElement = newElmnt;
            break;
        default:
            throw Error("Unknown page type!");
    }

    document.getElementById("WKPSC_info_btn").addEventListener("click", WKPSC_moreInformation_onClick);
}

/*
 * Kanji Info Pages
 */
function kanjiInfo_init()
{
    GM_addStyle('.WKPSC-hiragana { font-weight: bold; }');
    GM_addStyle('.WKPSC-more-information-button-margin { margin-bottom: -10px !important; display:block; }');
    GM_addStyle('.WKPSC-more-information-show { margin-top: 40px; margin-bottom: -10px !important; display:block; }');
}

/*
 * Reviews page
 */
function reviews_init()
{
    GM_addStyle('.WKPSC-hiragana { font-weight: normal; }');
    GM_addStyle('.WKPSC-more-information-button-margin { margin-bottom: 0 !important; display:block; }');
    GM_addStyle('span.reading-highlight { background-color: #474747; } span.kanji-highlight { background-color: #FF00AA; };');
    GM_addStyle('span.reading-highlight, span.kanji-highlight {-moz-box-sizing: border-box; border-radius: 3px; box-shadow: 0 -3px 0 rgba(0, 0, 0, 0.2) inset, 0 0 10px rgba(255, 255, 255, 0.5); color: #FFFFFF; display: inline-block; height: 1.8em; line-height: 1.7em; text-align: center; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3); padding-left: 3px; padding-right: 3px; }');

    GM_addStyle('.WKPSC-more-information-show { margin-top: 20px; margin-bottom: -10px !important; display:block; }');
}

lessons_init = reviews_init;

/* Snatched from the stroke order script ... */
/*
 * Returns the current kanji
 */
function getKanji()
{
    switch(curPage)
    {
        case PageEnum.kanji:
            return document.title[document.title.length - 1];

        case PageEnum.reviews:
            var curItem = $.jStorage.get("currentItem");

            if ("kan" in curItem)
                return curItem.kan.trim();
            else
                return null;

        case PageEnum.lessons:
            var kanjiNode = $("#character");

            if (kanjiNode === undefined || kanjiNode === null)
                return null;

            return kanjiNode.text().trim();
    }

    return null;
}

/*
 * Init Functions
 * Set up the hooks needed.
 */
function scriptEventFired(node)
{
    try
    {
        scriptLog("Event fired!");
        addElement(node);
    }
    catch(err)
    {
        logError(err);
    }
}

function scriptInit()
{
    // Add global CSS styles
    GM_addStyle('.WKPSC-more-information-button { color: #888888; cursor: pointer; text-align: center; background-image: url("/assets/default-v2/top-inset-shadow-290f5bd0a4f35ec34dd42c6c1f56a2f3.png"); background-position: center top; background-repeat: no-repeat; margin-top: 15px;} }');
    GM_addStyle('.WKPSC-more-information-hidden { display:block; visibility:hidden; height:0; }');

    scriptLog("loaded");

    // Set up hooks
    try
    {
        if (/\/kanji\/./.test(document.URL)) /* Kanji Pages */
        {
            scriptLog("Kanji Page");
            curPage = PageEnum.kanji;

            kanjiInfo_init();
            addElement();
        }
        else if (/\/review/.test(document.URL)) /* Reviews Pages */
        {
            scriptLog("Reviews page");
            curPage = PageEnum.reviews;

            reviews_init();

            var o = new MutationObserver( function(mutations) {
                // The last one always has 2 mutations, so let's use that
                if (mutations.length != 2)
                    return;

                scriptEventFired($("section[id=item-info-reading-mnemonic]"));
            });

            o.observe(document.getElementById('item-info'), {'attributes' : true});
        }
        else if (/\/lesson/.test(document.URL)) /* Lessons Pages */
        {
            scriptLog("Lessons page");
            curPage = PageEnum.lessons;

            lessons_init();

            var o2 = new MutationObserver( function(mutations) {
                scriptEventFired($("li.active"));
            });

            o2.observe(document.getElementById('supplement-kan'), {'attributes' : true});
        }
    }
    catch(err)
    {
        logError(err);
    }
}

/*
 * Helper Functions/Variables
 */


function isEmpty(value){
    return (typeof value === "undefined" || value === null);
}

if (!String.prototype.format) {
    String.prototype.format = function() {
        var args = arguments;
        return this.replace(/{(\d+)}/g, function(match, number) {
            return typeof args[number] != 'undefined' ? args[number] : match;
        });
    };
}

/*
 * Error handling
 * Can use 'error.stack', not cross-browser (though it should work on Firefox and Chrome)
 */
function logError(error)
{
    var stackMessage = "";

    if ("stack" in error)
        stackMessage = "\n\tStack: " + error.stack;

    console.error(scriptShortName + " Error: " + error.name + "\n\tMessage: " + error.message + stackMessage);
}


/*
 * Start the script
 */
scriptInit();