IME2Furigana

When inputting kanji with an IME, furigana is automatically added.

As of 2019-09-20. See the latest version.

// ==UserScript==
// @name         IME2Furigana
// @namespace    ime2furigana
// @version      0.3
// @description  When inputting kanji with an IME, furigana is automatically added.
// @author       Sinyaven
// @match        https://community.wanikani.com/*
// @grant        none
// ==/UserScript==

(function() {
    "use strict";

	let furigana = "";
	let mode = 0;
	let bMode = null;
	let tText = null;

	let dObserverTarget = document.getElementById("reply-control");
	let observer = new MutationObserver(m => m.forEach(handleMutation));
	observer.observe(dObserverTarget, {childList: true, subtree: true});

	function handleMutation(mutation) {
		let addedNodes = Array.from(mutation.addedNodes);
		addedNodes.filter(n => n.tagName === "TEXTAREA").forEach(n => {tText = n; n.addEventListener("compositionupdate", update); n.addEventListener("compositionend", addFurigana)});
		addedNodes.filter(n => n.classList && n.classList.contains("d-editor-button-bar")).forEach(addButton);
	}

	function addButton(div) {
		bMode = document.createElement("button");
		bMode.className = "btn no-text btn-icon ember-view";
		bMode.innerText = "F";
		updateButton();
		bMode.addEventListener("click", toggleActive);
		div.appendChild(bMode);
	}

	function toggleActive() {
		mode++;
		if (mode === 3) mode = 0;
		updateButton();
		if (tText) tText.focus();
	}

	function updateButton() {
		bMode.style.backgroundColor = mode ? "#00000042" : "";
		bMode.style.filter = mode === 2 ? "blur(2px)" : "";
		bMode.title = "IME2Furigana - " + (mode ? (mode === 1 ? "on" : "blur") : "off");
	}

	function update(event) {
		if (/^[\u301c\u3041-\u309fnー]+$/.test(event.data)) {
			furigana = event.data;
		}
	}

	function addFurigana(event) {
		if (!mode || event.data.length === 0) return;
		furigana = furigana.replace(/n/g, "ん");
		let parts = event.data.split(/([\uff66-\uff9d\u4e00-\u9faf\u3400-\u4dbf]+)/);
		if (parts.length === 1) return;
		let hiraganaParts = parts.map(p => Array.from(p).map(c => katakanaToHiragana(c)).join(""));
		//let regex = new RegExp("^" + parts.map((p, idx) => idx & 1 ? (p ? "(.+)" : "()") : p).join("") + "$");
		let regex = new RegExp("^" + hiraganaParts.map((p, idx) => "(" + (idx & 1 ? ".+" : p) + ")").join("") + "$");
		let rt = furigana.match(regex);
		if (!rt) {
			parts = [event.data];
			rt = [null, furigana];
		}
		rt.shift();
		let rtStart = "<rp>(</rp><rt>" + (mode === 2 ? "[spoiler]" : "");
		let rtEnd = (mode === 2 ? "[/spoiler]" : "") + "</rt><rp>)</rp>";
		let ruby = parts.map((p, idx) => idx & 1 ? "<ruby>" + p + rtStart + rt[idx] + rtEnd + "</ruby>" : p).join("");
		//if (event.data === furigana) return;
		//let ruby = "<ruby>" + event.data + "<rt>" + furigana + "</rt></ruby>";
		event.target.setRangeText(ruby, event.target.selectionStart - event.data.length, event.target.selectionStart, "end");
	}

	function katakanaToHiragana(k) {
		return (k + "ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわをんヴヵヶ")["ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピヘベペホボポマミムメモャヤュユョヨラリルレロヮワヲンヴヵヶ".indexOf(k) + 1];
	}
})();