WaniKani Example Sentences

Displays additional examples sentences for the given vocabulary.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name WaniKani Example Sentences
// @version 3.2
// @description  Displays additional examples sentences for the given vocabulary.
// @require https://greasyfork.org/scripts/34539-wanikani-api/code/WaniKani%20API.js?version=226222
// @match https://www.wanikani.com/settings/account*
// @match https://www.wanikani.com/vocabulary/*
// @match https://www.wanikani.com/review/session*
// @match https://www.wanikani.com/lesson/session*
// @run-at          document-end
// @copyright 2017 jeshuam
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @grant       GM_addStyle
// @namespace https://greasyfork.org/users/27794
// ==/UserScript==

//////////////////////////
/////// CSS Styles ///////
//////////////////////////
GM_addStyle(`
div#example-sentences {
max-height: 400px;
overflow: auto;
}

.example-sentence-unlearned-vocab {
display: none;
}

#example-sentences-toggle-display {
margin-bottom: 20px;
}`);


/////////////////////////
/////// Constants ///////
/////////////////////////
// The amount of time before the learned item cache is updated.
let CACHE_EXPIRATION_TIMER_MS = 24 * 60 * 60 * 1000; // (1 day)


/////////////////////////////////
/////// Utility Functions ///////
/////////////////////////////////
//
// Determine whether the given Japanese word ends in an 'u' sound (i.e. is a verb).
//
function EndsInUSound(japaneseWord) {
  return japaneseWord.match(/[るゆむふぬつすくう]/) !== null;
}

//
// Function to check whether a Japanese word only contains kana (hira- or katakana).
//
function OnlyContainsKanaOrPunctuation(japaneseWord) {
  return japaneseWord.match(/^[\u3000-\u30FF]+$/) !== null;
}


/////////////////////////////
/////// Main Function ///////
/////////////////////////////
function DisplayExampleSentences(known_vocab) {
  //
  // Extract the Kanji from the current page. This will have a switch for each type of page, and
  // will do something different for each. This will only include vocabulary pages, reviews and
  // lessons.
  //
  function GetVocabularyKanjiFromPage() {
    // Vocabulary information page.
    if (document.URL.indexOf('vocabulary') != -1) {
      return document.querySelector('header span.vocabulary-icon span').innerText.trim();
    }

    // Review page.
    else if (document.URL.indexOf('review/session') != -1) {
      return document.querySelector('div.vocabulary span').innerText.trim();
    }

    // Lesson page.
    else if (document.URL.indexOf('lesson/session') != -1) {
      return document.querySelector('div#main-info div#character').innerText.trim();
    }

    // Not on a valid page.
    else {
      return null;
    }
  }

  //
  // Get the data from the remote URL for the given vocabulary.
  //
  function GetExampleSentencesForVocabulary(vocabulary, complete) {
    WaniKaniAPI.load('https://jeshuam.pythonanywhere.com/wanikani-sentences/' + vocabulary, complete);
  }

  //
  // Generate the DOM required to display the example sentences. This will be consistent over the
  // various pages this script runs on.
  //
  function GetSectionWithExamplesSentences(known_vocab, sentences) {
    // Make the initial section.
    let section = document.createElement('section');
    section.id = 'examples-sentences-section';
    section.innerHTML = `
<h2>More Context Sentences</h2>
<button id="example-sentences-toggle-display">Show All Sentences</button>
<div id="example-sentences"></div>
`;

    // When the button is pressed, show/hide the example sentences with unlearned vocab.
    section.children[1].onclick = function() {
      if (this.innerText === 'Show All Sentences') {
        for (let element of document.querySelectorAll('.example-sentence-unlearned-vocab')) {
          element.style.display = 'block';
        }

        this.innerText = 'Show Only Known Vocab';
      } else {
        for (let element of document.querySelectorAll('.example-sentence-unlearned-vocab')) {
          element.style.display = 'none';
        }

        this.innerText = 'Show All Sentences';
      }
    };

    // Add each sentence to the section.
    for (let sentence of sentences) {
      // Check if this sentence has any words we don't know. If it does, add an extra class to it.
      let extra_class = '';
      let kanji = GetVocabularyKanjiFromPage();
      for (let word of sentence.jpn) {
        if (word != kanji && !OnlyContainsKanaOrPunctuation(word) && known_vocab[word] === undefined) {
          extra_class = 'example-sentence-unlearned-vocab';
          break;
        }
      }

      // Make the HTML for this sentence.
      let japanese_html = '';
      for (let word of sentence.jpn) {
        // Highlight the current word.
        if (word == kanji) {
          japanese_html += '<span class="vocabulary-highlight highlight-vocabulary">' + kanji + '</span>';
        }

        // Insert a link to the WaniKani page for learned vocabulary.
        else if (known_vocab[word] !== undefined) {
          japanese_html += '<span><a href="https://www.wanikani.com/vocabulary/' + known_vocab[word] + '">' + word + '</a></span>';
        }

        // Otherwise, just put the word into the text.
        else {
          japanese_html += '<span>' + word + '</span>';
        }
      }

      let sentence_html = document.createElement('div');
      sentence_html.className = `context-sentence-group ${extra_class}`;
      sentence_html.innerHTML = `<p lang="ja">${japanese_html}</p><p>${sentence.eng}</p>`;

      section.children[2].appendChild(sentence_html);
    }

    return section;
  }

  // Process the vocabulary page.
  if (document.URL.indexOf('vocabulary') >= 0) {
    let vocab = GetVocabularyKanjiFromPage();
    GetExampleSentencesForVocabulary(vocab, function(data) {
      if (data.length === 0) {
        return;
      }

      let section = GetSectionWithExamplesSentences(known_vocab, data);
      let insertion_section = document.querySelector('section.context-sentence');
      insertion_section.parentNode.insertBefore(section, insertion_section.nextSibling);
    });
  }

  // Process the review page. TODO(jeshua): test this.
  else if (document.URL.indexOf('review/session') >= 0) {
    // If the 'all-info' button is pressed, then display it.
    document.querySelector('div#all-info').onclick = function() {
      let vocab = GetVocabularyKanjiFromPage();
      GetExampleSentencesForVocabulary(vocab, function(data) {
        if (data.length === 0) {
          return;
        }

        // Remove the old section.
        let section_to_remove = document.querySelector('#examples-sentences-section');
        if (section_to_remove !== null) {
          section_to_remove.parentNode.removeChild(section_to_remove);
        }

        let section = GetSectionWithExamplesSentences(known_vocab, data);
        let insertion_section = document.querySelector('section#item-info-context-sentences');
        insertion_section.parentNode.appendChild(section);
      });
    };
  }

  // Process the lesson page.
  else if (document.URL.indexOf('lesson/session') >= 0) {
    let observer = new MutationObserver(function() {
      let vocab = GetVocabularyKanjiFromPage();
      GetExampleSentencesForVocabulary(vocab, function(data) {
        if (data.length === 0) {
          return;
        }

        // Remove the old section.
        let section_to_remove = document.querySelector('#examples-sentences-section');
        if (section_to_remove !== null) {
          section_to_remove.parentNode.removeChild(section_to_remove);
        }

        // Add the new section.
        let section = GetSectionWithExamplesSentences(known_vocab, data);
        let insertion_section = document.querySelector('div#supplement-voc-context-sentence');
        insertion_section.parentNode.insertBefore(section, insertion_section.nextSibling);
      });
    });
    
    observer.observe(document.querySelector('div#main-info'), {subtree: true, childList: true});
  }
}


