function charNotLatin(input) { var code = input.charCodeAt(0); if (0x0000 <= code && code <= 0x007f) return false; // basic latin return true; } function charNotJapanese(input) { var code = input.charCodeAt(0); if (0x3000 <= code && code <= 0x303f) return false; // japanese punctuation if (0x3040 <= code && code <= 0x309f) return false; // hiragana if (0x30a0 <= code && code <= 0x30ff) return false; // katakana if (0xff00 <= code && code <= 0xffef) return false; // full-width latin + half-width katakana if (0x4e00 <= code && code <= 0x9faf) return false; // kanji return true; } function calculateTagHue(input) { var out = 0; for (var i = 0; i < input.length; i++) out ^= input.charCodeAt(i); return Math.floor((out * 12) % 360); } function HTMLtoParseArr(input) { var out = []; var node = { data: "", type: "text" }; var tag_open = false; var new_node = false; var clear = () => { out.push(node); node = { data: "", type: "text" }; }; for (var i = 0; i < input.length; i++) { new_node = false; if (input[i] == "<") { clear(); tag_open = true; node.type = "html"; } if (input[i] == ">" && tag_open == true) { tag_open = false; node.data += input[i]; clear(); continue; } node.data += input[i]; } if (new_node == false) out.push(node); return out; } function parseOnTextOnly(nodes, parseFn) { for (var node of nodes) { if (node.type == "html") continue; node.data = parseFn(node.data); } return HTMLtoParseArr(nodes.map(n => n.data).join("")); // re-parse for newly created html } function parseFormat(nodes) { return parseOnTextOnly(nodes, input => { var bold = false; // currently bold var italic = false; // currently italic var out = ""; for (var i = 0; i < input.length; i++) { // escape characters preceded by \ if (input[i] == "\\") { var escaped = input[i+1]; if (escaped == "n") { out += "
"; i++; continue; } // newline if (escaped == "t") { out += "\t"; i++; continue; } // tab if (escaped == "*") { out += "*"; i++; continue; } // bold if (escaped == "_") { out += "_"; i++; continue; } // italic if (escaped == "\\") { out += "\\"; i++; continue; } // literal backslash } // parse *test* into test if (input[i] == "*") { bold = !bold; out += `<${bold ? "" : "/"}b>`; continue; } // parse _test_ into test if (input[i] == "_") { italic = !italic; out += `<${italic ? "" : "/"}i>`; continue; } out += input[i]; } return out; }); } function parseIndicators(nodes) { return parseOnTextOnly(nodes, input => { var indicator = false; // indicator is open var content = ""; // indicator content var stamp = ""; // filled if indicator has stamp var out = ""; for (var i = 0; i < input.length; i++) { // escape characters preceded by \ if (input[i] == "\\") { var escaped = input[i+1]; if (escaped == "[") { out += "["; i++; continue; } if (escaped == "]") { out += "]"; i++; continue; } if (escaped == "-" && indicator) { content += "-"; i++; continue; } if (escaped == "\\") { out += "\\"; i++; continue; } } if (input[i] == "[") { indicator = true; out += ``; continue; } if (input[i] == "]" && indicator) { indicator = false; if (stamp) out += `${stamp}`; out += `${content}`; content = ""; stamp = ""; continue; } if (input[i] == "-" && indicator) { stamp = content; content = ""; continue; } if (indicator) content += input[i]; else out += input[i]; } return out; }); } function parseFurigana(nodes) { return parseOnTextOnly(nodes, input => { var mode = "normal"; // normal, kanji, reading var out = ""; // output html var alwaysvisisble = false; // if furigana is always visible (on front of card) var kanji = ""; // current kanji var reading = ""; // current kanji reading var normal = ""; // normal text var flush_normal = () => { out += `${normal}`; normal = ""; }; for (var i = 0; i < input.length; i++, lastmode = mode) { // parse [kanji](reading) into ruby text // [kanji](reading) is only visible on card back // {kanji}(reading) is always visible if (mode == "normal" && input[i] == "[") // hidden reading kanji open { kanji = ""; mode = "kanji"; alwaysvisisble = false; continue; } if (mode == "normal" && input[i] == "{") // always visible reading kanji open { kanji = ""; mode = "kanji"; alwaysvisisble = true; continue; } if (mode == "kanji" && input[i] == "]") continue; // hidden reading kanji close if (mode == "kanji" && input[i] == "}") continue; // always visible reading kanji close if (mode == "kanji" && kanji.length > 0 && input[i] == "(") // reading open { reading = ""; mode = "reading"; continue; } if (mode == "reading" && input[i] == ")") { // reading close flush_normal(); mode = "normal"; out += `${kanji}${reading}`; continue; } // add current character to selected mode buffer if (mode == "normal") normal += input[i]; if (mode == "kanji") kanji += input[i]; if (mode == "reading") reading += input[i]; } flush_normal(); return out; }); } function parseReading(nodes) { return parseOnTextOnly(nodes, input => { var note_head = 0; var note_tail = 0; var out = ""; // output html for (var i = 0; i < input.length; i++) { if (i == 0) { // start kanji reading out += ``; var match = input.match(/\((.+?)\)/); // display "(note)" before kanji if (match) { out += `${match[1]}`; note_head = match.index; note_tail = note_head + match[0].length; } } // ignore note if parsed else if (i == note_head) { i = note_tail - 1; continue; } // reading open bracket if (input[i] == '\u3010') { out += `${input[i]}`; continue; } // reading closing bracket if (input[i] == '\u3011') { out += `${input[i]}`; continue; } // interpunct (syllable separator) if (input[i] == '\u30fb') { out += `${input[i]}`; continue; } out += input[i]; } return out; }); } function parseTags(nodes) { var out = ""; for (var tag of nodes.map(n => n.data).join("").split(" ")) out += `${tag}`; return HTMLtoParseArr(out); } function parseDefinitions(nodes) { out = ``; return HTMLtoParseArr(out); } function parseScript(nodes) { return parseOnTextOnly(nodes, input => { if (input.length == 0) return input; var lastScript = "unknown"; var out = ""; for (var i = 0; i < input.length; i++) { var script = "unknown"; if (!charNotJapanese(input[i])) script = "japanese"; if (!charNotLatin(input[i])) script = "latin"; if (i == 0) out += ``; else if (script != lastScript) out += ``; out += input[i]; lastScript = script; } return out + ""; }); } function setSpoiler(nodes) { return HTMLtoParseArr(`` + nodes.map(n => n.data).join("") + ""); } HTMLElement.prototype.parse = function() { if (this.classList.contains("parsed")) return; // ignore already parsed elements var input = this.innerHTML; // get raw data from anki field var nodes = HTMLtoParseArr(input); // seperate user text from html formatting (keep html intact) // parsers if (this.classList.contains("parse-format")) nodes = parseFormat(nodes); if (this.classList.contains("parse-furigana")) nodes = parseFurigana(nodes); if (this.classList.contains("parse-reading")) nodes = parseReading(nodes); if (this.classList.contains("parse-indicators")) nodes = parseIndicators(nodes); if (this.classList.contains("parse-tags")) nodes = parseTags(nodes); if (this.classList.contains("parse-definitions")) nodes = parseDefinitions(nodes); if (this.classList.contains("parse-script")) nodes = parseScript(nodes); if (this.classList.contains("spoiler")) nodes = setSpoiler(nodes); // join parsed text with unmodified html var out = nodes.map(n => n.data).join(""); this.innerHTML = out; this.classList.add("parsed"); if (nodes.reduce((current, n) => current + (n.type == "text" ? n.data.length : 0), 0) == 0) this.classList.add("empty"); // if innerHTML only contains empty html (no 'user' text) }; function layout() { // set vertical layout on vertical displays (primarily mobile screens) var el = document.getElementById("card"); if (screen.orientation.type.startsWith("landscape") && el.classList.contains("vertical-layout")) { el.classList.remove("vertical-layout"); el.classList.add("horizontal-layout"); } else if (screen.orientation.type.startsWith("portrait") && el.classList.contains("horizontal-layout")) { el.classList.remove("horizontal-layout"); el.classList.add("vertical-layout"); } } function run() { for (var el of document.getElementsByClassName("parse")) el.parse(); // toggle spoiler by clicking for (var el of document.getElementsByClassName("spoiler")) el.onclick = function () { this.classList.toggle("hidden"); this.classList.toggle("visible"); }; // remove spoiler from sentence translation if word reading field is empty if(document.getElementById("target-word-reading").classList.contains("empty")) document.getElementById("sentence-translation").classList.remove("hidden"); layout(); } run(); window.onload = () => run(); window.onresize = () => layout(); window.ondeviceorientation = () => layout(); window.screen.orientation.onchange = () => layout();