// ==UserScript==
// @name Cloze-Test Generator
// @description Generate cloze tests for both Japanese and English texts, from any highlighted text on any site.
// @namespace https://gf.qytechs.cn/en/users/3656-kaiko
// @require https://gf.qytechs.cn/scripts/23318-tinysegmenter/code/TinySegmenter.js?version=148172
// @version 1.0
// @grant GM_registerMenuCommand
// @include *
// ==/UserScript==
GM_registerMenuCommand("Run a Cloze Test", getSelectionText);
var styled = false;
var refreshTest = false;
var prevText;
function getSelectionText() {
//Make sure we only have one quiz instance at a time
var sanityCheck = document.getElementsByClassName("cloze-container-popup");
//Get our selected text
var text = "";
if (sanityCheck.length === 0){
if (window.getSelection) {
text = window.getSelection().toString();
} else if (document.selection && document.selection.type != "Control") {
text = document.selection.createRange().text;
}
refreshTest = false;
}
//Restart the process if we change difficulty during run-time
//After changing difficulty, see if we have already run the quiz first, and re-run the test to avoid duplicate test overlays.
var currentDifficulty = localStorage.getItem("kaiko_clozeTestCurDif");
var previousDifficulty = localStorage.getItem("kaiko_clozeTestPrevDif");
//Restart!
if (currentDifficulty !== null && previousDifficulty !== null){
if (currentDifficulty !== previousDifficulty && sanityCheck.length > 0) {
refreshTest = true;
if (prevText !== undefined){
text = prevText;
}
localStorage.setItem("kaiko_clozeTestPrevDif", currentDifficulty);
}
}
//Start a new process
if (text !== ""){
if (sanityCheck.length === 0 || refreshTest === true) {
//Store our selected text, in case difficulty is changed later
prevText = text;
//Style the quiz popup overlay
if (!styled){
var cssNode = document.createElement('style');
cssNode.innerHTML = '.clozetitle,.clozeTestOptions{font-family:Verdana,Geneva,sans-serif}.cloze-container-popup,.cloze-popup{z-index:99999 !important;top:0 !important;right:0 !important;bottom:0 !important;left:0 !important}.cloze-container-popup{position:fixed;background:rgba(0,0,0,.85) !important}.cloze-popup{width:60% !important;height:80% !important;background:#2C3E50 !important;color:#fff !important;position:absolute !important;margin:auto !important;padding:20px !important;font-size:18px !important;overflow-y:scroll !important;line-height:30px !important;display:table-cell !important;vertical-align:middle !important;text-align:justify !important;}.clozetitle,.clozeTestOptions{background-color:#34495E !important}.clozetitle{text-indent:50px !important;letter-spacing:6px !important;color:#fff !important;font-size:25px !important;text-align:center !important;padding:15px !important;margin:0 0 10px !important}.clozeTestOptions{padding:5px !important;display:block !important}.content{height:100% !important;position:relative !important}.textareaform{display:inline !important;background-color:#395168 !important;box-shadow:none !important;color:#fff !important;resize:none !important;font-family:Meiryo-UI;border:1px solid #000 !important;padding:1px !important;margin:0 5px -4px !important;min-height:20px !important;max-height:20px !important;max-width:125px !important;}.closebtn,.resetbtn{text-transform:none !important; box-shadow:none !important; line-height:1 !important; cursor:pointer !important; color:#fff !important;border:2px solid #0C8698 !important}.btnbg,.closebtn{font-family:Verdana,Geneva,sans-serif}@-moz-document url-prefix(){.textareaform{margin:0 5px 4px !important;max-height:22px !important}}.btnbg{background-color:#34495E !important;padding:10px !important;bottom:0 !important;display:block !important}.closebtn,.closebtn:hover,.resetbtn,.resetbtn:hover{background:#00AEC8 !important;text-decoration:none !important}.closebtn{font-size:20px !important;padding:5px !important}.resetbtnbg{position:absolute !important;display:inline !important;right:15px !important}.resetbtn{font-size:15px !important;padding:5px !important}a.copyright:active,a.copyright:link,a.copyright:visited{color:#d30 !important;text-decoration:none !important}a.copyright:hover{text-decoration:underline !important}.clozeTestForm{margin:5px !important; line-height:2 !important;font-family:"ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro",Osaka, "メイリオ", Meiryo, "MS Pゴシック", "MS PGothic", sans-serif; font-weight:normal !important;}.clozeTestForm .input{display:table-cell !important; position:relative !important;}.clozeTestDifficultyDrpdwn{background:#fff !important;color:#000 !important;width:auto !important;height:auto !important;padding:0 !important;font-size:14px !important;line-height:1 !important;border:0 !important;border-radius:0 !important;}';
document.body.appendChild(cssNode);
styled = true;
}
//Remove the old test instance if difficulty changed
if (refreshTest) {
closeQuiz();
}
//Start segmentation
var segmenter = new TinySegmenter();
var segs = segmenter.segment(text);
// Make the output cleaner by separating the output at specific grammar points, and combining verb auxiliaries
var illegalChars = ['を', 'へ', 'に', 'と', 'の', 'は', 'が', 'も', 'こと', 'から', 'まで', 'とき', 'など','ために','ための', '「', '」', 'ず', 'っ', '!', '?', 'しか', 'だけ', 'や', 'か', '、', '。', 'で', 'では', '.', '!', '?', '"', "'", '’', '(', ')', '(', ')', 'and', 'the', 'in', 'to', 'a', 'from', 'when', 'only', 'just', 'of', 'for', 'is', ' ', 'with', 'at', ':', ':', '=', '='];
//Char Codes:
//Hiragana: >= 12353 && <= 12435
//Katakana: >= 12443 && <= 12532
for (var i = 0; i < segs.length; ++i){
if(illegalChars.indexOf(segs[i]) === -1){
if (segs[i+1] && illegalChars.indexOf(segs[i+1]) === -1){
if (segs[i+1] && segs[i].charCodeAt(segs[i].length-1) >= 12353 && segs[i].charCodeAt(segs[i].length-1) <= 12435 && segs[i+1].charCodeAt(0) >= 12353 && segs[i+1].charCodeAt(0) <= 12435){
segs[i] += segs[i+1];
segs.splice(i+1, 1);
i = i-1;
}
else if(segs[i+1] && segs[i+1].length == 1){
segs[i] += segs[i+1];
segs.splice(i+1, 1);
i = i-1;
}
}
}
//Accepted combinations: のを, ことを, への, までの, からの, とても, して, って, った
//i, i+1
else if(segs[i+1] && segs[i] == 'の' && segs[i+1] == 'を' || segs[i] == 'こと' && segs[i+1] == 'を' || segs[i] == 'へ' && segs[i+1] == 'の' ||
segs[i] == 'まで' && segs[i+1] == 'の' || segs[i] == 'から' && segs[i+1] == 'の' || segs[i] == 'と' && segs[i+1].startsWith('ても') || segs[i].endsWith('し') && segs[i+1].startsWith('て') || segs[i].endsWith('っ') && segs[i+1].startsWith('て') || segs[i].endsWith('っ') && segs[i+1].startsWith('た')){
segs[i] += segs[i+1];
segs.splice(i+1, 1);
i = i-1;
}
//Accepted combinations: 子どもたち, ず-ending-kanji-starting words (ie: verbs?)
//-1, i, i+1
else if (segs[i-1] == '子ども' && segs[i] == 'も' && segs[i+1] == 'たち' || /^[\u4e00-\u9faf]+$/.test(segs[i-1]) && segs[i+1].endsWith('ず') === -1){
segs[i] = segs[i-1] + segs[i] + segs[i+1];
segs.splice(i-1, 1);
segs.splice(i, 1);
i = i-2;
}
//Accepted combinations (with prior term): など, とか, ず
//i-1, i
else if (segs[i] == 'など' || segs[i] == 'とか' || segs[i] == 'ず'){
segs[i] = segs[i-1] + segs[i];
segs.splice(i-1, 1);
segs.splice(i, 1);
i = i-1;
}
}
// Cloze-Test by a factor of every X destignated item
// 'Remove X number of items every Y number of items'?
// (set the value for a few at a time, and adjust for-loop placement)
var skippedChars = ['、', '。', '「', '"', '”', '’','?','!', '」', 'こと', 'から', 'まで', 'とき', 'など', 'ことが', 'のは', 'のが', 'ことは', 'では', 'という', 'ても', 'ために', 'ための', 'and', 'the', 'in', 'to', 'a', 'from', 'when', 'only', 'just', 'of', 'for', 'is',' ', 'with', 'at', '(', ')'];
var factorOfItemToChange;
if (currentDifficulty && factorOfItemToChange === undefined){
factorOfItemToChange = currentDifficulty;
localStorage.setItem("kaiko_clozeTestPrevDif", factorOfItemToChange);
} else {
factorOfItemToChange = 3;
localStorage.setItem("kaiko_clozeTestPrevDif", factorOfItemToChange);
localStorage.setItem("kaiko_clozeTestCurDif", factorOfItemToChange);
}
//Create an array to store our answers in.
var selectedEntries = [];
var colLen;
var engCount = 0;
var nonEngCount = 0;
var segJoin = "";
for (var i = 0; i < segs.length; ++i) {
//Check if the text is mostly English or not
if (/[a-z]/i.test(segs[i])){
++engCount;
} else {
++nonEngCount;
}
// If the modulus of the index + 1 (it's order in non-index notation) is 0
// (divides perfectly by the value passed in with no remainder)
if ((i + 1) % factorOfItemToChange === 0) {
//Exclude banned terms
if(skippedChars.indexOf(segs[i]) === -1 && segs[i].length != 1){
colLen = segs[i].length *2.25;
selectedEntries.push(segs[i] + '_-' + i + '-_');
segs[i] = '<textarea class="textareaform" rows="1" cols="' + colLen + '" id="' + segs[i] + '_-' + i + '-_' + '"></textarea>';
}
else if (segs[i+1] && skippedChars.indexOf(segs[i+1]) === -1 && segs[i+1].length != 1){
colLen = segs[i+1].length *2.25;
selectedEntries.push(segs[i+1] + '_-' + i + '-_');
segs[i+1] = '<textarea class="textareaform" rows="1" cols="' + colLen + '" id="' + segs[i+1] + '_-' + i + '-_' + '"></textarea>';
i = i+1;
}
}
}
//Set out join character between text
if (engCount >= nonEngCount){
segJoin = " ";
}
//Store our IDs (answers) of each textarea to compare later.
localStorage.setItem('kaiko_clozeTestAnswers', JSON.stringify(selectedEntries));
//Create the popup cloze test.
window.document.body.innerHTML += '<div class="cloze-container-popup"><div class="cloze-popup"><div class="clozetitle">CLOZE TEST</div>' + '<div class="clozeTestOptions">Difficulty: Remove every <select id="clozeTestDifficultyDrpdwn" class="clozeTestDifficultyDrpdwn"><option><option value="2">2nd<option value="3">3rd<option value="4">4th<option value="5">5th<option value="6">6th<option value="7">7th<option value="8">8th<option value="9">9th<option value="10">10th</select> word (+1 if grammar particle) <div class="resetbtnbg"><button id="resetbtn" class="resetbtn">Reset Form</button></div> </div>' + '<p id="clozeTestForm" class="clozeTestForm">' + segs.join(segJoin) + '</p><div class="btnbg"><center><button id="checkAnswers" class="closebtn">Show Answers</button> <button id="closebtn" class="closebtn">Close</button><br /><a href="https://kainokage.wordpress.com" target="_new" class="copyright">Made With Love</a> ・ <a href="https://gf.qytechs.cn/en/scripts/18228-automatically-hide-ruby-based-furigana" target="_new" class="copyright">Furigana Issues?</a></center></div></div></div>';
//Add eventListeners for GUI functions
document.getElementById("clozeTestDifficultyDrpdwn").addEventListener("change", changeDifficulty);
document.getElementById("resetbtn").addEventListener("click", resetForm);
document.getElementById("checkAnswers").addEventListener("click", checkAnswers);
document.getElementById("closebtn").addEventListener("click", closeQuiz);
}
}
return text;
}
//Implement closeQuiz(), changeDifficulty(), checkAnswers() and resetForm().
function changeDifficulty () {
var chosenDifficulty = document.getElementById("clozeTestDifficultyDrpdwn").value;
localStorage.setItem("kaiko_clozeTestCurDif", chosenDifficulty);
//Restart!
var currentDifficulty = localStorage.getItem("kaiko_clozeTestCurDif");
var previousDifficulty = localStorage.getItem("kaiko_clozeTestPrevDif");
if (currentDifficulty !== previousDifficulty) {
getSelectionText();
return;
}
}
function resetForm () {
localStorage.setItem('kaiko_clozeTestPrevDif', 0);
getSelectionText();
return;
}
function checkAnswers () {
var clozeTestAnswers = JSON.parse(localStorage.getItem("kaiko_clozeTestAnswers"));
var clozeInput;
var clozeInputValue;
var clozeTestForm = document.getElementById(clozeTestForm);
var chosenClozeTestAnswer;
var styledChosenClozeTestAnswer;
for (var i = 0; i < clozeTestAnswers.length; ++i){
clozeInput = document.getElementById(clozeTestAnswers[i]);
clozeInputValue = clozeInput.value;
chosenClozeTestAnswer = clozeTestAnswers[i].replace(/_-(.*)-_/, "");
if (clozeInputValue != chosenClozeTestAnswer){
styledChosenClozeTestAnswer = "<span style=\"color:lightgreen\">" + chosenClozeTestAnswer+ "</span>";
clozeInput.insertAdjacentHTML("afterend", styledChosenClozeTestAnswer);
}
}
clozeTestAnswers = [];
localStorage.setItem("kaiko_clozeTestAnswers", "");
}
function closeQuiz () {
var containerPopup = document.getElementsByClassName("cloze-container-popup");
containerPopup[0].parentNode.removeChild(containerPopup[0]);
}