//////////////////////////////
/////// Start Function ///////
//////////////////////////////
document.addEventListener('DOMContentLoaded', function() {
  // Get their API key. If we are on the account page, go no further.
  if (WaniKaniAPI.getAPIKey() === undefined) {
    console.log('EXAMPLE-SENTENCES: No WaniKani API key found!');
    return;
  }

  // Load the unlocked vocab, initializing to an empty object.
  let unlockedVocab = JSON.parse(GM_getValue('wanikani-sentences-learned-cache', '{"__cache-time": 0}'));

  // If cache expired, update first then run main.
  let currentTime = (new Date().getTime());
  if ((currentTime - unlockedVocab['__cache-time']) > CACHE_EXPIRATION_TIMER_MS) {
    console.log('EXAMPLE-SENTENCES: Cache expired, refreshing known vocabulary.');
    WaniKaniAPI.load(WaniKaniAPI.apiURL('vocabulary'), function(data) {
      for (let vocab of data.requested_information.general) {
        if (vocab.user_specific !== null) {
          // Remove any preceding ~ characters (as they aren't part of the word).
          let kanji = vocab.character.replace(/〜/g, '');
          unlockedVocab[kanji] = kanji;

          // For verbs, remove the U sound at the end. This should do a decent job of showing
          // the verb even if it is conjugated (it won't be perfect, but better than nothing).
          if (EndsInUSound(kanji)) {
            unlockedVocab[kanji.substr(0, kanji.length - 1)] = kanji;
          }
        }
      }

      // Cache time in milliseconds since the epoch.
      unlockedVocab['__cache-time'] = (new Date().getTime());

      // Save the cache, then keep going with the main program.
      GM_setValue('wanikani-sentences-learned-cache', JSON.stringify(unlockedVocab));
      DisplayExampleSentences(unlockedVocab);
    });
  } else {
    DisplayExampleSentences(unlockedVocab);
  }
});