// Copyright 2017; Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0 /* * TODO: * * ! Finish porting the underlay's deconjugation rules * ! Definition priority handling (prefer words with any commonness tags, shorter deconjugations, expressions, non-archaic/non-obscure words, high frequencies) * - Fix katakana-hiragana matching (only happens in lookups right now, not display generation) * - More configurability * - VNstats frequency data * - Load deconjugation rules from an advanced setting, with a reset button * - Work with text input fields * ? List definitions of shorter text strings if they're not pure kana (in addition to main definitions) * */ 'use strict'; function print_object(message) { //console.log(JSON.stringify(message)); console.log(message); } let settings = { enabled: false, compact: true, length: 25, fixedwidth: false, fixedwidthpositioning: false, }; let last_time_display = Date.now(); let div_class = "nazeka_fGKRTDGFGgr9atT"; function delete_div () { let other = document.body.getElementsByClassName(div_class); if(other.length > 0) { other[0].style.visibility = "hidden"; if(other[0].children.length > 0) other[0].children[0].innerHTML = ""; } } function display_div (middle, x, y, time) { middle.style = "background-color: #111; border-radius: 2.5px; border: 1px solid #111;"; middle.firstChild.style = "border: 1px solid white; border-radius: 2px; padding: 2px; background-color: #111; color: #CCC; font-family: Arial, sans-serif; font-size: 13px; text-align: left;"; let find_root = window; let newx = x; let newy = y; while(find_root.parent && find_root.parent != find_root) { let rect = find_root.frameElement.getBoundingClientRect(); let sx = find_root.scrollX; let sy = find_root.scrollY; find_root = find_root.parent; newx += (rect.x - sx); newy += (rect.y - sy); } let mydoc = find_root.document; let styletext = ""; if(settings.fixedwidth) styletext += "width: 600px; " else styletext += "max-width: 600px; " styletext += "position: absolute; top: 0; left: 0;"; styletext += "background-color: white; border-radius: 3px; border: 1px solid white; z-index: 100000;"; let other = mydoc.body.getElementsByClassName(div_class); let outer = undefined; if(other.length > 0) { outer = other[0]; outer.replaceChild(middle, other[0].firstChild); outer.style.visibility = "visible"; } else { outer = mydoc.createElement("div"); outer.className = div_class; outer.appendChild(middle); mydoc.body.appendChild(outer); } outer.style = styletext; let mywidth = 600; if(!settings.fixedwidthpositioning) mywidth = outer.offsetWidth; let buffer = 25; let pretend_doc_width = Math.max(mywidth, mydoc.defaultView.innerWidth); if(newx + mywidth > pretend_doc_width) { newx -= (newx + mywidth - pretend_doc_width); newx -= buffer; if(newx < 0) newx = 0; } outer.style.top = (newy+5)+"px"; outer.style.left = (newx+5)+"px"; } function exists_div() { let find_root = window; while(find_root.parent && find_root.parent != find_root) find_root = find_root.parent; let mydoc = find_root.document; let other = document.body.getElementsByClassName(div_class); return (other.length > 0 && other[0].style.visibility != "hidden"); } // In JMdict, part-of-speech tags are XML entities. // We processed JMdict's XML with entity processing disabled so we can just use the bare tags (e.g. "v1", not "ichidan verb"). // This left the entity syntax &this; behind so we want to cut it off when we return results. function clip(str) { if(str == undefined) return; if(str.length <= 2) return ""; return str.substring(1, str.length-1); } // FIXME: redundant garbage, find a way to deduplicate a lot of the logical parts of this function build_div (text, result) { //console.log("Displaying:"); //print_object(result); let middle = document.createElement("div"); let temp = document.createElement("div"); temp.innerHTML += ""; //temp.innerHTML += "Looked up " + text + "
"; // lookups can have multiple results (e.g. する -> 為る, 刷る, 掏る, 剃る, 擦る) // FIXME: A bunch of code here depends on the literal text used to run the search instead of the text with which the search succeeded. // The search can convert between hiragana and katakana to find a valid match, so we should know what text it actually used. for(let i = 0; i < result.length; i++) { let term = result[i]; if(term.deconj.size > 0) text = term.deconj.values().next().value.text; //print_object(term); let temptag = ""; let found_kanji = true; if(term.k_ele) { // pick out which index has the kanji we looked up if we looked up kanji let which = 0; let looked_up_kanji = true; while(which < term.k_ele.length && term.k_ele[which].keb != text) which += 1; if(which >= term.k_ele.length) { which = 0; looked_up_kanji = false; } let whichkana = 0; let r_restr = []; if(!looked_up_kanji) { while(whichkana < term.r_ele.length && term.r_ele[whichkana].reb != text) whichkana += 1; if(whichkana >= term.r_ele.length) whichkana = 0; whichkana = term.r_ele[whichkana]; if(whichkana.restr && whichkana.restr.length > 0) r_restr = whichkana.restr; for(let j = 0; j < term.k_ele.length; j++) { if(term.k_ele[j].restr) { for(let l = 0; l < term.k_ele[j].restr.length; l++) { if(term.k_ele[j].restr[l] == text) { if(!r_restr || r_restr.indexOf(term.k_ele[j].keb) > -1) { which = j; found_kanji = true; break; } } } } else { if(!r_restr || r_restr.indexOf(term.k_ele[j].keb) > -1) { which = j; found_kanji = true; break; } } } } else found_kanji = true; if(found_kanji) { let kanji_text = term.k_ele[which].keb; temptag += "" temptag += kanji_text; temptag += "" // FIXME: show inf for located keb // list readings let readings = []; // don't list readings if restricted to kanji we didn't look up for(let j = 0; j < term.r_ele.length; j++) { let r = term.r_ele[j]; let invalid = r.restr != undefined; if(r.restr) { for(let l = 0; l < r.restr.length; l++) if(r.restr[l] == kanji_text) invalid = false; } if(!invalid) readings.push(term.r_ele[j]); } if(term.deconj) { for(let form of term.deconj) { if(form.process.length > 0) temptag += "~"; for(let f = form.process.length-1; f >= 0; f--) { let info = form.process[f]; if(info.startsWith("(") && info.endsWith(")") && f != 0) continue; temptag += info; if(f > 0) temptag += "―"; } } } temptag += " 《"; for(let j = 0; j < readings.length; j++) { temptag += readings[j].reb; if(readings[j].inf) { for(let info of readings[j].inf) { temptag += " ("; temptag += clip(info); temptag += ")"; } } if(j+1 != readings.length) temptag += "・"; } temptag += "》"; // list alternatives let alternatives = []; // don't list spellings if restricted to kana we didn't look up for(let j = 0; j < term.k_ele.length; j++) { if(term.k_ele[j].keb != kanji_text) { let k = term.k_ele[j]; let invalid = !looked_up_kanji && k.restr; if(!looked_up_kanji && k.restr) { for(let l = 0; l < k.restr.length; l++) if(k.restr[l] == text) invalid = false; } if(!looked_up_kanji && r_restr !== [] && r_restr.length > 0 && r_restr.indexOf(k.keb) < 0) invalid = true; if(!invalid) alternatives.push(term.k_ele[j]); } } if(alternatives.length > 0) temptag += " (also "; for(let j = 0; j < alternatives.length; j++) { temptag += alternatives[j].keb; if(alternatives[j].inf) { for(let info of alternatives[j].inf) { temptag += " ("; temptag += clip(info); temptag += ")"; } } if(j+1 < alternatives.length) temptag += ", "; } if(alternatives.length > 0) temptag += ")"; } } else found_kanji = false; if(!found_kanji) { temptag += "" temptag += text; temptag += "" // FIXME: show inf for located reb if(term.deconj) { for(let form of term.deconj) { if(form.process.length > 0) temptag += "~"; for(let f = form.process.length-1; f >= 0; f--) { let info = form.process[f]; if(info.startsWith("(") && info.endsWith(")") && f != 0) continue; temptag += info; if(f > 0) temptag += "―"; } } } // list alternatives let alternatives = []; for(let j = 0; j < term.r_ele.length; j++) if(term.r_ele[j].reb != text) alternatives.push(term.r_ele[j]); if(alternatives.length > 0) temptag += " (also "; for(let j = 0; j < alternatives.length; j++) { temptag += alternatives[j].reb; if(alternatives[j].inf) { for(let info of alternatives[j].inf) { temptag += " ("; temptag += clip(info); temptag += ")"; } } if(j+1 < alternatives.length) temptag += ", "; } if(alternatives.length > 0) temptag += ")"; } temptag += ""; temp.innerHTML += temptag; temp.innerHTML += "
"; let goodsenses = []; for(let j = 0; j < term.sense.length; j++) { let sense = term.sense[j]; if(sense.stagk && found_kanji) { let found = false; for(let l = 0; l < sense.stagk.length; l++) if(sense.stagk[l] == text) found = true; if(!found) continue; } if(sense.stagr && !found_kanji) { let found = false; for(let l = 0; l < sense.stagr.length; l++) if(sense.stagr[l] == text) found = true; if(!found) continue; } goodsenses.push(sense); } let lastpos = []; for(let j = 0; j < goodsenses.length; j++) { let sense = goodsenses[j]; if(sense.pos == lastpos) sense.pos = undefined; else lastpos = sense.pos; //temp.innerHTML += "" + (j+1) + ". "; if(sense.pos) { let temptag = "("; let parts = []; for(let l = 0; l < sense.pos.length; l++) parts.push(sense.pos[l]); temptag += parts.join(", "); temptag += ")"; if(settings.compact) temptag += " "; else temptag += "
"; temp.innerHTML += temptag; } if(settings.compact) { if(goodsenses.length > 1) temp.innerHTML += " (" + (j+1) + ") "; } else { temp.innerHTML += "" + (j+1) + ". "; } if(sense.inf) temp.innerHTML += "(" + sense.inf + ") "; if(sense.misc) { let temptag = "("; let parts = []; for(let l = 0; l < sense.misc.length; l++) parts.push(sense.misc[l].substring(1, sense.misc[l].length-1)); temptag += parts.join(", "); temptag += ")"; temptag += " "; temp.innerHTML += temptag; } temp.innerHTML += sense.gloss.join("; "); if(settings.compact) { if(sense.gloss.length > 1 || j+1 != goodsenses.length) temp.innerHTML += "; "; } else { temp.innerHTML += "
"; } } if(settings.compact) temp.innerHTML += "
"; } middle.appendChild(temp); return middle; } let settings_reload_rate = 200; let settings_reloader = undefined; async function settings_reload() { try { settings.enabled = (await browser.storage.local.get("enabled")).enabled; if(settings.enabled == undefined) settings.enabled = false; settings.compact = (await browser.storage.local.get("compact")).compact; if(settings.compact == undefined) settings.compact = true; settings.length = (await browser.storage.local.get("length")).length; if(!settings.length) settings.length = 25; settings.fixedwidth = (await browser.storage.local.get("fixedwidth")).fixedwidth; if(!settings.fixedwidth) settings.fixedwidth = false; settings.fixedwidthpositioning = (await browser.storage.local.get("fixedwidthpositioning")).fixedwidthpositioning; if(!settings.fixedwidthpositioning) settings.fixedwidthpositioning = false; if(!settings.enabled && exists_div()) delete_div(); //console.log("set settings"); //console.log(settings.enabled); //console.log(settings.compact); } catch(error) { //console.log("failed to set settings, maybe not stored yet?"); } // not stored yet, probably settings_reloader = setTimeout(settings_reload, settings_reload_rate); } settings_reloader = setTimeout(settings_reload, settings_reload_rate); // look up words on a timer loop that only uses the most recent lookup request and ignores all the others let lookup_timer = undefined; let lookup_queue = []; let lookup_rate = 8; let lookup_loop_cancel = false; let lookup_last_time = Date.now(); async function lookup_loop() { lookup_last_time = Date.now(); let t_start = Date.now(); if(lookup_queue.length > 0) { //console.log("queue not empty"); let lookup = lookup_queue.pop(); lookup_queue = []; let response = await browser.runtime.sendMessage({type:"search", text:lookup[0], time:Date.now(), divexisted:exists_div()}); if(response) { let mydiv = build_div(response.text, response.result); if(mydiv) display_div(mydiv, lookup[3], lookup[4]); } } let t_end = Date.now(); let t_to_wait = lookup_rate - (t_end - t_start); if(t_to_wait < 0) t_to_wait = 0; if(t_to_wait > lookup_rate) t_to_wait = lookup_rate; if(lookup_loop_cancel) { lookup_loop_cancel = false; lookup_timer = undefined; return; } else lookup_timer = setTimeout(lookup_loop, t_to_wait); } lookup_timer = setTimeout(lookup_loop, lookup_rate); function lookup_enqueue(text, x, y, x2, y2) { lookup_queue = [[text, x, y, x2, y2]]; if(!lookup_timer || lookup_last_time+lookup_rate*100 < Date.now()) { if(lookup_timer) clearTimeout(lookup_timer); lookup_timer = setTimeout(lookup_loop, lookup_rate); } } function lookup_cancel() { lookup_queue = []; delete_div(); } let time_of_last = Date.now(); let throttle = 8; let seach_x_offset = -3; window.addEventListener("mousemove", (event)=> { if(!settings.enabled) return; if(Date.now() - time_of_last < throttle) { //console.log("too soon, returning"); //console.log(Date.now() + " vs " + time_of_last); return; } //console.log("---- mousemove entry at " + Date.now()); time_of_last = Date.now(); //console.log("searching for text"); let textNode; let offset; let nodeResetList = []; let nodeResetSeen = new Set(); // find the text under the mouse event let nodeIsBad = true; while(nodeIsBad) { nodeIsBad = false; let hitpage = function(x, y) { let range = undefined; if (document.caretPositionFromPoint) range = document.caretPositionFromPoint(x, y); else if (document.caretRangeFromPoint) range = document.caretRangeFromPoint(x, y); if(range) { textNode = range.offsetNode; offset = range.offset; } }; hitpage(event.clientX+seach_x_offset, event.clientY); // try without the offset if (textNode == undefined || textNode.nodeType != 3) hitpage(event.clientX, event.clientY); if(!(textNode == undefined)) { // we hit an node, see if it's a transparent element and try to move it under everything temporarily if it is try { if(textNode.nodeType == 1 && !nodeResetSeen.has(textNode)) { let style = window.getComputedStyle(textNode); let bg_color = style.getPropertyValue("background-color"); if(bg_color == "rgba(0, 0, 0, 0)") { nodeIsBad = true; nodeResetList.push([textNode, style.getPropertyValue("z-index")]); nodeResetSeen.add(textNode); textNode.style.zIndex = -100000000; continue; } } } catch(err) {} } } for(let toreset of nodeResetList) { let element = toreset[0]; let z_index = toreset[1]; element.style.zIndex = z_index; } // if there was text, use it if (textNode && textNode.nodeType == 3) { //print_object(textNode); //print_object(textNode.parentNode); let rect = textNode.parentNode.getBoundingClientRect(); let fud = 5; // FIXME: Doesn't work to reject in all cases let hit = (event.clientX+fud >= rect.left && event.clientX-fud <= rect.right && event.clientY+fud >= rect.top && event.clientY-fud <= rect.bottom); if(!hit) { // console.log("no hit"); lookup_cancel(); return; } //console.log("found text"); let text = textNode.textContent.substring(offset, textNode.textContent.length); // grab text from later and surrounding DOM nodes let current_node = textNode; while(text.length < settings.length) { if(current_node == undefined) break; try { let display = getComputedStyle(current_node).display; if(display != "inline" && display != "ruby") break; } catch(err) {} let parent = current_node.parentNode; if(parent == undefined) break; let i = Array.prototype.indexOf.call(current_node.parentNode.childNodes, current_node); if(i < 0) break; i++; while(i < current_node.parentNode.childNodes.length) { let next_node = current_node.parentNode.childNodes[i]; let tagname = next_node.tagName ? next_node.tagName.toLowerCase() : ""; if(tagname == "rt" || tagname == "rp") continue; // next_node might not be an Element try { let display = getComputedStyle(next_node).display; if(display == "ruby") continue; if(display == "inline") text += next_node.textContent; } catch(err) { text += next_node.textContent; } i++; } if(text.length < settings.length) current_node = current_node.parentNode; } text = text.trim(); //print_object(text); text = text.substring(0, Math.min(text.length, settings.length)); //if(text != "") //lookup_indirect(text, event.clientX, event.clientY, time_of_last); if(text != "") lookup_enqueue(text, event.clientX, event.clientY, event.pageX, event.pageY); else { // console.log("no text"); lookup_cancel(); } } else { // console.log("no text node"); // console.log("actual type:"); // console.log(textNode.nodeType); // console.log("offset:"); // console.log(offset); lookup_cancel(); } });