Here is the updated version of this script that should work with the recent changes to the lesson screen:
"use strict";
// ==UserScript==
// @name Advanced Context Sentence
// @namespace https://openuserjs.org/users/abdullahalt
// @version 1.40
// @description Enhance the context sentence section, highlighting kanji and adding audio
// @author abdullahalt
// @match https://www.wanikani.com/lesson/session
// @match https://www.wanikani.com/review/session
// @match https://www.wanikani.com/vocabulary/*
// @match https://preview.wanikani.com/lesson/session
// @match https://preview.wanikani.com/review/session
// @match https://preview.wanikani.com/vocabulary/*
// @require https://unpkg.com/@popperjs/core@2.5.4/dist/umd/popper.min.js
// @require https://unpkg.com/tippy.js@6.2.7/dist/tippy-bundle.umd.min.js
// @require https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=962341
// @grant none
// @copyright 2019, abdullahalt (https://openuserjs.org//users/abdullahalt)
// @license MIT
// ==/UserScript==
// ==OpenUserJS==
// @author abdullahalt
// ==/OpenUserJS==
(() => {
//--------------------------------------------------------------------------------------------------------------//
//-----------------------------------------------INITIALIZATION-------------------------------------------------//
//--------------------------------------------------------------------------------------------------------------//
const wkof = window.wkof;
const scriptId = "AdvancedContextSentence";
const scriptName = "Advanced Context Sentence";
const recognizedSelector = "a.recognized";
const unrecognizedSelector = "a.unrecognized";
let state = {
settings: {
recognizedKanjiColor: "#f100a1",
unrecognizedKanjiColor: "#888888",
recognitionLevel: "5",
tooltip: {
show: true,
delay: 0,
position: "top"
},
voice: "browser"
},
kanjis: [],
jiff: false // JLPT, Joyo and Frequency Filters
};
// Application start Point
main();
function main() {
init(() => wkItemInfo.forType(`vocabulary`).under(`examples`).notify(() => evolveContextSentence(sentences => sentences[0].previousElementSibling)));
}
function init(callback) {
createReferrer();
createStyle();
if (wkof) {
wkof.include("ItemData,Settings");
wkof
.ready("ItemData,Settings")
.then(loadSettings)
.then(proccessLoadedSettings)
.then(getKanji)
.then(extractKanjiFromResponse)
.then(callback);
} else {
console.warn(
`${scriptName}: You are not using Wanikani Open Framework which this script utlizes to see the kanji you learned and highlights it with a different color, it also provides the settings dialog for the script. You can still use Advanced Context Sentence normally though`
);
callback();
}
}
function evolveContextSentence(getHeader) {
const sentences = document.querySelectorAll(".context-sentence-group:not(.advanced)");
sentences.forEach(s => s.classList.add("advanced"));
if (sentences.length === 0) return;
if (wkof) evolveHeader(getHeader(sentences));
sentences.forEach(sentence => {
const japaneseSentence = sentence.querySelector('p[lang="ja"]');
const sentenceText = japaneseSentence.textContent;
const audioButton = createAudioButton(sentenceText);
const chars = Array.from(sentenceText);
const newNodes = chars.map(char => tagAndLinkKanji(char));
japaneseSentence.replaceChildren(...newNodes);
japaneseSentence.append(audioButton);
});
highlightKanji();
}
function evolveHeader(header) {
const settings = document.createElement("i");
settings.setAttribute("class", "icon-gear");
settings.setAttribute(
"style",
"font-size: 14px; cursor: pointer; vertical-align: middle; margin-left: 10px;"
);
settings.onclick = openSettings;
if (!header.querySelector("i.icon-gear")) header.append(settings);
}
function recreateAudioButtons() {
document.querySelectorAll(`.context-sentence-group > p > span:last-child, .context-sentence-group > p > button:last-child`).forEach(audioButton => audioButton.remove());
const sentences = document.querySelectorAll(".context-sentence-group");
sentences.forEach(sentence => {
const japaneseSentence = sentence.querySelector('p[lang="ja"]');
const sentenceText = japaneseSentence.textContent;
const audioButton = createAudioButton(sentenceText);
japaneseSentence.append(audioButton);
});
}
function createAudioButton(sentence) {
if (state.settings.voice === "google") {
return createAudioButtonGoogleTL(sentence);
} else {
return createAudioButtonSpeechSynthesis(sentence);
}
}
/**
* To fix a weird issue that occur in the session pages(where all audios play
* if the audio for reading the word is clicked),
* we have to create the audio element only for the time of palying the audio
* and remove it afterward
* @param {*} sentence
*/
function createAudioButtonGoogleTL(sentence) {
// contains audio and button as sibiling elements
const audioContainer = document.createElement("span");
const mpegSource = createSource("audio/mpeg", sentence);
const oogSource = createSource("audio/oog", sentence);
const button = document.createElement("button");
button.setAttribute("class", "audio-btn audio-idle");
button.onclick = () => {
if (audioContainer.childElementCount > 1) {
const audio = audioContainer.querySelector("audio");
audio.pause();
button.setAttribute("class", "audio-btn audio-idle");
audio.remove();
return;
}
const audio = document.createElement("audio");
audio.setAttribute("display", "none");
audio.append(mpegSource, oogSource);
audio.onplay = () => {
button.setAttribute("class", "audio-btn audio-play");
};
audio.onended = () => {
button.setAttribute("class", "audio-btn audio-idle");
audio.remove();
};
audioContainer.append(audio);
audio.play();
};
audioContainer.append(button);
return audioContainer;
}
function createAudioButtonSpeechSynthesis(sentence) {
if (!window.SpeechSynthesisUtterance) {
console.warn("Advanced Context Sentence: your browser does not support SpeechSynthesisUtterance which this script utilizes to implement the audio feature. update your browser or use another one if you want that feature");
return null;
}
const button = document.createElement("button");
button.setAttribute("class", "audio-btn audio-idle");
button.onclick = () => {
var msg = new SpeechSynthesisUtterance(sentence);
msg.lang = "ja-JP";
window.speechSynthesis.speak(msg);
msg.onstart = () => {
button.setAttribute("class", "audio-btn audio-play");
};
msg.onend = () => {
button.setAttribute("class", "audio-btn audio-idle");
};
};
return button;
}
//--------------------------------------------------------------------------------------------------------------//
//----------------------------------------------SETTINGS--------------------------------------------------------//
//--------------------------------------------------------------------------------------------------------------//
function loadSettings() {
return wkof.Settings.load(scriptId, state.settings);
}
function proccessLoadedSettings() {
state.settings = wkof.settings[scriptId];
}
function openSettings() {
var config = {
script_id: scriptId,
title: scriptName,
on_save: updateSettings,
content: {
highlightColors: {
type: "section",
label: "Highlights"
},
recognizedKanjiColor: {
type: "color",
label: "Recognized Kanji",
hover_tip:
"Kanji you should be able to recognize will be highlighted using this color",
default: state.settings.recognizedKanjiColor
},
unrecognizedKanjiColor: {
type: "color",
label: "Unrecognized Kanji",
hover_tip:
"Kanji you shouldn't be able to recognize will be highlighted using this color",
default: state.settings.unrecognizedKanjiColor
},
recognitionLevel: {
type: "dropdown",
label: "Recognition Level",
hover_tip:
"Any kanji with this level or higher will be highlighted with the 'Recognized Kanji' color",
default: state.settings.recognitionLevel,
content: {
1: stringfySrs(1),
2: stringfySrs(2),
3: stringfySrs(3),
4: stringfySrs(4),
5: stringfySrs(5),
6: stringfySrs(6),
7: stringfySrs(7),
8: stringfySrs(8),
9: stringfySrs(9)
}
},
tooltip: {
type: "section",
label: "Tooltip"
},
show: {
type: "checkbox",
label: "Show Tooltip",
hover_tip:
"Display a tooltip when hovering on kanji that will display some of its properties",
default: state.settings.tooltip.show,
path: "@tooltip.show"
},
delay: {
type: "number",
label: "Delay",
hover_tip: "Delay in ms before the tooltip is shown",
default: state.settings.tooltip.delay,
path: "@tooltip.delay"
},
position: {
type: "dropdown",
label: "Position",
hover_tip: "The placement of the tooltip",
default: state.settings.tooltip.position,
path: "@tooltip.position",
content: {
top: "Top",
bottom: "Bottom",
right: "Right",
left: "Left"
}
},
voiceSection: {
type: "section",
label: "Voice"
},
voice: {
type: "dropdown",
label: "Voice",
hover_tip: "Select the machine voice that reads the sentence aloud",
default: state.settings.voice,
content: {
browser: "Web Browser",
google: "Google Translate"
}
}
}
};
var dialog = new wkof.Settings(config);
dialog.open();
}
// Called when the user clicks the Save button on the Settings dialog.
function updateSettings() {
state.settings = wkof.settings[scriptId];
highlightKanji();
recreateAudioButtons();
}
//---------------------------------------------------------------------------------------------------------------//
//-------------------------------------------HELPER FUNCTIONS----------------------------------------------------//
//---------------------------------------------------------------------------------------------------------------//
function isPage(page) {
const path = window.location.pathname;
return path.includes(page);
}
function getSessionDependingOnPage() {
let result = null;
sessions.forEach(session => {
if (isPage(session.page)) result = session;
});
return result;
}
function tagAndLinkKanji(char) {
return isKanji(char) ? wrapInAnchor(char) : document.createTextNode(char);
}
/**
* Determine if the character is a Kanji, inspired by https://stackoverflow.com/a/15034560
*/
function isKanji(char) {
return isCommonOrUncommonKanji(char) || isRareKanji(char);
}
function isCommonOrUncommonKanji(char) {
return char >= "\u4e00" && char <= "\u9faf";
}
function isRareKanji(char) {
char >= "\u3400" && char <= "\u4dbf";
}
/**
* Renders the link for the kanji
* Knji pages always use https://www.wanikani.com/kanji/{kanji} where {kanji} is the kanji character
*/
function wrapInAnchor(char) {
const anchor = document.createElement("a");
anchor.setAttribute("target", "_blank");
anchor.setAttribute("class", "recognized");
if (!wkof) {
anchor.setAttribute("href", `https://www.wanikani.com/kanji/${char}`);
anchor.innerText = char;
return anchor;
}
const kanji = state.kanjis.find(item => item.char == char);
anchor.setAttribute("data-srs", kanji ? kanji.srs : -1);
anchor.setAttribute("data-kanji", char);
anchor.setAttribute(
"href",
kanji ? kanji.url : `https://jisho.org/search/${char}`
);
anchor.innerText = char;
return anchor;
}
function createTooltip(kanji) {
if (!wkof) {
const container = document.createElement("span");
return container;
}
const container = document.createElement("div");
container.setAttribute("class", "acs-tooltip");
if (!kanji) {
const span = document.createElement("span");
span.innerText = "Wanikani doesn't have this kanji! :(";
container.append(span);
return container;
}
const onyomis = kanji.readings.filter(
item => item.type.toLocaleLowerCase() === "onyomi"
);
const kunyomis = kanji.readings.filter(
item => item.type.toLocaleLowerCase() === "kunyomi"
);
const onyomi = stringfyArray(onyomis, item => item.reading);
const kunyomi = stringfyArray(kunyomis, item => item.reading);
const meaning = stringfyArray(kanji.meanings, item => item.meaning);
container.append(generateInfo("LV", kanji.level));
container.append(generateInfo("EN", meaning));
if (onyomi !== "None" && onyomi !== "")
container.append(generateInfo("ON", onyomi));
if (kunyomi !== "None" && kunyomi !== "")
container.append(generateInfo("KN", kunyomi));
container.append(generateInfo("SRS", stringfySrs(kanji.srs)));
if (state.jiff) {
container.append(generateInfo("JOYO", kanji.joyo));
container.append(generateInfo("JLPT", kanji.jlpt));
container.append(generateInfo("FREQ", kanji.frequency));
}
return container;
}
function stringfyArray(array, pathToString) {
let stringfied = "";
array.forEach(item => {
stringfied = stringfied.concat(pathToString(item) + ", ");
});
stringfied = stringfied.substring(0, stringfied.length - 2);
return stringfied;
}
function stringfySrs(srs) {
switch (srs) {
case -1:
return "Locked";
case 0:
return "Ready To Learn";
case 1:
return "Apprentice 1";
case 2:
return "Apprentice 2";
case 3:
return "Apprentice 3";
case 4:
return "Apprentice 4";
case 5:
return "Guru 1";
case 6:
return "Guru 2";
case 7:
return "Master";
case 8:
return "Enlightened";
case 9:
return "Burned";
default:
return "";
}
}
function generateInfo(title, info) {
const container = document.createElement("div");
const key = document.createElement("span");
key.setAttribute("class", "acs-tooltip-header");
const value = document.createElement("span");
key.innerText = title;
value.innerText = info;
container.append(key, " ", value);
return container;
}
function getKanji() {
const filters = {
item_type: ["kan"]
};
if (wkof.get_state("JJFFilters") === "ready") {
state.jiff = true;
filters.include_frequency_data = true;
filters.include_jlpt_data = true;
filters.include_joyo_data = true;
} else {
console.warn(
`${scriptName}: You don't have Open Framework JLPT Joyo and Frequency Filters by @Kumirei installed (version 0.1.4 or later). Install the script if you want to get more information while hovering on Kanji on Context Sentences. Script URL: https://community.wanikani.com/t/userscript-open-framework-jlpt-joyo-and-frequency-filters/35096`
);
}
return wkof.ItemData.get_items({
wk_items: {
options: {
assignments: true
},
filters
}
});
}
function extractKanjiFromResponse(items) {
const kanjis = [];
items.forEach(item => {
const kanji = {
char: item.data.characters,
readings: item.data.readings,
level: item.data.level,
meanings: item.data.meanings,
url: item.data.document_url,
srs: item.assignments ? item.assignments.srs_stage : -1,
jlpt: item.jlpt_level,
joyo: item.joyo_grade,
frequency: item.frequency
};
kanjis.push(enhanceWithAditionalFilters(kanji, item));
});
state.kanjis = kanjis;
}
function enhanceWithAditionalFilters(kanji, item) {
if (state.jiff) {
kanji.jlpt = item.jlpt_level;
kanji.joyo = item.joyo_grade;
kanji.frequency = item.frequency;
}
return kanji;
}
function createSource(type, sentence) {
const source = document.createElement("source");
source.setAttribute("type", type);
source.setAttribute(
"src",
`https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=ja&total=1&idx=0&q=${encodeURIComponent(sentence)}`
);
return source;
}
let tippys = new Set();
function highlightKanji() {
const rules = document.querySelector("#acs-style").sheet.cssRules;
rules[0].style.color = state.settings.recognizedKanjiColor;
rules[1].style.color = state.settings.unrecognizedKanjiColor;
if (!wkof) return;
tippys.forEach(t => t.destroy());
tippys = new Set();
const anchors = document.querySelectorAll(".context-sentence-group a");
anchors.forEach(anchor => {
const srs = anchor.getAttribute("data-srs");
const char = anchor.getAttribute("data-kanji");
if (srs >= state.settings.recognitionLevel)
anchor.setAttribute("class", "recognized");
else {
anchor.setAttribute("class", "unrecognized");
}
if (state.settings.tooltip.show) {
const kanji = state.kanjis.find(item => item.char == char);
const tooltip = createTooltip(kanji);
tippy(anchor, {
content: tooltip,
size: "small",
arrow: true,
placement: state.settings.tooltip.position,
delay: [state.settings.tooltip.delay, 20]
});
tippys.add(anchor._tippy);
}
});
}
// Neccessary in order for audio to work
function createReferrer() {
const remRef = document.createElement("meta");
remRef.name = "referrer";
remRef.content = "no-referrer";
document.querySelector("head").append(remRef);
}
// Styles
function createStyle() {
const style = document.createElement("style");
style.setAttribute("id", "acs-style");
style.innerHTML = `
/* Kanji */
/* It's important for this one to be the first rule*/
${recognizedSelector} {
}
/* It's important for this one to be the second rule*/
${unrecognizedSelector} {
}
.context-sentence-group p a {
text-decoration: none;
}
.context-sentence-group p a:hover {
text-decoration: none;
}
.acs-tooltip {
text-align: left
}
.acs-tooltip-header {
color: #929292
}
`;
document.querySelector("head").append(style);
}
})();
I have also added a setting for selecting the machine voice that reads the sentences aloud. The choices are “Google Translate” (sends the sentence to Google to get the audio file) and “Web Browser” (uses the text-to-speech provided by your web browser). I have added this setting because for some people in this thread (including me), the Google Translate hack did not work for several weeks in the past.