// Copyright 2017; Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0 'use strict'; if (typeof globalThis.browser === "undefined" && typeof chrome !== "undefined") { globalThis.browser = { ...chrome, runtime: { ...chrome.runtime, sendMessage: function(message) { return new Promise((resolve, reject) => { chrome.runtime.sendMessage(message, response => { const err = chrome.runtime.lastError; // Chrome sets lastError when a message intentionally has no direct response. // Firefox-style Nazeka code often uses async side-channel replies instead. if (err) { console.warn("Nazeka sendMessage warning:", err.message); resolve(undefined); } else { resolve(response); } }); }); }, getPlatformInfo: function() { return new Promise((resolve, reject) => { chrome.runtime.getPlatformInfo(info => { const err = chrome.runtime.lastError; if (err) reject(err); else resolve(info); }); }); }, getURL: chrome.runtime.getURL.bind(chrome.runtime), onMessage: { addListener: function(listener) { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { try { const result = listener(message, sender); if (result && typeof result.then === "function") { result.then(sendResponse).catch(error => { console.error("Nazeka message listener error:", error); sendResponse(undefined); }); return true; } if (result !== undefined) { sendResponse(result); } return false; } catch (error) { console.error("Nazeka message listener exception:", error); sendResponse(undefined); return false; } }); } } }, storage: { ...chrome.storage, local: { ...chrome.storage.local, get: function(keys) { return new Promise((resolve, reject) => { chrome.storage.local.get(keys, result => { const err = chrome.runtime.lastError; if (err) reject(err); else resolve(result); }); }); }, set: function(items) { return new Promise((resolve, reject) => { chrome.storage.local.set(items, () => { const err = chrome.runtime.lastError; if (err) reject(err); else resolve(); }); }); }, remove: function(keys) { return new Promise((resolve, reject) => { chrome.storage.local.remove(keys, () => { const err = chrome.runtime.lastError; if (err) reject(err); else resolve(); }); }); } }, onChanged: chrome.storage.onChanged }, tabs: chrome.tabs ? { ...chrome.tabs, getCurrent: function() { return new Promise((resolve, reject) => { chrome.tabs.getCurrent(tab => { const err = chrome.runtime.lastError; if (err) reject(err); else resolve(tab); }); }); }, sendMessage: function(tabId, message) { return new Promise((resolve, reject) => { chrome.tabs.sendMessage(tabId, message, response => { const err = chrome.runtime.lastError; if (err) { console.warn("Nazeka tabs.sendMessage warning:", err.message); resolve(undefined); } else { resolve(response); } }); }); }, create: chrome.tabs.create.bind(chrome.tabs) } : undefined, windows: chrome.windows ? { ...chrome.windows, create: chrome.windows.create.bind(chrome.windows) } : undefined, extension: { ...chrome.extension, getURL: chrome.runtime.getURL.bind(chrome.runtime) }, browserAction: chrome.browserAction || undefined, contextMenus: chrome.contextMenus || undefined }; } // updated by a timer looping function, based on local storage set by the options page let settings = { enabled: false, kanji_mode: false, compact: true, usetextfields: true, length: 25, contextlength: 100, // nonconfigurable fixedwidth: false, fixedwidthpositioning: false, superborder: false, disableborder: false, space_saver: false, hide_deconj: false, showoriginal: true, reader_sticky: false, definitions_mode: 0, normal_definitions_in_mining: false, alternatives_mode: 3, // 0: longest only; 1: longest and shortest; 2: longest and second longest; 3: all matches strict_alternatives: true, // if true, alternatives looked up in all kana can not return results with kanji glosses that don't have any usually/exclusively kana info strict_epwing: true, json_newlines_replace: false, json_newlines_character: " ⬥ ", scale: 1, width: 600, lookuprate: 8, bgcolor: "#111111", fgcolor: "#CCCCCC", hlcolor: "#99DDFF", hlcolor2: "#99FF99", titlecolor: "#D8A888", font: "", hlfont: "", definition_fontsize: 13, dict_item_fontsize: 18, reading_fontsize: 15, corner: 0, xoffset: 5, yoffset: 22, strip_spaces: true, ignore_linebreaks: true, ignore_divs: false, sticky: false, popup_follows_mouse: true, popup_requires_key: 0, // 0: none; 1: ctrl; 2: shift x_dodge: 1, y_dodge: 0, sticky_maxheight: 0, kanji_show_stroke_count: true, kanji_show_readings: true, kanji_show_composition: true, kanji_show_quality_warning: true, hotkey_mine: "m", hotkey_nazeka_toggle: "j", hotkey_close: "n", hotkey_sticky: "b", hotkey_audio: "p", hotkey_kanji_mode: "k", hotkey_nudge_left: "ArrowLeft", hotkey_nudge_right: "ArrowRight", volume: 0.2, live_mining: false, use_selection: false, only_selection: false, audio_force_https: false, highlight_dict_headers: false, dropshadow: false, dropshadowradius: 10, dropshadowcolor: "#000000", padding: 2, novalign: 2, inserthr: false, info_styling: false, info_fgcolor: "#CCCCCC", info_bgcolor_pos: "#606060", info_bgcolor_num: "#406080", info_bgcolor_etc: "#608040" }; let platform = "win"; let is_reader = false; async function get_real_platform() { let my_platform = undefined; try { my_platform = await browser.runtime.sendMessage({type:"platform"}); if(my_platform) my_platform = my_platform["response"]; }catch(err){}// don't care while(my_platform == "") { try { my_platform = await browser.runtime.sendMessage({type:"platform"}); if(my_platform) my_platform = my_platform["response"]; }catch(err){}// don't care } platform = my_platform; } get_real_platform(); let last_time_display = Date.now(); // FIXME: this should be the extension ID or a per-page random string, but it's not useful to change this yet, not until firefox lands shadow DOM let div_class = "nazeka_fGKRTDGFGgr9atT"; // the popup is inserted into the page as a screen-relative div, we never delete it because modifying the root DOM of very long pages is expensive, we hide it instead let last_displayed = undefined; function delete_div() { last_displayed = undefined; let other = get_div(); if(other) { other.innerHTML = ""; other.removeAttribute('style'); } } function set_sticky_styles(mydiv) { mydiv.style.position = "fixed"; mydiv.style.left = "unset"; mydiv.style.right= "unset"; mydiv.style.bottom = "unset"; mydiv.style.top = "10px"; if(settings.corner != 1 && settings.corner != 3) mydiv.style.right = "10px"; else mydiv.style.left = "10px"; mydiv.style.marginRight = "unset"; if(settings.sticky_maxheight == 0) mydiv.style.maxHeight = "calc(100vh - 20px)"; else mydiv.style.maxHeight = Math.round(Number(settings.sticky_maxheight))+"px"; mydiv.style.overflowY = "scroll"; mydiv.style.marginLeft = "unset"; mydiv.style.marginTop = "unset"; mydiv.style.marginBottom = "10px"; } function is_not_frameset(root) { try { return root.parent.document.body.tagName != "FRAMESET"; } catch(err) { return true; } } function get_doc() { let find_root = document.defaultView; let mydoc = document; while(find_root.parent && find_root.parent != find_root && is_not_frameset(find_root)) { find_root = find_root.parent; try { mydoc = find_root.document; } catch(err) { console.log("hit error trying to find root; nazeka might behave incoherently"); console.log(err); break; } } return mydoc; } function mining_ui_exists() { return get_doc().body.getElementsByClassName("nazeka_mining_ui").length > 0; } function delete_mining_ui() { while(mining_ui_exists()) get_doc().body.getElementsByClassName("nazeka_mining_ui")[0].remove(); } function get_mining_ui() { return get_doc().body.getElementsByClassName("nazeka_mining_ui")[0]; } function getViewportSize(mydoc) { if (mydoc.compatMode == "CSS1Compat") return { w: mydoc.documentElement.clientWidth, h: mydoc.documentElement.clientHeight }; return { w: mydoc.body.clientWidth, h: mydoc.body.clientHeight }; } function is_sticky() { return (settings.sticky && platform != "android" && !mining_ui_exists()) || (is_reader && settings.reader_sticky); } // here we set all the styling and positioning of the div, passing "middle" as the actual contents of it. // this is rather elaborate because of 1) a lack of shadow DOM, even for just styling 2) options 3) """features""" of how HTML viewport stuff works that are actually terrible let last_display_x = 0; let last_display_y = 0; function display_div(middle, x, y) { console.log("Nazeka display_div called", x, y, middle); last_display_x = x; last_display_y = y; let font = settings.font.trim().replace(";","").replace("}",""); if(font != "") font += ","; middle.style = `background-color: ${settings.bgcolor}; border-radius: 2.5px; border: 1px solid ${settings.bgcolor};`; let border_text = (settings.disableborder)?("border: 0px solid transparent;"):(`border: 1px solid ${settings.fgcolor}`); middle.firstChild.style = `${border_text}; border-radius: 2px; padding: ${settings.padding}px; background-color: ${settings.bgcolor}; color: ${settings.fgcolor}; font-family: ${font} Arial, sans-serif; text-align: left; font-size: ${settings.definition_fontsize}px;`; let find_root = document.defaultView; let newx = x; let newy = y; let mydoc = document; while(find_root.parent && find_root.parent != find_root && is_not_frameset(find_root)) { let rect = find_root.frameElement.getBoundingClientRect(); let rx = rect.x; let ry = rect.y; let sx1 = find_root.parent.scrollX; let sy1 = find_root.parent.scrollY; let sx2 = find_root.scrollX; let sy2 = find_root.scrollY; find_root = find_root.parent; newx += (rx + sx1 - sx2); newy += (ry + sy1 - sy2); try { mydoc = find_root.document; } catch(err) { break; } } let basewidth = Math.round(Number(settings.width)); if(settings.x_dodge != 1 && basewidth > mydoc.documentElement.getBoundingClientRect().width/2 - settings.xoffset - 5) basewidth = Math.max(150, Math.round(mydoc.documentElement.getBoundingClientRect().width/2 - settings.xoffset - 5)); let styletext = ""; if(settings.fixedwidth) { styletext += "max-width: " + basewidth + "px; " styletext += "min-width: " + basewidth + "px; " } else { styletext += "max-width: " + basewidth + "px; " styletext += "min-width: 150px; " } styletext += "position: absolute; top: 0; left: 0; transition: unset; "; if(settings.superborder && !settings.disableborder) styletext += `background-color: ${settings.fgcolor}; border-radius: 3px; border: 1px solid ${settings.fgcolor}; z-index: 1000000000000000000000;`; else styletext += `border-radius: 3px; background-color: ${settings.bgcolor}; z-index: 1000000000000000000000;`; styletext += "writing-mode: horizontal-tb; line-height: initial; white-space: initial;"; if(settings.dropshadow) { let v = parseInt(settings.dropshadowcolor.substring(1), 16); let r = Math.floor(v / 256 / 256); let g = Math.floor(v / 256) % 256; let b = Math.floor(v) % 256; let rgba = "rgba(" + r + "," + g + "," + b + ",0.5)"; styletext += `box-shadow: 0 0 ${settings.dropshadowradius}px ${rgba};` } let other = mydoc.body.getElementsByClassName(div_class); let outer = undefined; if(other.length > 0) { outer = other[0]; if(!outer.firstChild) outer.appendChild(middle); else if(outer.firstChild != middle) outer.replaceChild(middle, outer.firstChild); outer.style.display = "block"; } else { outer = mydoc.createElement("div"); outer.className = div_class; if(!outer.firstChild || outer.firstChild != middle) outer.appendChild(middle); mydoc.body.appendChild(outer); } outer.lang = "ja"; if(settings.scale != 1 && settings.scale > 0 && settings.scale < 64) styletext += " transform-origin: top left; transform: scale(" + Number(settings.scale) + ");"; outer.style = styletext; let mywidth = basewidth*settings.scale; if(!settings.fixedwidthpositioning) mywidth = outer.offsetWidth*settings.scale; let corner = settings.corner; if(is_sticky()) set_sticky_styles(outer); else { if(corner == 1 || corner == 3) { newx -= mywidth; newx -= settings.xoffset; } else newx += settings.xoffset; if(corner == 2 || corner == 3) newy = newy - outer.getBoundingClientRect().height - settings.yoffset; else newy = newy + settings.yoffset; // fixes an absolute positioning """feature""" that doesn't work properly with very wide pages (e.g. pages of left-to-right vertical text) outer.style.top = "0px"; outer.style.left = "0px"; let width = outer.offsetWidth; // outer.style.top = (newy)+"px"; outer.style.bottom = "unset"; outer.style.right = "unset"; outer.style.left = (newx)+"px"; outer.style.marginRight = "unset"; outer.style.marginLeft = "unset"; // fixes an absolute positioning """feature""" that doesn't work properly with very wide pages (e.g. pages of left-to-right vertical text) outer.style.width = (width)+"px"; // } let localrect; let dodged_vertically = false; let viewport = getViewportSize(mydoc); // dodge top/bottom localrect = outer.getBoundingClientRect(); if(localrect.bottom+5 > viewport.h) { if(settings.y_dodge == 1) // slide newy -= localrect.bottom+5 - viewport.h; else // flip newy -= localrect.height + settings.yoffset*2; outer.style.top = (newy)+"px"; dodged_vertically = true; } localrect = outer.getBoundingClientRect(); if(localrect.top-5 < 0 && (!dodged_vertically || corner == 0 || corner == 1)) // (prefer running off the top when using "bottom" alignments) { if(settings.y_dodge == 1) newy -= localrect.top-5; else newy += localrect.height + settings.yoffset*2; outer.style.top = (newy)+"px"; } // dodge left/right localrect = outer.getBoundingClientRect(); if(localrect.right+5 > viewport.w) { if(settings.x_dodge == 1) newx -= localrect.right+5 - viewport.w; else newx -= localrect.width + settings.xoffset*2; outer.style.left = (newx)+"px"; } localrect = outer.getBoundingClientRect(); if(localrect.left-5 < 0) { if(settings.x_dodge == 1) newx -= localrect.left-5; else newx += localrect.width + settings.xoffset*2; outer.style.left = (newx)+"px"; } } function exists_div() { let mydoc = get_doc(); let other = mydoc.body.getElementsByClassName(div_class); return (other.length > 0 && other[0].style.display != "none" && other[0].innerHTML != ""); } function get_div() { let mydoc = get_doc(); let other = mydoc.body.getElementsByClassName(div_class); if(other.length > 0 && other[0].style.display != "none" && other[0].innerHTML != "") return other[0]; else return undefined; } // 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); } function elementize_jmdict_defs(goodsenses) { let lastpos = []; let jmdict_defs = document.createElement("div"); jmdict_defs.className = "jmdict_definitions"; for(let j = 0; j < goodsenses.length; j++) { let sense = goodsenses[j]; if(sense.pos + "" == lastpos) // removing this + "" breaks repetitive sense hiding in chrome sense.pos = undefined; else lastpos = sense.pos; if(sense.pos && sense.pos.length > 0) { let part = document.createElement("span"); part.className = "nazeka_pos"; let temptext = "("; let parts = []; for(let l = 0; l < sense.pos.length; l++) parts.push(sense.pos[l]); temptext += parts.join(", "); temptext += ")"; part.textContent = temptext; jmdict_defs.appendChild(part); if(settings.compact) jmdict_defs.appendChild(document.createTextNode(" ")); else jmdict_defs.appendChild(document.createElement("br")); } if(settings.compact) { if(goodsenses.length > 1) { let number = document.createElement("span"); number.className = "nazeka_num"; number.textContent = "("+(j+1)+")"; jmdict_defs.appendChild(number); jmdict_defs.appendChild(document.createTextNode(" ")); } } else { let temptag = document.createElement("span"); temptag.className = "nazeka_num"; temptag.textContent = ""+(j+1)+"."; jmdict_defs.appendChild(temptag); jmdict_defs.appendChild(document.createTextNode(" ")); } if(sense.inf) { let info = document.createElement("i"); info.textContent = "("+sense.inf+") "; jmdict_defs.appendChild(info); } if(sense.misc) { let temptag = document.createElement("span"); temptag.className = "nazeka_misc"; let temptext = "("; let parts = []; for(let l = 0; l < sense.misc.length; l++) parts.push(sense.misc[l].substring(1, sense.misc[l].length-1)); temptext += parts.join(", "); temptext += ")"; temptag.textContent = temptext; jmdict_defs.appendChild(temptag); jmdict_defs.appendChild(document.createTextNode(" ")); } jmdict_defs.appendChild(document.createTextNode(sense.gloss.join("; "))); if(settings.compact) { if(sense.gloss.length > 1 || j+1 != goodsenses.length) jmdict_defs.appendChild(document.createTextNode("; ")); } else { jmdict_defs.appendChild(document.createElement("br")); } } return jmdict_defs; } function elementize_json_defs(json) { if(json.length > 0) { let json_defs = document.createElement("div"); json_defs.className = "json_definitions"; for(let entry of json) { let json_head = document.createElement("div"); let json_title = document.createElement("span"); json_title.appendChild(document.createTextNode("―"+entry["z"]+"―")); json_title.className = "json_title"; json_head.appendChild(json_title); json_head.appendChild(document.createElement("br")); let json_head_text = entry["r"]; if(entry["s"] && entry["s"][0] != "") { let isfirst = true; json_head_text += "【"; for(let spelling of entry["s"]) { if(!isfirst) json_head_text += "・"; json_head_text += spelling; isfirst = false; } json_head_text += "】"; } json_head.appendChild(document.createTextNode(json_head_text)); let json_definition = document.createElement("div"); let isfirst = true; for(let line of entry["l"]) { if(!isfirst) { json_definition.appendChild(document.createElement("br")); let filler = document.createElement("span"); filler.className = "json_newline_filler"; json_definition.appendChild(filler); } let def_line = document.createElement("span"); def_line.textContent = line; json_definition.appendChild(def_line); isfirst = false; } json_head.className = "json_head"; json_definition.className = "json_definition"; json_defs.appendChild(json_head); json_defs.appendChild(json_definition); } return json_defs; } else { return undefined; } } function filter_for_css(text) { return text.replace(";","").replace("}","").replace("'","").replace('"',""); } function filter_for_css_trimmed(text) { return filter_for_css(text).trim(); } function get_style() { // styling for highlighted stuff and the lookup text let style = document.createElement("style"); style.type = "text/css"; let font = filter_for_css_trimmed(settings.hlfont); let morefont = filter_for_css_trimmed(settings.font); let newline_char = filter_for_css(settings.json_newlines_character); if(font != "") font += ","; if(morefont != "") font += morefont + ","; let valign = ".nazeka_word * {vertical-align: middle}"; if(settings.novalign) valign = ""; let infostyle = ""; if(settings.info_styling) { infostyle = `.nazeka_pos, .nazeka_num, .nazeka_misc {font-size:80%; border-radius:3px; padding: 1px 3px 2px; color:${settings.info_fgcolor}; display:inline-block; position: relative; bottom: 1px; margin-bottom: -1px;} .nazeka_pos {background-color:${settings.info_bgcolor_pos};} .nazeka_num {background-color:${settings.info_bgcolor_num};} .nazeka_misc {background-color:${settings.info_bgcolor_etc};} `; } style.textContent = `.nazeka_main_keb{font-size:${settings.dict_item_fontsize}px;white-space:nowrap;font-family: ${font}IPAGothic,TakaoGothic,Noto Sans CJK JP Regular,Meiryo,sans-serif;color:${settings.hlcolor}} .nazeka_main_reb{font-size:${settings.dict_item_fontsize}px;white-space:nowrap;font-family: ${font}IPAGothic,TakaoGothic,Noto Sans CJK JP Regular,Meiryo,sans-serif;color:${settings.hlcolor}} ${valign} ${infostyle} .${div_class} hr{margin: 0.5em; padding: 0; border: none; border-top: 1px solid ${settings.fgcolor}; opacity: 0.25} .nazeka_sub_keb{font-size:${settings.reading_fontsize}px;white-space:nowrap;font-family: ${font}IPAGothic,TakaoGothic,Noto Sans CJK JP Regular,Meiryo,sans-serif} .nazeka_sub_reb{font-size:${settings.reading_fontsize}px;white-space:nowrap;color:${settings.hlcolor2}} .nazeka_original{float: right; margin-right: 2px; margin-left:4px; opacity:0.7;} .json_head{margin: 0 4px 6px;} .jmdict_definitions{margin: 0 4px 6px;} .json_definition{margin: 0 4px 6px;} .kanji_info{margin: 2px 4px 2px;} `; if(settings.highlight_dict_headers) style.textContent += `.jmdict_title{color:${settings.titlecolor}} .json_title{color:${settings.titlecolor}} `; if(settings.json_newlines_replace) style.textContent += `.json_definition br{display: none;} .json_definition .json_newline_filler::before{content: '${newline_char}';} `; return style; } // Here we actually build the content of the lookup popup, based on the text we looked up and the list of lookups from the background script // note that we can get multiple lookups and this function only handles a single lookup function build_div_inner(text, result, moreText, index, first_of_many = false) { let temp = document.createElement("div"); temp.style.position = "relative"; // the "Looked up XXXX" text if(settings.showoriginal) { // nesting it like this lets us access nazeka_lookup when we want to get the text we looked up, if we have text in front of it, which we don't anymore let original = document.createElement("div"); original.className = "nazeka_original"; //original.textContent = "Looked up "; let moreText_start = moreText.substring(0, index) let moreText_end = moreText.substring(index + text.length); if(moreText_start.length > 5) { moreText_start = "…"+moreText_start.substring(moreText_start.length-3); } if(moreText_end.length > 5) { moreText_end = moreText_end.substring(0, 3)+"…"; } let original_inner = document.createElement("span"); original_inner.className = "nazeka_lookup"; original_inner.textContent = text; original_inner.style.fontWeight = "bold"; original_inner.style.color = `${settings.hlcolor}`; original.appendChild(document.createTextNode(moreText_start)); original.appendChild(original_inner); original.appendChild(document.createTextNode(moreText_end)); if(first_of_many && (platform == "android" || is_sticky())) { let buttons = document.createElement("div"); let left_arrow = document.createElement("img"); let right_arrow = document.createElement("img"); left_arrow.src = browser.extension.getURL("img/leftarrow24.png"); right_arrow.src = browser.extension.getURL("img/rightarrow24.png"); left_arrow.onclick = lookup_left; right_arrow.onclick = lookup_right; left_arrow.style.marginTop = "-3px"; right_arrow.style.marginTop = "-3px"; left_arrow.style.display = "inline"; right_arrow.style.display = "inline"; left_arrow.style.padding = "initial"; right_arrow.style.padding = "initial"; buttons.appendChild(left_arrow); buttons.appendChild(right_arrow); if(platform == "android") { let close_button = document.createElement("img"); close_button.src = browser.extension.getURL("img/closebutton24.png"); close_button.onclick = manual_close; close_button.style.marginTop = "-3px"; close_button.style.display = "inline"; close_button.style.padding = "initial"; buttons.appendChild(close_button); } else { let close_button = document.createElement("img"); close_button.src = browser.extension.getURL("img/closebutton24.png"); close_button.onclick = manual_disable_sticky; close_button.style.marginTop = "-3px"; close_button.style.display = "inline"; close_button.style.padding = "initial"; buttons.appendChild(close_button); } buttons.style.float = "right"; original.appendChild(buttons); } temp.appendChild(original); let original_sentence = document.createElement("span"); original_sentence.className = "nazeka_lookup_sentence"; original_sentence.textContent = moreText; original_sentence.style.display = "none"; temp.appendChild(original_sentence); let original_index = document.createElement("span"); original_index.className = "nazeka_lookup_index"; original_index.textContent = `${index}`; original_index.style.display = "none"; temp.appendChild(original_index); } else { let original_inner = document.createElement("span"); original_inner.className = "nazeka_lookup"; original_inner.textContent = text; original_inner.style.display = "none"; temp.appendChild(original_inner); let original_sentence = document.createElement("span"); original_sentence.className = "nazeka_lookup_sentence"; original_sentence.textContent = moreText; original_sentence.style.display = "none"; temp.appendChild(original_sentence); let original_index = document.createElement("span"); original_index.className = "nazeka_lookup_index"; original_index.textContent = `${index}`; original_index.style.display = "none"; temp.appendChild(original_index); } let style = get_style(); temp.appendChild(style); function makespan(text) { let s = document.createElement("span"); s.textContent = text; return s; } // lookups can have multiple results (e.g. する -> 為る, 刷る, 掏る, 剃る, 擦る) for(let i = 0; i < result.length; i++) { let term = result[i]; let container = document.createElement("div"); container.className = "nazeka_word_container"; container.style.marginBottom = "3px"; container.setAttribute("nazeka_seq", term.seq); let text = ""; if(term.found.keb) text = term.found.keb; else if(term.found.reb) text = term.found.reb; else continue; // shouldn't happen, but again, just in case of broken data if(term.has_audio.length > 0) { for(let audio of term.has_audio) { let audio_data_holder = document.createElement("div"); audio_data_holder.className = "nazeka_audioref"; audio_data_holder.style.display = "none"; audio_data_holder.innerText += audio+"\n"; container.appendChild(audio_data_holder); } } // for mining let reading_data_holder = document.createElement("div"); reading_data_holder.className = "nazeka_mining_readings"; reading_data_holder.style.display = "none"; let reading_data_list = new Array(); for(let r of term.r_ele) reading_data_list.push(r.reb); reading_data_holder.innerText = reading_data_list.join("、"); container.appendChild(reading_data_holder); let spelling_data_holder = document.createElement("div"); spelling_data_holder.className = "nazeka_mining_spellings"; spelling_data_holder.style.display = "none"; let spelling_data_list = new Array(); if(term.k_ele) for(let k of term.k_ele) spelling_data_list.push(k.keb); spelling_data_holder.innerText = spelling_data_list.join("、"); container.appendChild(spelling_data_holder); // end of "for mining" let temptag = document.createElement("span"); temptag.className = "nazeka_word"; let original_kana = ""; if(term.found.reb) original_kana = term.found.reb; if(term.found.keb) { let k_ele = term.found; let kanji_text = k_ele.keb; let keb = document.createElement("span"); keb.className = "nazeka_main_keb"; keb.textContent = kanji_text; temptag.appendChild(keb); if(k_ele.inf) { let maininfos = document.createElement("span"); maininfos.className = "nazeka_main_infos"; for(let info of k_ele.inf) { let keb_inf = document.createElement("span"); keb_inf.className = "nazeka_main_inf"; keb_inf.textContent += "("; keb_inf.textContent += clip(info); keb_inf.textContent += ")"; maininfos.appendChild(keb_inf); } temptag.appendChild(maininfos); } // list readings let readings = term.r_ele; temptag.appendChild(document.createElement("wbr")); let e_readings = document.createElement("span"); if(!settings.space_saver) e_readings.appendChild(makespan("《")); else e_readings.appendChild(makespan(" ")); e_readings.className = "nazeka_readings"; for(let j = 0; j < readings.length; j++) { let subreb = document.createElement("span"); subreb.className = "nazeka_sub_reb"; subreb.textContent = readings[j].reb; e_readings.appendChild(subreb); if(readings[j].inf) { for(let info of readings[j].inf) { let reb_inf = document.createElement("span"); reb_inf.className = "nazeka_reb_inf"; reb_inf.textContent += "("; reb_inf.textContent += clip(info); reb_inf.textContent += ")"; e_readings.appendChild(reb_inf); } } if(j+1 != readings.length) { e_readings.appendChild(document.createElement("wbr")); e_readings.appendChild(makespan("、")); } } if(!settings.space_saver) e_readings.appendChild(makespan("》")); else e_readings.appendChild(makespan(" ")); temptag.appendChild(e_readings); // list alternatives let alternatives = []; for(let j = 0; j < term.k_ele.length; j++) if(term.k_ele[j].keb != kanji_text) alternatives.push(term.k_ele[j]); if(alternatives.length > 0) temptag.appendChild(makespan((settings.space_saver)?(" ("):(" (also "))); for(let j = 0; j < alternatives.length; j++) { let subkeb = document.createElement("span"); subkeb.className = "nazeka_sub_keb"; subkeb.textContent = alternatives[j].keb; temptag.appendChild(subkeb); if(alternatives[j].inf) { for(let info of alternatives[j].inf) { temptag.appendChild(makespan(" ")); let keb_inf = document.createElement("span"); keb_inf.className = "nazeka_keb_inf"; keb_inf.textContent += "("; keb_inf.textContent += clip(info); keb_inf.textContent += ")"; temptag.appendChild(keb_inf); } } if(j+1 < alternatives.length) temptag.appendChild(makespan(", ")); } if(alternatives.length > 0) temptag.appendChild(makespan(")")); // deconjugations if(term.deconj && !settings.hide_deconj) { let deconj = ""; let first = true; for(let form of term.deconj) { let formtext = ""; let added = 0; for(let f = form.process.length-1; f >= 0; f--) { let info = form.process[f]; if(info == "") continue; if(info.startsWith("(") && info.endsWith(")") && f != 0) continue; if(added > 0) formtext += "→"; added++; formtext += info; } if(formtext != "") { if(first) { temptag.appendChild(document.createElement("wbr")); deconj = "~"; } else { temptag.appendChild(document.createElement("wbr")); deconj = ";"; } temptag.appendChild(makespan(deconj)); temptag.appendChild(document.createElement("wbr")); deconj = formtext; temptag.appendChild(makespan(deconj)); } first = false; } } } else { let main_reb = document.createElement("span"); main_reb.className = "nazeka_main_reb"; main_reb.textContent = term.found.reb; temptag.appendChild(main_reb); if(term.deconj && !settings.hide_deconj) { let deconj = ""; let first = true; for(let form of term.deconj) { let formtext = ""; let added = 0; for(let f = form.process.length-1; f >= 0; f--) { let info = form.process[f]; if(info == "") continue; if(info.startsWith("(") && info.endsWith(")") && f != 0) continue; if(added > 0) formtext += "→"; added++; formtext += info; } if(formtext != "") { if(first) deconj += "~"; else deconj += "・"; deconj += formtext; } first = false; } temptag.appendChild(makespan(deconj)); } // FIXME wrapper span if(term.found.inf) { let maininfos = document.createElement("span"); maininfos.className = "nazeka_main_infos"; for(let info of term.found.inf) { let reb_inf = document.createElement("span"); reb_inf.className = "nazeka_reb_inf"; reb_inf.textContent += "("; reb_inf.textContent += clip(info); reb_inf.textContent += ")"; maininfos.appendChild(reb_inf); } temptag.appendChild(maininfos); } // 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.appendChild(makespan((settings.space_saver)?(" ("):(" (also "))); for(let j = 0; j < alternatives.length; j++) { temptag.appendChild(makespan(alternatives[j].reb)); if(alternatives[j].inf) { for(let info of alternatives[j].inf) { temptag.appendChild(makespan(" ")); let reb_inf = document.createElement("span"); reb_inf.className = "nazeka_reb_inf"; reb_inf.textContent += "("; reb_inf.textContent += clip(info); reb_inf.textContent += ")"; temptag.appendChild(reb_inf); } } if(j+1 < alternatives.length) temptag.appendChild(makespan(", ")); } if(alternatives.length > 0) temptag.appendChild(makespan(")")); } container.appendChild(temptag); if(term.freq && term.freq.length > 0) { let vn_freq_data = document.createElement("span"); vn_freq_data.style = "font-size: 80%; margin-top: 4px; opacity: 0.7; margin-left: 5px;" if(term.freq[0] != term.freq[1]) vn_freq_data.innerText = "#" + term.freq[2] + " (" + term.freq[0] + ":" + term.freq[1] + ")"; else vn_freq_data.innerText = "#" + term.freq[2] + " (" + term.freq[0] + ")"; if(term.freq[3] > 0.005) vn_freq_data.innerText += " (" + Math.round(term.freq[3]*100)/100 + "ppm)"; else if(term.freq[3] > 0.0005) vn_freq_data.innerText += " (" + Math.round(term.freq[3]*1000)/1000 + "ppm)"; else vn_freq_data.innerText += " (" + Math.round(term.freq[3]*10000)/10000 + "ppm)"; container.appendChild(vn_freq_data); } container.appendChild(document.createElement("br")); let definition = document.createElement("div"); definition.className = "nazeka_definitions"; let jmdict_div = undefined; if(typeof term.seq == "string" && term.seq.startsWith("NONE_")) jmdict_div = document.createElement("div"); else jmdict_div = elementize_jmdict_defs(term.sense); let json_div = elementize_json_defs(term.json); if (settings.definitions_mode == 0 || (settings.normal_definitions_in_mining && mining_ui_exists())) // normal { if(jmdict_div) definition.appendChild(jmdict_div); if(json_div) definition.appendChild(json_div); } else if (settings.definitions_mode == 1) // json first { if(json_div) definition.appendChild(json_div); if(jmdict_div) { let jmdict_head_div = document.createElement("div"); let jmdict_head_span = document.createElement("span"); jmdict_head_span.appendChild(document.createTextNode("―jmdict―")); jmdict_head_span.className = "jmdict_title"; jmdict_head_div.className = "jmdict_head"; jmdict_head_div.appendChild(jmdict_head_span); jmdict_div.insertBefore(jmdict_head_div, jmdict_div.firstChild); definition.appendChild(jmdict_div); } } else if (settings.definitions_mode == 2) // json or else jmdict { if(json_div) definition.appendChild(json_div); else definition.appendChild(jmdict_div); } else if (settings.definitions_mode == 3) // json only { if(json_div) definition.appendChild(json_div); } else if (settings.definitions_mode == 4) // none { } else // normal (fallback) { if(jmdict_div) definition.appendChild(jmdict_div); if(json_div) definition.appendChild(json_div); } container.appendChild(definition); if(settings.corner == 2 || settings.corner == 3) { if(settings.showoriginal) temp.insertBefore(container, temp.children[0].nextSibling); else temp.insertBefore(container, temp.children[0]); } else temp.appendChild(container); } return temp; } // this are the divs we use to contain the list of lookups. they're all contained in the same inner div from here. function build_div_intermediary() { let middle = document.createElement("div"); let inner = document.createElement("div"); inner.className = "nazeka_listing"; middle.appendChild(inner); return middle; } // for popups with single lookup results let lastMoreText = ""; function build_div (text, result, moreText, index) { lastMoreText = moreText; last_displayed = [{text:text, result:result}]; let middle = build_div_intermediary(); middle.firstChild.appendChild(build_div_inner(text, result, moreText, index)); return middle; } // for popups with multiple lookup results function build_div_compound (results, moreText, index) { lastMoreText = moreText; last_displayed = results; let middle = build_div_intermediary(); let first = true; for(let lookup of results) { if(settings.corner == 2 || settings.corner == 3) { if(!first && settings.inserthr) middle.firstChild.insertBefore(document.createElement("hr"), middle.firstChild.children[0]); middle.firstChild.insertBefore(build_div_inner(lookup.text, lookup.result, moreText, index, first), middle.firstChild.children[0]); } else { if(!first && settings.inserthr) middle.firstChild.appendChild(document.createElement("hr")); middle.firstChild.appendChild(build_div_inner(lookup.text, lookup.result, moreText, index, first)); } first = false; } return middle; } /* `.nazeka_main_keb{font-size:${settings.dict_item_fontsize}px;white-space:nowrap;font-family: ${font}IPAGothic,TakaoGothic,Noto Sans CJK JP Regular,Meiryo,sans-serif;color:${settings.hlcolor}} .nazeka_main_reb{font-size:${settings.dict_item_fontsize}px;white-space:nowrap;font-family: ${font}IPAGothic,TakaoGothic,Noto Sans CJK JP Regular,Meiryo,sans-serif;color:${settings.hlcolor}} .nazeka_word * {vertical-align: middle} .nazeka_sub_keb{font-size:${settings.reading_fontsize}px;white-space:nowrap;font-family: ${font}IPAGothic,TakaoGothic,Noto Sans CJK JP Regular,Meiryo,sans-serif} .nazeka_sub_reb{font-size:${settings.reading_fontsize}px;white-space:nowrap;color:${settings.hlcolor2}} .nazeka_original{float: right; margin-right: 2px; margin-left:4px; opacity:0.7;} */ function build_div_kanji(text, kanjidata, moreText, index) { if(!kanjidata) return undefined; let middle = build_div_intermediary(); let target = middle.firstChild; let style = get_style(); target.appendChild(style); let head = document.createElement("div"); let info = document.createElement("div"); let tail = document.createElement("div"); if(platform != "android") head.innerText = "Currently in individual kanji mode. Press [" + settings.hotkey_kanji_mode + "] to cancel."; else head.innerText = "Currently in individual kanji mode. Disable in options to go back to dictionary."; let char = document.createElement("div"); char.className = "nazeka_main_keb"; char.textContent = text; head.appendChild(char); info.className = "kanji_info"; let grade = document.createElement("div"); grade.textContent = "Grade: "; if(kanjidata["g"] == "X") grade.textContent += "Hyougai"; else if(kanjidata["g"] == "9" || kanjidata["g"] == "10") grade.textContent += "Jinmeiyou"; else if(kanjidata["g"] == "8") grade.textContent += "Jouyou"; else if("123456".includes(kanjidata["g"])) grade.textContent += "Kyouiku"; else grade.textContent += "Unknown (Hyougai)"; let strokes = document.createElement("div"); strokes.textContent = "Strokes: " + kanjidata["s"]; let readings = document.createElement("div"); let readings_header = document.createElement("div"); readings_header.textContent = "Jouyou readings:"; readings.appendChild(readings_header); let added_readings = 0; function add_readings(target, readings) { let first = true; for(let reading of readings) { added_readings += 1; if(!first) target.appendChild(document.createTextNode("、")); let reading_div = document.createElement("span"); reading_div.className = "nazeka_sub_reb"; reading_div.innerText = reading; target.appendChild(reading_div); first = false; } } if("o" in kanjidata) { let onyomi = document.createElement("div"); onyomi.innerText = "On'yomi: "; add_readings(onyomi, kanjidata["o"]); readings.appendChild(onyomi); } if("k" in kanjidata) { let onyomi = document.createElement("div"); onyomi.innerText = "Kun'yomi: "; add_readings(onyomi, kanjidata["k"]); readings.appendChild(onyomi); } if("os" in kanjidata) { let onyomi = document.createElement("div"); onyomi.innerText = "On'yomi (special): "; add_readings(onyomi, kanjidata["os"]); readings.appendChild(onyomi); } if("ks" in kanjidata) { let onyomi = document.createElement("div"); onyomi.innerText = "Kun'yomi (special): "; add_readings(onyomi, kanjidata["ks"]); readings.appendChild(onyomi); } let composition = document.createElement("div"); composition.textContent = "Composition: " + kanjidata["z"]; info.appendChild(grade); if(settings.kanji_show_stroke_count) info.appendChild(strokes); if(settings.kanji_show_readings && added_readings > 0) info.appendChild(readings); if(settings.kanji_show_composition) info.appendChild(composition); if(settings.kanji_show_quality_warning) { tail.appendChild(document.createTextNode("Data for non-jouyou kanji may contain errors.")); tail.appendChild(document.createElement("br")); tail.appendChild(document.createTextNode("Composition might not render correctly if it contains obscure characters.")); } tail.style.marginBottom = "4px"; target.appendChild(head); target.appendChild(info); target.appendChild(tail); return middle; } async function settings_init() { try { async function getvar(name) { let temp = (await browser.storage.local.get(name))[name]; if(temp != undefined) settings[name] = temp; } getvar("enabled"); getvar("compact"); getvar("length"); getvar("scale"); getvar("width"); getvar("lookuprate"); getvar("fixedwidth"); getvar("fixedwidthpositioning"); getvar("superborder"); getvar("disableborder"); getvar("space_saver"); getvar("hide_deconj"); getvar("showoriginal"); getvar("reader_sticky"); getvar("bgcolor"); getvar("fgcolor"); getvar("hlcolor"); getvar("hlcolor2"); getvar("titlecolor"); getvar("font"); getvar("hlfont"); getvar("definition_fontsize"); getvar("dict_item_fontsize"); getvar("reading_fontsize"); getvar("alternatives_mode"); getvar("strict_alternatives"); getvar("definitions_mode"); getvar("normal_definitions_in_mining"); getvar("strict_epwing"); getvar("json_newlines_replace"); getvar("json_newlines_character"); getvar("corner"); getvar("xoffset"); getvar("yoffset"); getvar("strip_spaces"); getvar("ignore_linebreaks"); getvar("ignore_divs"); getvar("sticky"); getvar("popup_follows_mouse"); getvar("popup_requires_key"); getvar("x_dodge"); getvar("y_dodge"); getvar("sticky_maxheight"); getvar("hotkey_mine"); getvar("hotkey_nazeka_toggle"); getvar("hotkey_close"); getvar("hotkey_sticky"); getvar("hotkey_audio"); getvar("hotkey_nudge_left"); getvar("hotkey_nudge_right"); getvar("volume"); getvar("live_mining"); getvar("use_selection"); getvar("only_selection"); getvar("kanji_show_stroke_count"); getvar("kanji_show_readings"); getvar("kanji_show_composition"); getvar("kanji_show_quality_warning"); getvar("audio_force_https"); getvar("highlight_dict_headers"); getvar("dropshadow"); getvar("dropshadowradius"); getvar("dropshadowcolor"); getvar("padding"); getvar("novalign"); getvar("inserthr"); getvar("info_styling"); getvar("info_fgcolor"); getvar("info_bgcolor_pos"); getvar("info_bgcolor_num"); getvar("info_bgcolor_etc"); if(!settings.enabled && exists_div()) delete_div(); if(!settings.enabled) delete_mining_ui(); } catch(err) {} // options not stored yet } settings_init(); browser.storage.onChanged.addListener((updates, storageArea) => { if(storageArea != "local") return; for(let setting of Object.entries(updates)) { let option = setting[0]; let value = setting[1]; if(Object.keys(settings).includes(option)) settings[option] = value.newValue; } if(!settings.enabled && exists_div()) delete_div(); if(!settings.enabled) delete_mining_ui(); }); let last_lookup = undefined; let last_manual_interaction = Date.now(); function may_contain_japanese(text) { for(let c of text.split("")) if(c.codePointAt(0) > 0x3000) return true; return false; } function is_number(c) { return "1234567890".includes(c); } let last_send_time = performance.now(); let currently_looking_up = false; async function send_lookup(lookup, time) { if(currently_looking_up) return; let my_time = time + 1; my_time -= 1; if(is_number(lookup[0].split("")[0]) && !may_contain_japanese(lookup[5])) return; if(!settings.kanji_mode) { currently_looking_up = true; let response = undefined; try { response = await browser.runtime.sendMessage( { type:"search", text:lookup[0], time:my_time, divexisted:exists_div(), settings:{ alternatives_mode:settings.alternatives_mode, strict_alternatives:settings.strict_alternatives, strict_epwing:settings.strict_epwing } }); } catch(e) { currently_looking_up = false; return; } currently_looking_up = false; if(response) response = response["response"]; if(my_time < last_send_time) return; last_send_time = my_time; if(response && response != "itsthesame") { if(!response.length) { let mydiv = build_div(response.text, response.result, lookup[5], lookup[6]); last_lookup = lookup; if(mydiv) display_div(mydiv, lookup[3], lookup[4]); } else { last_lookup = lookup; let mydiv = build_div_compound(response, lookup[5], lookup[6]); if(mydiv) display_div(mydiv, lookup[3], lookup[4]); } } else if(response != "itsthesame" && exists_div() && !lastMoreText.includes(lookup[0].substring(0, Math.max(1, lookup[0].length-2)))) lookup_cancel(); } else { let response = await browser.runtime.sendMessage( { type:"search_kanji", divexisted:exists_div(), text:[...lookup[0]][0] }); if(response) response = response["response"]; if(response && response != "itsthesame") { last_lookup = lookup; let mydiv = build_div_kanji([...lookup[0]][0], response, lookup[5], lookup[6]); if(mydiv) display_div(mydiv, lookup[3], lookup[4]); } } } function lookup_enqueue(text, x, y, x2, y2, moreText, index, time) { send_lookup([text, x, y, x2, y2, moreText, index], time); } function lookup_cancel() { if(!is_sticky()) delete_div(); } function manual_close() { delete_div(); last_manual_interaction = Date.now(); } function manual_disable_sticky() { settings.sticky = false; browser.storage.local.set({"sticky":false}); delete_div(); } function manual_toggle_enabled() { settings.enabled = !settings.enabled; browser.storage.local.set({"enabled":settings.enabled}); if(!settings.enabled) delete_div(); } function lookup_cancel_force() { delete_div(); } let japanesePunctuation = "、。「」{}()【】『』〈〉《》:・/…︙‥︰+=-÷?!.~―"; let japaneseSeparators = "。?!"; function lookup_left() { if(!exists_div()) return; let text = last_lookup[5]; let index = last_lookup[6]; if (index <= 0) return; index--; while (index > 0 && (text.codePointAt(index) < 0x100 || japanesePunctuation.includes(text.charAt(index)))) index--; text = text.substring(index); last_manual_interaction = Date.now(); send_lookup([text, last_lookup[1], last_lookup[2], last_lookup[3], last_lookup[4], last_lookup[5], index], performance.now()); } function lookup_right() { if(!exists_div()) return; let text = last_lookup[5]; let index = last_lookup[6]; if (index >= text.length-1) return; index++; while (index < text.length-1 && (text.codePointAt(index) < 0x100 || japanesePunctuation.includes(text.charAt(index)))) index++; text = text.substring(index); last_manual_interaction = Date.now(); send_lookup([text, last_lookup[1], last_lookup[2], last_lookup[3], last_lookup[4], last_lookup[5], index], performance.now()); } let time_of_last = Date.now(); let search_x_offset = -3; function selection_filter_enabled(selection_range) { if(!settings.use_selection) return false; if(!selection_range) return false; if(selection_range.startOffset == selection_range.endOffset) return false; return true; } function selection_rejects_node(selection_range, textNode, offset) { if(!selection_range) return false; if(selection_range.startOffset == selection_range.endOffset) return false; try { if(!selection_range.isPointInRange(textNode, offset) || !selection_range.isPointInRange(textNode, offset+1)) return true; } catch(e){} // selection range was strange return false; } function selection_clip_node_with_offsets(node, selection_range) { if (!selection_filter_enabled(selection_range)) { return [node.textContent, 0]; } if(!selection_range.intersectsNode(node)) return ["", 0]; let start = 0; let end = node.length; while(start < end) { if(!selection_range.isPointInRange(node, start)) start += 1; else break; } while(end > start) { if(!selection_range.isPointInRange(node, end)) end -= 1; else break; } return [node.data.slice(start, end), start]; } function get_element_text_with_offsets(element, selection_range) { if(!(element instanceof Element)) { return selection_clip_node_with_offsets(element, selection_range); } try { let display = getComputedStyle(element).display; if(display == "ruby-text") return ["", 0]; let tagname = element.tagName.toLowerCase(); if(tagname == "rt" || tagname == "rp") return ["", 0]; let ret = ""; let ret_offset = -1; for(let child of element.childNodes) { let asdf = get_element_text_with_offsets(child, selection_range); ret += asdf[0]; if(ret_offset == -1) ret_offset = asdf[1]; } return [ret, ret_offset]; } catch(err) { console.log("encountered error while trying to grab element text"); console.log(err); } return ["", 0]; } function get_element_text(element, selection_range) { let ret = get_element_text_with_offsets(element, selection_range)[0]; return ret; } let hard_length_limit = 3000; function grab_more_text(textNode, selection_range, direction = 1) { let ignore_normal_boundaries = selection_filter_enabled(selection_range); if(direction > 0) direction = 1; else direction = -1; let text = ""; let current_node = textNode; let iters = 0; while(ignore_normal_boundaries || (text.length < settings.contextlength && (!text.includes("。") && !text.includes("!") && !text.includes("‼") && !text.includes("?") && !text.includes("??") && !text.includes("…") && (!text.includes("\n") || settings.ignore_linebreaks)) ) ) { if(text.length > hard_length_limit) break; iters += 1; if(current_node == undefined) break; // search up parent element only if current element is inline-like if (!ignore_normal_boundaries && !settings.ignore_divs) { try { let display = getComputedStyle(current_node).display; let tagname = current_node.tagName.toLowerCase(); // FIXME get real inline vs block detection let inline_like = display.includes("inline") || display == "ruby" || display == "ruby-base"; let ruby_interior = tagname == "rt" || tagname == "rp"; if(ruby_interior || !inline_like) break; } catch(err){} // doesn't work with getComputedStyle or maybe tagName } if(iters > 100) { console.log("too many iterations"); console.log(current_node); break; } let parent = current_node.parentNode; if(parent == undefined || parent == current_node) break; let i = Array.prototype.indexOf.call(current_node.parentNode.childNodes, current_node); if(i < 0) break; i += direction; while(i < current_node.parentNode.childNodes.length && i >= 0 && (ignore_normal_boundaries || (text.length < settings.contextlength && (!text.includes("\n") || settings.ignore_linebreaks)))) { if(text.length > hard_length_limit) break; let next_node = current_node.parentNode.childNodes[i]; i += direction; if(ignore_normal_boundaries && !selection_range.intersectsNode(next_node)) break; let tagname = next_node.tagName ? next_node.tagName.toLowerCase() : ""; if(tagname == "br" && !settings.ignore_linebreaks && !ignore_normal_boundaries) break; if(tagname == "rt" || tagname == "rp") continue; // next_node might not be an Element try { let ttext = ""; let display = getComputedStyle(next_node).display; // FIXME get real inline vs block detection if(ignore_normal_boundaries || (display != "block" && display != "grid" && display != "table" && display != "none")) { let current = get_element_text(next_node, selection_range); if(direction > 0) ttext += current; else ttext = current + ttext; } if(direction > 0) text += ttext; else text = ttext + text; } catch(err) { if(direction > 0) text += get_element_text(next_node, selection_range); else text = get_element_text(next_node, selection_range) + text; } } if(text.length < settings.contextlength) current_node = current_node.parentNode; else break; } return text; } function grab_text(textNode, offset, elemental) { let selection_range = window.getSelection(); if(selection_range && selection_range.rangeCount > 0) selection_range = selection_range.getRangeAt(0); else selection_range = undefined; if(selection_filter_enabled(selection_range) && selection_rejects_node(selection_range, textNode, offset)) selection_range = undefined; let text = ""; let moreText = ""; if(elemental) { moreText = textNode.value; text = moreText.substring(offset, moreText.length); } else { let etc = get_element_text_with_offsets(textNode, selection_range); moreText = etc[0]; offset -= etc[1]; text = moreText.substring(offset, moreText.length); } let lhs = grab_more_text(textNode, selection_range, -1); let rhs = grab_more_text(textNode, selection_range); text += rhs; moreText = lhs + moreText + rhs; if(settings.ignore_linebreaks) { text = text.replace(/\n/g, ""); moreText = moreText.replace(/\n/g, ""); } if(settings.strip_spaces) { text = text.replace(/  /g, ""); moreText = moreText.replace(/  /g, ""); } let index = moreText.lastIndexOf(text); let leftwards = 0; let rightwards = 0; if (!selection_filter_enabled(selection_range)) { while(!japaneseSeparators.includes(moreText[index + leftwards]) && moreText[index + leftwards] != "\n" && index + leftwards >= 0) leftwards--; leftwards++; while(!japaneseSeparators.includes(moreText[index + rightwards]) && moreText[index + rightwards] != "\n" && index + rightwards < moreText.length) rightwards++; while(japanesePunctuation.includes(moreText[index + rightwards]) && index + rightwards < moreText.length) rightwards++; moreText = moreText.substring(index+leftwards, index+rightwards); text = text.substring(0, rightwards); index = -leftwards; } // grab text from later and surrounding DOM nodes text = moreText.substring(index); return [text, moreText, index]; } function true_next_sibling(node) { if(node === null) return null; if(node.nextSibling !== null) return node.nextSibling; else return true_next_sibling(node.parentNode); } let last_seen_event = undefined; let shift_down = false; let ctrl_down = false; function ele_data_from_point(doc, x, y) { let ret = doc.elementFromPoint(x, y); while (ret && ret.tagName && ["frame", "iframe"].includes(ret.tagName.toLowerCase())) { let wind; try { wind = ret.contentWindow; if (!wind) break; } catch (err) { break; } let rect; try { rect = ret.getBoundingClientRect(); } catch (err) { break; } let rx = rect.x; let ry = rect.y; let sx1 = 0; let sy1 = 0; let sx2 = 0; let sy2 = 0; try { if (wind.parent && wind.parent !== wind) { sx1 = wind.parent.scrollX || 0; sy1 = wind.parent.scrollY || 0; } } catch (err) { // Cross-origin parent frame; stop descending further break; } try { sx2 = wind.scrollX || 0; sy2 = wind.scrollY || 0; } catch (err) { sx2 = 0; sy2 = 0; } x -= rx; y -= ry; x += sx2; y += sy2; try { doc = wind.document; } catch (err) { break; } try { ret = doc.elementFromPoint(x, y); } catch (err) { break; } } return [ret, doc, x, y]; } let last_event_time = 0; function update(event) { if(!settings.enabled) return; if(!event) return; if(event.timeStamp < last_event_time) return; last_event_time = event.timeStamp; last_seen_event = event; shift_down = event.shiftKey; ctrl_down = event.ctrlKey; if((settings.popup_requires_key == 2 && !shift_down) || (settings.popup_requires_key == 1 && !ctrl_down)) { function sq(n) {return n*n;} if(Math.sqrt(sq(last_display_x-event.pageX) + sq(last_display_y-event.pageY)) > 20) lookup_cancel(); return; } if(settings.popup_follows_mouse && exists_div() && !is_sticky() && platform != "android" ) { let other = get_div(); let middle = other.firstChild; if(middle) { last_lookup[3] = event.pageX; last_lookup[4] = event.pageY; display_div(middle, event.pageX, event.pageY); } } if(Date.now() - time_of_last < settings.lookuprate) { return; } if(platform == "android" && Date.now() - last_manual_interaction < 150) return; time_of_last = Date.now(); let textNode; let offset; let hitrect; let nodeResetList = []; let nodeResetSeen = new Set(); function reset_nodes() { for(let toreset of nodeResetList) { try { let element = toreset[0]; let z_index = toreset[1]; element.style.zIndex = z_index; } catch (err){} // don't care just reset everything correctly } } function acceptable_element(node) { if(!textNode) return false; if(textNode.nodeType != 1) return false; if(textNode.tagName.toLowerCase() == "textarea") return true; if(textNode.tagName.toLowerCase() == "input") if(["search", "submit", "text", "url"].includes(node.type.toLowerCase())) return true; return false; } // find the text under the mouse event let nodeIsBad = true; while(nodeIsBad) { nodeIsBad = false; let hitpage = function(x, searchoffset, y) { let xoffset = 0; let yoffset = 0; let data = ele_data_from_point(event.target.ownerDocument, x, y); let ele = data[0]; let mydoc = data[1]; x = data[2]; y = data[3]; if(ele && window.getComputedStyle(ele).writingMode.includes("vertical")) yoffset = searchoffset; else xoffset = searchoffset; let caretdata = mydoc.caretPositionFromPoint(x+xoffset, y+yoffset); if(caretdata) { textNode = caretdata.offsetNode; offset = caretdata.offset; try { let range = mydoc.createRange(); range.selectNode(textNode); hitrect = range.getBoundingClientRect(); } catch(e) { hitrect = undefined; } } // sticky mode and android need to break out on parent detection if(ele && !ele.contains(textNode) && is_sticky() && platform != "android" ) { textNode = undefined; offset = undefined; hitrect = undefined; } // break out if the mouse isn't actually over the hit rectangle if(hitrect && (x > hitrect.right+5 || x < hitrect.left-5+xoffset || y > hitrect.bottom+5 || y < hitrect.top-5+yoffset)) { textNode = undefined; offset = undefined; hitrect = undefined; } }; hitpage(event.clientX, search_x_offset, event.clientY); // try without the offset if (textNode == undefined || (textNode.nodeType != 3 && !acceptable_element(textNode))) { hitpage(event.clientX, 0, event.clientY); } if (exists_div() && (platform == "android" || is_sticky())) { let ele = get_div(); if(ele.contains(textNode)) { reset_nodes(); return; } } 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) && !acceptable_element(textNode)) { let style = getComputedStyle(textNode) let bg_color = style.getPropertyValue("background-color"); let position = style.getPropertyValue("position"); if(bg_color == "rgba(0, 0, 0, 0)" && (position == "absolute" || (position != "static" && textNode.tagName == "A"))) { nodeIsBad = true; nodeResetList.unshift([textNode, style.getPropertyValue("z-index")]); nodeResetSeen.add(textNode); textNode.style.zIndex = "-1000000000000000000000"; continue; } } } catch(err) { console.log(err); } } } reset_nodes(); if (settings.only_selection) { try { if(selection_rejects_node(window.getSelection().getRangeAt(0), textNode, offset)) { lookup_cancel(); return; } } catch(e){ } // window.getSelection() was null or null-like } // if there was text, use it let elemental = acceptable_element(textNode); if (textNode && (textNode.nodeType == 3 || elemental)) { let rect = undefined; let fud = 5; if(elemental) rect = textNode.getBoundingClientRect(); else rect = textNode.parentNode.getBoundingClientRect(); // 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); //let hit = (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom); if(!hit) { lookup_cancel(); return; } let found = grab_text(textNode, offset, elemental); let text = found[0]; let moreText = found[1]; let index = found[2]; text = text.substring(0, Math.min(text.length, settings.length)); if(text != "") lookup_enqueue(text, event.clientX, event.clientY, event.pageX, event.pageY, moreText, index, event.timeStamp); else lookup_cancel(); } else lookup_cancel(); } function update_touch(event) { if(event.touches) { update(event.touches[0]); } } function errormessage(text, soft = false) { if(!text) return; let mydiv = document.createElement("div"); if(!soft) mydiv.style = "background-color: #111; color: #CCC; font-family: Arial, sans-serif; font-size: 13px; width: 300px; border: 3px double red; position: fixed; right: 25px; top: 25px; z-index: 1000000000000000000000; padding: 5px; border-radius: 3px;" else mydiv.style = "background-color: #111; color: #CCC; font-family: Arial, sans-serif; font-size: 13px; width: 300px; border: 3px double orange; position: fixed; right: 25px; top: 25px; z-index: 1000000000000000000000; padding: 5px; border-radius: 3px;" mydiv.textContent = text; get_doc().body.appendChild(mydiv); function delete_later() { mydiv.remove(); } setTimeout(delete_later, 3000); } browser.runtime.onMessage.addListener((req, sender) => { if (req.type == "error") errormessage(req.error); }); async function mine_to_storage(object) { let cards = (await browser.storage.local.get("cards")).cards; if(!cards) cards = []; cards.push(object); browser.storage.local.set({cards:cards}); } function get_audio_text(mydiv, index = 0) // [reading, spelling] { mydiv = mydiv.getElementsByClassName("nazeka_audioref")[index] if(mydiv) { let text = mydiv.innerText; if(text.includes(";")) { let fields = text.split(";"); return [fields[0], fields[1]]; } else return [text, text]; } return undefined; } async function try_to_play_audio(index = 0) { if(!exists_div()) return; let mydiv = get_div(); let fields = get_audio_text(mydiv, index); if(fields) { let url = ""; if(settings.audio_force_https) url = "https://"; else url = "http://"; url += "assets.languagepod101.com/dictionary/japanese/audiomp3.php?kana=" + fields[0] + "&kanji=" + fields[1]; console.log("Trying to play audio " + url); browser.runtime.sendMessage({type:"play_audio", host:url, volume:settings.volume}); } } function send_ankiconnect_command(host, command) { let json = JSON.stringify(command); browser.runtime.sendMessage({type:"ankiconnect_mine", host:host, command:json}); } var __host = "http://127.0.0.1:8765" async function mine_to_ankiconnect_with_base64_audio(object, audiofname, audiodata) { let object_keys = Object.keys(object); let promise = browser.storage.local.get(["livemining_deckname", "livemining_modelname", "livemining_fields"]); promise.then((storage) => { if(storage["livemining_deckname"] === undefined) storage["livemining_deckname"] = "Default"; if(storage["livemining_modelname"] === undefined) storage["livemining_modelname"] = "Basic"; if(storage["livemining_fields"] === undefined) storage["livemining_fields"] = [["Front", "found_spelling"], ["Back", "readings"]]; console.log("attempting to mine card"); let note = { "action": "addNote", "version": 6, "params": { "note": { "deckName": storage["livemining_deckname"], "modelName": storage["livemining_modelname"], "fields": {}, "tags": [ "nazeka" ] } } }; let audioadded = false; let fields = {}; for(let pair of storage["livemining_fields"]) { if(pair[1] != "audio_anki" && object_keys.includes(pair[1])) fields[pair[0]] = String(object[pair[1]]); if(pair[1] == "audio_anki") { if(audiofname != "" && audiodata != "") { if(!audioadded) { let mediafilecommand = { "action": "storeMediaFile", "version": 6, "params": { "filename": audiofname, "data": audiodata } }; send_ankiconnect_command(__host, mediafilecommand); audioadded = true; } // for some reason not even anki 2.0 supports this //if(pair[1] == "audio_html") // fields[pair[0]] = ""; //else fields[pair[0]] = "[sound:" + audiofname + "]"; } else fields[pair[0]] = ""; } } note["params"]["note"]["fields"] = fields; send_ankiconnect_command(__host, note); }, (e) => {console.log(e);}); } async function get_url_blob_base64(url) { try { let data = await browser.runtime.sendMessage({type:"get_audio_base64", url:url}); return data; } catch(e) { console.log("errored"); console.log(e); errormessage("Getting audio failed: " + e); } } async function mine_to_ankiconnect(object) { try { let audio_base64 = ""; if(object.audio !== undefined) { let url = ""; if(settings.audio_force_https) url = "https://"; else url = "http://"; url += "assets.languagepod101.com/dictionary/japanese/audiomp3.php?kana=" + object.audio[0] + "&kanji=" + object.audio[1]; let response = await get_url_blob_base64(url); if(response) { let res = response.response; console.log("loaded"); let fname = "nazeka_audio_" + object.audio[0] + "_" + object.audio[1] + ".mp3"; let result = res.replace(/[^,]*,/, ""); mine_to_ankiconnect_with_base64_audio(object, fname, result); } else { console.log("errored"); errormessage("AnkiConnect mining partially failed: Failed to connect to languagepod101 to grab audio (going to try mining without audio)", true); mine_to_ankiconnect_with_base64_audio(object, "", ""); } } else mine_to_ankiconnect_with_base64_audio(object, "", ""); } catch(e) { errormessage("AnkiConnect failed: " + e); } } function mine(highlight) { let front = highlight.textContent; let word = highlight.parentElement.parentElement; let readings = ""; let readings_elements = word.getElementsByClassName("nazeka_readings"); if(readings_elements.length) readings = readings_elements[0].textContent; let storage_readings = readings; if(storage_readings.length < 2 || !storage_readings.startsWith("《")) storage_readings = "《" + storage_readings.trim() + "》"; let definitions = word.getElementsByClassName("nazeka_definitions")[0]; let lookup = word.parentElement.querySelector(".nazeka_lookup"); let sentence = word.parentElement.querySelector(".nazeka_lookup_sentence"); let index = word.parentElement.querySelector(".nazeka_lookup_index"); let seq = word.getAttribute("nazeka_seq"); mine_to_storage({front: front, readings: storage_readings, definitions: definitions.innerText, lookup: lookup.textContent, sentence: sentence.textContent, index: index.textContent, seq: seq}); if(settings.live_mining) { let now = new Date(); let readings_raw = word.querySelector(".nazeka_mining_readings").innerText; let spellings_raw = word.querySelector(".nazeka_mining_spellings").innerText; let audiostuff = get_audio_text(word); let audio_kanji = ""; let audio_reading = ""; if(readings.length >= 2 && readings.startsWith("《")) readings = readings.substring(1, readings.length-1); if(audiostuff !== undefined) { audio_kanji = audiostuff[1]; audio_reading = audiostuff[0]; } mine_to_ankiconnect( { nothing:"", found_spelling:front, readings:readings, definitions:definitions.innerHTML, definitions_raw:definitions.innerText, found_text: lookup.textContent, context: sentence.textContent, context_left: sentence.textContent.substring(0, index.textContent), context_right: sentence.textContent.substring(parseInt(index.textContent) + lookup.textContent.length), audio: audiostuff, audio_kanji: audio_kanji, audio_reading: audio_reading, jmdict_id: seq, url: get_doc().URL, page_title: get_doc().title, time_unix: now.getTime(), time_local: now.toLocaleString(), readings_raw: readings_raw, spellings_raw: spellings_raw, }); } } function keytest(event) { if(event.target != get_doc().body) return; if(settings.popup_requires_key == 2 && event.key == "Shift") { shift_down = true; update(last_seen_event); return; } if(settings.popup_requires_key == 1 && event.key == "Control") { ctrl_down = true; update(last_seen_event); return; } if(event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return; if(event.key == settings.hotkey_mine) { if(settings.kanji_mode) errormessage("Cannot mine isolated kanji (make kanji flashcards manually)"); else { if(mining_ui_exists()) { delete_mining_ui(); } else { if(!exists_div()) return; delete_mining_ui(); let mydiv = get_div().cloneNode(true); delete_div(); set_sticky_styles(mydiv); mydiv.className = "nazeka_mining_ui"; let newheader = document.createElement("div"); newheader.textContent = "Mining UI. Press the given entry's main spelling to mine it, or click this message to cancel."; newheader.addEventListener("click", ()=> { delete_mining_ui(); }); mydiv.firstChild.firstChild.prepend(newheader); mydiv.style.zIndex = 1000000000000000000000; // do not compound! z-index can be range limited by the browser mydiv.style.zIndex -= 1; for(let keb of mydiv.getElementsByClassName("nazeka_main_keb")) { keb.addEventListener("click", (event)=> { mine(event.target); delete_mining_ui(); }); } for(let reb of mydiv.getElementsByClassName("nazeka_main_reb")) { reb.addEventListener("click", (event)=> { mine(event.target); delete_mining_ui(); }); } get_doc().body.appendChild(mydiv); } } } if(event.key == settings.hotkey_kanji_mode) { settings.kanji_mode = !settings.kanji_mode; browser.storage.local.set({"kanji_mode":settings.kanji_mode}); if(exists_div()) send_lookup(last_lookup, performance.now()); } if(event.key == settings.hotkey_sticky) { settings.sticky = !settings.sticky; browser.storage.local.set({"sticky":settings.sticky}); if(exists_div()) { let last_text = last_lookup[0]; let last_index = last_lookup[6]; send_lookup([" ", last_lookup[1], last_lookup[2], last_lookup[3], last_lookup[4], last_lookup[5], 0], performance.now()); send_lookup([last_text, last_lookup[1], last_lookup[2], last_lookup[3], last_lookup[4], last_lookup[5], last_index], performance.now()); } } // audio (no mining ui) or mining (yes mining ui) based on number keys if("123456789".includes(event.key)) { let index = parseInt(event.key)-1; if(mining_ui_exists()) { if(settings.kanji_mode) errormessage("Cannot mine isolated kanji (make kanji flashcards manually)"); else { let mydiv = get_mining_ui(); let possibilities = mydiv.getElementsByClassName("nazeka_main_keb"); if(index >= possibilities.length) errormessage("Mining index out of range"); else { mine(possibilities[index]); delete_mining_ui(); } } } else { if(exists_div()) try_to_play_audio(index); } } if(event.key == settings.hotkey_nazeka_toggle) manual_toggle_enabled(); if(!exists_div()) return; if(event.key == settings.hotkey_close) lookup_cancel_force(); if(event.key == settings.hotkey_audio) try_to_play_audio(); if(event.key == settings.hotkey_nudge_left) lookup_left(); if(event.key == settings.hotkey_nudge_right) lookup_right(); } function keyuntest(event) { if(event.key == "Shift") shift_down = false; if(event.key == "Control") ctrl_down = false; } browser.runtime.onMessage.addListener((req, sender) => { if(req.type == "reader_lookup") send_lookup([req.text, req.x, req.y, req.x, req.y, req.text, 0], performance.now()); else if(req.type == "reader_mode") { console.log("we a readuh!"); is_reader = true; } }); window.addEventListener("mousemove", update); window.addEventListener("keydown", keytest); window.addEventListener("keyup", keyuntest); get_doc().addEventListener("touchstart", update_touch); console.log("inited content script"); console.log("inited at: " + get_doc().URL); console.log("inited via: " + document.URL); console.log("inited via: " + window.location);