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 charNotNumeric(input) { var code = input.charCodeAt(0); if (0x30 <= code && code <= 0x39) return false; // ascii numbers return true; } function charNotPunctuation(input) { var code = input.charCodeAt(0); if (0x20 == code) return false; // space if (0x21 == code) return false; // exclamation mark if (0x2e == code) return false; // full stop if (0x3f == code) return false; // question mark 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 var writings = [""]; var writingIndex = 0; var mode = "writing"; // parsing mode ("writing" or "reading") var flush_writings = () => { if (writings.reduce((current, n) => current + n.length, 0) == 0) return; out += ``; for(let i = 0; i < writings.length; i++) { if (i == 1) out += ``; if (i > 0) out += `\u3001`; var classes = ["writing"]; if (i == 0) classes.push("first"); out += `${writings[i].trim()}`; } if (writings.length > 1) out += `+${writings.length - 1}`; out += ``; writings = []; writingIndex = 0; }; for (var i = 0; i < input.length; i++) { if (i == 0) { 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 (mode == "writing" && input[i] == '\u3010') { mode = "reading"; flush_writings(); out += `${input[i]}`; continue; } // reading closing bracket if (mode == "reading" && input[i] == '\u3011') { out += `${input[i]}`; continue; } // interpunct (syllable separator) if (mode == "reading" && input[i] == '\u30fb') { out += `${input[i]}`; continue; } // comma (writing separator) if (mode == "writing" && (input[i] == ',' || input[i] == "\u3001")) { writings[++writingIndex] = ""; continue; } if (mode == "writing") writings[writingIndex] += input[i]; else out += input[i]; } flush_writings(); // kana only word fix 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 numberOnly = true; var punctuationOnly = true; var script = "unknown"; var lastScript = "unknown"; var out = ""; var buffer = ""; function flush() { var classes = [`script-${lastScript}`]; if (numberOnly) classes.push("number-only"); if (punctuationOnly) classes.push("punctuation-only"); if (numberOnly || punctuationOnly) classes.push("horizontal-in-vertical"); out += `${buffer}`; buffer = ""; numberOnly = true; punctuationOnly = true; } for (var i = 0; i < input.length; i++) { if (input[i] != " ") { if (!charNotJapanese(input[i])) script = "japanese"; if (!charNotLatin(input[i])) script = "latin"; } if (i != 0 && script != lastScript) flush(); if (charNotNumeric(input[i])) numberOnly = false; if (charNotPunctuation(input[i])) punctuationOnly = false; buffer += input[i]; lastScript = script; } flush(); return out; }); } function setSpoiler(nodes) { return HTMLtoParseArr(`` + nodes.map(n => n.data).join("") + ""); } function parse(input, classes) { var nodes = HTMLtoParseArr(input); // seperate user text from html formatting (keep html intact) // parsers if (classes.includes("parse-format")) nodes = parseFormat(nodes); if (classes.includes("parse-furigana")) nodes = parseFurigana(nodes); if (classes.includes("parse-reading")) nodes = parseReading(nodes); if (classes.includes("parse-indicators")) nodes = parseIndicators(nodes); if (classes.includes("parse-tags")) nodes = parseTags(nodes); if (classes.includes("parse-definitions")) nodes = parseDefinitions(nodes); if (classes.includes("parse-script")) nodes = parseScript(nodes); if (classes.includes("spoiler")) nodes = setSpoiler(nodes); return nodes; }; HTMLElement.prototype.has = function(fn) { if (fn(this)) return true; for (var child of this.children) if (child.has(fn)) return true; return false; }; HTMLElement.prototype.parse = function() { if (this.classList.contains("parsed")) return; // ignore already parsed elements var nodes = parse(this.innerHTML, Array.from(this.classList)); this.classList.add("parsed"); // if innerHTML only contains empty html (no 'user' text) if (nodes.reduce((current, n) => current + (n.type == "text" ? n.data.length : 0), 0) == 0) { this.classList.add("empty"); this.innerHTML = ""; return; } this.innerHTML = nodes.map(n => n.data).join(""); if (this.id == "sentence" && this.has(n => n.tagName == "B")) this.classList.add("has-b"); if (this.id == "target-word-translation" && this.has(n => n.classList.contains("script-latin"))) this.classList.add("has-script-latin"); for (var el of this.getElementsByClassName("horizontal-in-vertical")) { var size = el.getBoundingClientRect(); el.style.setProperty("--self-width", size.width); el.style.setProperty("--self-height", size.height); if (size.width > size.height) el.classList.add("squeeze"); } }; 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"); document.getElementById("sentence-translation").classList.add("visible"); } layout(); } run(); window.onload = () => run(); window.onresize = () => layout(); window.ondeviceorientation = () => layout(); window.screen.orientation.onchange = () => layout();