// 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) { 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 // we only use a tiny number of settings here let settings = { reader_width: 800, reader_height: 300, deconjugator_rules_json: "", freqlist_mode: 1, ankiconnect_force_sync: false, json_ignore_entries: "一,二,三,四,五,六,七,八,九,〇" }; 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("reader_width"); getvar("reader_height"); getvar("deconjugator_rules_json"); getvar("freqlist_mode"); getvar("ankiconnect_force_sync"); getvar("json_ignore_entries"); } 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(option == "enabled") fixicon(); } }); // We use JMdict converted to JSON. It's like 32MB (larger after converting into a data structure) so we don't want to load it for every tab. // So we need to use a background script and send messages to it from the content script. // Unfortunately webextensions don't have an easy way to load files, even from in the extension. // We have to send an HTTP request and store the result in a string before parsing it into an object. // Seriously. let json_dicts = []; function add_json_reading(dict, text, id) { if(text === "") return; if (!dict.lookup_json_kana.has(text)) dict.lookup_json_kana.set(text, [id]); else dict.lookup_json_kana.get(text).push(id); } function add_json_spelling(dict, text, id) { if(text === "") return; if (!dict.lookup_json_kan.has(text)) dict.lookup_json_kan.set(text, [id]); else dict.lookup_json_kan.get(text).push(id); } function or_default(val, fallback) { if(val !== undefined) return val; else return fallback; } async function get_storage_or_default(name, fallback) { return or_default((await browser.storage.local.get(name))[name], fallback); } async function refresh_json() { let custom_dicts = (await get_storage_or_default("custom_dicts", [])); let new_json_dicts = []; for(let stored_dict of custom_dicts) { if(!stored_dict.enabled) continue; let name = stored_dict.name; let entries = JSON.parse(stored_dict.entries); let lookup_json_kan = new Map(); let lookup_json_kana = new Map(); let dict = {"name": name, "entries": entries, "lookup_json_kan": lookup_json_kan, "lookup_json_kana": lookup_json_kana}; try { for (let i = 0; i < entries.length; i++) { add_json_reading(dict, entries[i]["r"], i); for(let spelling of entries[i]["s"]) add_json_spelling(dict, spelling, i); } } catch(e) { console.log("error while loading/reloading json dictionary:"); console.log(e); } new_json_dicts.push(dict); } json_dicts = new_json_dicts; } async function migrate_legacy_then_refresh() { let myjson = (await browser.storage.local.get(["epwing"]))["epwing"]; if(myjson !== undefined) { let epwing = JSON.parse(myjson); let name = epwing.shift(); let custom_dicts = (await get_storage_or_default("custom_dicts", [])); custom_dicts.push({"name": name, "entries": JSON.stringify(epwing), "enabled": true}); browser.storage.local.set({"custom_dicts":custom_dicts}); browser.storage.local.remove(["epwing"]); } refresh_json(); } migrate_legacy_then_refresh(); function json_lookup_arbitrary_as_is(dict, text) { let index = dict.lookup_json_kan.get(text); if(index === undefined) index = dict.lookup_json_kana.get(text); if(index === undefined) return undefined; return copy(dict.entries[index]); } function json_dict_any_as_is(text) { let maybe_found_non_latin = false; for(let i = 0; i < text.length; i++) { let codepoint = text.codePointAt(i); if(codepoint > 0x100) maybe_found_non_latin = true; } if(maybe_found_non_latin) { for(let dict of json_dicts) { let ret = json_lookup_arbitrary_as_is(dict, text); if(ret !== undefined) return ret; } } return undefined; } let dict = []; let lookup_kan = new Map(); let lookup_kana = new Map(); let dictsloaded = 0; function builddict() { if (this.readyState === 4) { if (this.status === 200) { dict = dict.concat(JSON.parse(this.responseText)); dictsloaded++; } else console.error(xhr.statusText); } } let lookup_audio = new Set(); let lookup_audio_broken = new Map(); function build_audio_table() { if (this.readyState === 4) { if (this.status === 200) { let i = 0; let j = 0; while ((j = this.responseText.indexOf("\n", i)) !== -1) { let text = this.responseText.substring(i, j); if(!text.includes(",")) lookup_audio.add(text); else lookup_audio_broken.set(text.split(",")[1], text.split(",")[0]); i = j + 1; } let text = this.responseText.substring(i, j); if(!text.includes(",")) lookup_audio.add(text); else lookup_audio_broken.set(text.split(",")[1], text.split(",")[0]); } else console.error(xhr.statusText); } } let kanji_data = new Map(); function load_kanji() { if (this.readyState === 4) { if (this.status === 200) { let data = JSON.parse(this.responseText); for(let entry of data) kanji_data.set(entry["c"], entry); } else console.error(xhr.statusText); } } // Having a >4MB flat text file with stupid adhoc formatting is okay // but having a >4MB json file with nothing weird in it isn't // Thanks, linter team! function load_part_of_dictionary(filename) { let req = new XMLHttpRequest(); req.addEventListener("load", builddict); req.open("GET", browser.extension.getURL(filename)); req.send(); return req; } function load_jdic_audio_table(filename) { let req = new XMLHttpRequest(); req.addEventListener("load", build_audio_table); req.open("GET", browser.extension.getURL(filename)); req.send(); return req; } function load_kanji_data(filename) { let req = new XMLHttpRequest(); req.addEventListener("load", load_kanji); req.open("GET", browser.extension.getURL(filename)); req.send(); return req; } load_part_of_dictionary("dict/JMdict1.json"); load_part_of_dictionary("dict/JMdict2.json"); load_part_of_dictionary("dict/JMdict3.json"); load_part_of_dictionary("dict/JMdict4.json"); load_part_of_dictionary("dict/JMdict5.json"); load_part_of_dictionary("dict/JMdict6.json"); load_part_of_dictionary("dict/JMdict7.json"); load_part_of_dictionary("dict/JMdict8.json"); load_part_of_dictionary("dict/JMdict9.json"); load_part_of_dictionary("dict/JMdict10.json"); load_part_of_dictionary("dict/JMdict11.json"); load_part_of_dictionary("dict/JMdict12.json"); load_part_of_dictionary("dict/JMdict13.json"); load_jdic_audio_table("dict/jdic audio.txt"); load_kanji_data("dict/kanjidata.json"); function buildlookups() { // Build map of spellings to dictionary entry. for (let i = 0; i < dict.length; i++) { let entry = dict[i]; if (entry.k_ele != undefined) for (let j = 0; j < entry.k_ele.length; j++) { let s = entry.k_ele[j]; if (!lookup_kan.has(s.keb)) lookup_kan.set(s.keb, [i]); else lookup_kan.get(s.keb).push(i); } for (let j = 0; j < entry.r_ele.length; j++) { let s = entry.r_ele[j]; if (!lookup_kana.has(s.reb)) lookup_kana.set(s.reb, [i]); else lookup_kana.get(s.reb).push(i); } } for (let i = 0; i < 5; i++) { console.log(dict[i]); } console.log("built lookup tables"); console.log("size:"); console.log(lookup_kan.size); console.log(lookup_kana.size); console.log("dict size:"); console.log(dict.length); } let waiter = undefined; function waittobuildlookups() { if(dictsloaded == 13) { clearInterval(waiter); waiter = undefined; buildlookups(); } } waiter = setInterval(waittobuildlookups, 1000); // 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 getfromdict(indexes, text) { let ret = []; for(let i = 0; i < indexes.length; i++) { let entry = {}; Object.assign(entry, dict[indexes[i]]); entry.from = text; if(entry.k_ele) { for(let spelling of entry.k_ele) { if(spelling.keb == entry.from) { entry.found = spelling; break; } } } if(!entry.found && entry.r_ele) { for(let spelling of entry.r_ele) { if(spelling.reb == entry.from) { entry.found = spelling; break; } } } let lastpos = []; // JMdict tags part-of-speech for senses sparsely // e.g. if 1. and 2. share the same list of POSs, only 1. will have a POS list // undoing this here makes things in the content script WAY less inconvenient for(let j = 0; j < entry.sense.length; j++) { if(entry.sense[j].pos) { for(let t = 0; t < entry.sense[j].pos.length; t++) { if(entry.sense[j].pos[t].charAt(0)=="&") entry.sense[j].pos[t] = clip(entry.sense[j].pos[t]); } } else entry.sense[j].pos = lastpos; lastpos = entry.sense[j].pos; } ret.push(entry); } return ret; } function replace_hira_with_kata(text) { let newtext = ""; for(let i = 0; i < text.length; i++) { let codepoint = text.codePointAt(i); if(codepoint >= 0x3040 && codepoint <= 0x3096) codepoint += (0x30A0 - 0x3040); else if(codepoint >= 0x309D && codepoint <= 0x309E) codepoint += (0x30A0 - 0x3040); newtext += String.fromCodePoint(codepoint); } return newtext; } function replace_kata_with_hira(text) { let newtext = ""; for(let i = 0; i < text.length; i++) { let codepoint = text.codePointAt(i); if(codepoint >= 0x30A0 && codepoint <= 0x30F6) codepoint -= (0x30A0 - 0x3040); else if(codepoint >= 0x30FD && codepoint <= 0x30FE) codepoint -= (0x30A0 - 0x3040); newtext += String.fromCodePoint(codepoint); } return newtext; } function search_inner(text) { if (lookup_kan.has(text)) return getfromdict(lookup_kan.get(text), text); else if (lookup_kana.has(text)) return getfromdict(lookup_kana.get(text), text); else return; } function search(text) { let ret = undefined; ret = search_inner(text); if(ret) return ret; ret = search_inner(replace_hira_with_kata(text)); if(ret) return ret; ret = search_inner(replace_kata_with_hira(text)); if(ret) return ret; ret = json_dict_any_as_is(text); if(ret) { let r_ele = [{reb: ret.r}]; let k_ele = []; let seq = "NONE_"+text; let sense = [{gloss: [], pos: []}]; for(let spelling of ret.s) { if(spelling == "" || is_kana(spelling)) continue; k_ele.push({keb: spelling}); } let re_ret = undefined; if(k_ele == undefined || k_ele.length == 0) re_ret = [{r_ele: r_ele, seq: seq, sense: sense, from: text, found: r_ele[0]}]; else re_ret = [{k_ele: k_ele, r_ele: r_ele, seq: seq, sense: sense, from: text, found: k_ele[0]}]; return re_ret; } } // priority rules let priority_rules = []; function load_priority_rules(filename) { let req = new XMLHttpRequest(); req.addEventListener("load", () => { if (req.readyState === 4) { console.log("loaded priority rules"); if (req.status === 200) priority_rules = JSON.parse(req.responseText); else console.error(req.statusText); } }); req.open("GET", browser.extension.getURL(filename)); req.send(); return req; } load_priority_rules("dict/priority.json"); // vn frequency data let freq_vns = {}; function load_freqlist_vns(filename) { let req = new XMLHttpRequest(); req.addEventListener("load", () => { if (req.readyState === 4) { console.log("loaded vn freq data"); if (req.status === 200) freq_vns = JSON.parse(req.responseText); else console.error(req.statusText); } }); req.open("GET", browser.extension.getURL(filename)); req.send(); return req; } load_freqlist_vns("dict/freqlist_vns.json"); // narou frequency data let freq_narou = {}; function load_freqlist_narou(filename) { let req = new XMLHttpRequest(); req.addEventListener("load", () => { if (req.readyState === 4) { console.log("loaded narou freq data"); if (req.status === 200) freq_narou = JSON.parse(req.responseText); else console.error(req.statusText); } }); req.open("GET", browser.extension.getURL(filename)); req.send(); return req; } load_freqlist_narou("dict/freqlist_narou.json"); // narou frequency data let freq_novels = {}; function load_freqlist_novels(filename) { let req = new XMLHttpRequest(); req.addEventListener("load", () => { if (req.readyState === 4) { console.log("loaded novels freq data"); if (req.status === 200) freq_novels = JSON.parse(req.responseText); else console.error(req.statusText); } }); req.open("GET", browser.extension.getURL(filename)); req.send(); return req; } load_freqlist_novels("dict/freqlist_novels.json"); // deconjugation rules let rules = []; function load_deconjugation_rules(filename) { let req = new XMLHttpRequest(); req.addEventListener("load", () => { if (req.readyState === 4) { console.log("loaded deconjugation rules"); if (req.status === 200) rules = JSON.parse(req.responseText); else console.error(req.statusText); } }); req.open("GET", browser.extension.getURL(filename)); req.send(); return req; } load_deconjugation_rules("dict/deconjugator.json"); // return deconjugated form if stdrule applies to form, return otherwise function stdrule_deconjugate_inner(my_form, my_rule) { // ending doesn't match if(!my_form.text.endsWith(my_rule.con_end)) return; // tag doesn't match if(my_form.tags.length > 0 && my_form.tags[my_form.tags.length-1] != my_rule.con_tag) return; let newtext = my_form.text.substring(0, my_form.text.length-my_rule.con_end.length)+my_rule.dec_end; // I hate javascript reeeeeeeeeeeeeee let newform = {};//new Object(); newform.text = newtext; newform.original_text = my_form.original_text; newform.tags = my_form.tags.slice(); newform.seentext = new Set([...my_form.seentext]); newform.process = my_form.process.slice(); newform.text = newtext; newform.process.push(my_rule.detail); if(newform.tags.length == 0) newform.tags.push(my_rule.con_tag); newform.tags.push(my_rule.dec_tag); if(newform.seentext.size == 0) newform.seentext.add(my_form.text); newform.seentext.add(newtext); return newform; }; function stdrule_deconjugate(my_form, my_rule) { // can't deconjugate nothingness if(my_form.text == "") return; // deconjugated form too much longer than conjugated form if(my_form.text.length > my_form.original_text.length+10) return; // impossibly information-dense if(my_form.tags.length > my_form.original_text.length+6) return; // blank detail mean it can't be the last (first applied, but rightmost) rule if(my_rule.detail == "" && my_form.tags.length == 0) return; let array = undefined; // pick the first one that is an array // FIXME: use minimum length for safety reasons? assert all arrays equal length? if(Array.isArray(my_rule.dec_end)) array = my_rule.dec_end; else if(Array.isArray(my_rule.con_end)) array = my_rule.con_end; else if(Array.isArray(my_rule.dec_tag)) array = my_rule.dec_tag; else if(Array.isArray(my_rule.con_tag)) array = my_rule.con_tag; if(array == undefined) return stdrule_deconjugate_inner(my_form, my_rule); else { let collection = new Set(); for(let i = 0; i < array.length; i++) { let virtual_rule = {}; virtual_rule.type = my_rule.type; let index_or_value = function (variable, index) { if(Array.isArray(variable)) return variable[index]; else return variable; } virtual_rule.dec_end = index_or_value(my_rule.dec_end, i); virtual_rule.con_end = index_or_value(my_rule.con_end, i); virtual_rule.dec_tag = index_or_value(my_rule.dec_tag, i); virtual_rule.con_tag = index_or_value(my_rule.con_tag, i); virtual_rule.detail = my_rule.detail; let ret = stdrule_deconjugate_inner(my_form, virtual_rule); if(ret) collection.add(ret); } return collection; } }; function rewriterule_deconjugate(my_form, my_rule) { if(my_form.text != my_rule.con_end) return; return stdrule_deconjugate(my_form, my_rule); }; function onlyfinalrule_deconjugate(my_form, my_rule) { if(my_form.tags.length != 0) return; return stdrule_deconjugate(my_form, my_rule); }; function neverfinalrule_deconjugate(my_form, my_rule) { if(my_form.tags.length == 0) return; return stdrule_deconjugate(my_form, my_rule); }; function contextrule_deconjugate(my_form, my_rule) { if(!context_functions[my_rule.contextrule](my_form, my_rule)) return; return stdrule_deconjugate(my_form, my_rule); }; function substitution_inner(my_form, my_rule) { if(!my_form.text.includes(my_rule.con_end)) return; let newtext = my_form.text.replace(new RegExp(my_rule.con_end, 'g'), my_rule.dec_end); // I hate javascript reeeeeeeeeeeeeee let newform = {};//new Object(); newform.text = newtext; newform.original_text = my_form.original_text; newform.tags = my_form.tags.slice(); newform.seentext = new Set([...my_form.seentext]); newform.process = my_form.process.slice(); newform.text = newtext; newform.process.push(my_rule.detail); if(newform.seentext.size == 0) newform.seentext.add(my_form.text); newform.seentext.add(newtext); return newform; }; function substitution_deconjugate(my_form, my_rule) { if(my_form.process.length != 0) return; // can't deconjugate nothingness if(my_form.text == "") return; let array = undefined; // pick the first one that is an array // FIXME: use minimum length for safety reasons? assert all arrays equal length? if(Array.isArray(my_rule.dec_end)) array = my_rule.dec_end; else if(Array.isArray(my_rule.con_end)) array = my_rule.con_end; if(array == undefined) return substitution_inner(my_form, my_rule); else { let collection = new Set(); for(let i = 0; i < array.length; i++) { let virtual_rule = {}; virtual_rule.type = my_rule.type; let index_or_value = function (variable, index) { if(Array.isArray(variable)) return variable[index]; else return variable; } virtual_rule.dec_end = index_or_value(my_rule.dec_end, i); virtual_rule.con_end = index_or_value(my_rule.con_end, i); virtual_rule.detail = my_rule.detail; let ret = substitution_inner(my_form, virtual_rule); if(ret) collection.add(ret); } return collection; } } let rule_functions = { stdrule: stdrule_deconjugate, rewriterule: rewriterule_deconjugate, onlyfinalrule: onlyfinalrule_deconjugate, neverfinalrule: neverfinalrule_deconjugate, contextrule: contextrule_deconjugate, substitution: substitution_deconjugate }; function v1inftrap_check(my_form, my_rule) { if(my_form.tags.length != 1) return true; let my_tag = my_form.tags[0]; if(my_tag == "stem-ren") return false; return true; }; function saspecial_check(my_form, my_rule) { if(my_form.text == "") return false; if(!my_form.text.endsWith(my_rule.con_end)) return false; let base_text = my_form.text.substring(0, my_form.text.length-my_rule.con_end.length); if(base_text.endsWith("さ")) return false; return true; }; let context_functions = { v1inftrap: v1inftrap_check, saspecial: saspecial_check, }; function union(setA, setB) { for (let elem of setB) setA.add(elem); return setA; } // Returns a set of objects, each containing the "final form" of a series of deconjugations // The majority of these objects will fail being compared to the dictionary forms // The algorithm is a right-edge rule checker that finds all applicable series of rules. // TODO: add all underlay rule types function deconjugate(mytext) { let processed = new Set(); let novel = new Set(); let start_form = {text: mytext, original_text: mytext, tags: [], seentext: new Set(), process: [] }; novel.add(start_form); let myrules = rules; try { if(settings.deconjugator_rules_json != "") myrules = JSON.parse(settings.deconjugator_rules_json); } catch(err) { console.log("error while parsing deconjugation rules:"); console.log(err); } while(novel.size > 0) { //print_object(novel); let new_novel = new Set(); for(let form of novel) { for(let rule of myrules) { if(!(rule instanceof Object)) continue; let newform = rule_functions[rule.type](form, rule); if(newform != undefined && newform.constructor === Set) { for(let myform of newform) { if(myform != undefined && !processed.has(myform) && !novel.has(myform) && !new_novel.has(myform)) new_novel.add(myform); } } else if(newform != undefined && !processed.has(newform) && !novel.has(newform) && !new_novel.has(newform)) new_novel.add(newform); } } processed = union(processed, novel); novel = new_novel; } return processed; } // takes deconjugated forms and looks them up in a dictionary // building a list of all matches function build_lookup_comb(forms) { // FIXME: dumpster fire that shouldn't be anywhere near as complicated as I made it // This is the only part of this function that doesn't look like a garbage fire. // Looks for dictionary definitions of each deconjugated form. let looked_up = {}; for(let form of forms) { if(!looked_up.hasOwnProperty(form.text)) { let result = search(form.text); let copied_result = []; if(result) { for(let r = 0; r < result.length; r++) { let entry = result[r]; //Object.assign(entry, result[r]); entry.deconj = undefined; // clear if something set it before, we need this later // store all the parts of speech that this dictionary entry can apply to // normally, they're scattered across its senses // FIXME: care about senses with restricted spellings when building part-of-speech tags list entry.allpos = new Set(); for(let i = 0; i < entry.sense.length; i++) { let sense = entry.sense[i]; for(let j = 0; j < sense.pos.length; j++) entry.allpos.add(sense.pos[j]); } copied_result.push(entry); } looked_up[form.text] = copied_result; } } } // FIXME: This is garbage. // Add deconjugation field to all those entries for(let form of forms) { if(looked_up.hasOwnProperty(form.text)) { let result = looked_up[form.text]; for(let entry of result) { // If the form requires definition to have a particular POS tag, check for it if(form.tags.length > 0) { if(entry.allpos.has(form.tags[form.tags.length-1])) { if(entry.deconj === undefined) entry.deconj = new Set(); entry.deconj.add(form); } } else { if(entry.deconj === undefined) entry.deconj = new Set(); entry.deconj.add(form); } } } } // FIXME: This should merge based on map from JMdict entity ID to entry. // FIXME: Because it's theoretically possible to get the same definition from different spellings from different deconjugations of the same string. // Make a list of each definition that has an associated deconjugation (even if it's a zero-depth deconjugation) let merger = []; for(let result of Object.values(looked_up)) { for(let entry of result) { if(entry.deconj) { entry.deconj = Array.from(entry.deconj); merger.push(entry); } } } //print_object(merger); if(merger.length > 0) return merger; else return; } function json_lookup_kanji(dict, spelling_list, readings, inexact) { let exclude = settings.json_ignore_entries.split(",").map(x => x.trim()); let indexes_set = new Set(); let indexes = []; for(let spelling of spelling_list) { let possibilities = dict.lookup_json_kan.get(spelling); if(!possibilities) continue; for(let id of dict.lookup_json_kan.get(spelling)) { let bad = false; for(let spelling of dict.entries[id]["s"]) { if(exclude.includes(spelling)) { bad = true; break; } } if(bad) break; if(inexact || readings.includes(dict.entries[id]["r"])) { if(!indexes_set.has(id)) { indexes_set.add(id); indexes.push(id); } } } } return indexes; } function json_lookup_kana_exact(dict, kana) { let exclude = settings.json_ignore_entries.split(",").map(x => x.trim()); let possibilities = dict.lookup_json_kana.get(kana); if(!possibilities) return []; let actual_possibilities = []; for(let index of possibilities) { let spellings = dict.entries[index]["s"] if(spellings.length == 1 && spellings[0] === "") { if(exclude.includes(kana)) continue; actual_possibilities.push(index); } else { for(let spelling of spellings) { if(exclude.includes(spelling)) break; if(replace_kata_with_hira(spelling) == replace_kata_with_hira(kana)) { actual_possibilities.push(index); break; } } } } return actual_possibilities; } function json_lookup_kana_inexact(dict, kana) { let possibilities = copy_gen(dict.lookup_json_kana.get(kana)); if(!possibilities) return []; return possibilities; } function add_json_info(lookups, other_settings) { for(let lookup of lookups) { for(let entry of lookup.result) { entry.json = []; for(let dict of json_dicts) { // look for an json entry that matches this lookup and jmdict entry let json_data = undefined; let foundtext = undefined; if(entry.orig_found.keb) foundtext = entry.orig_found.keb; else foundtext = entry.orig_found.reb; if(entry.k_ele) { let spellings = [lookup.text, foundtext]; for(let spelling of entry.k_ele) spellings.push(spelling.keb); let core_readings_a = [lookup.text, foundtext]; let core_readings_b = [entry.r_ele[0].reb]; let readings = []; for(let reading of entry.r_ele) readings.push(reading.reb); // try exact matches let possibilities = json_lookup_kanji(dict, spellings, core_readings_a, false); if(possibilities.length == 0) possibilities = json_lookup_kanji(dict, spellings, core_readings_b, false); if(possibilities.length == 0) possibilities = json_lookup_kanji(dict, spellings, readings, false); // try inexact matches if(!other_settings.strict_epwing) { if(possibilities.length == 0) possibilities = json_lookup_kanji(dict, spellings, core_readings_a, true); if(possibilities.length == 0) possibilities = json_lookup_kanji(dict, spellings, core_readings_b, true); if(possibilities.length == 0) possibilities = json_lookup_kanji(dict, spellings, readings, true); } if(possibilities.length > 0) json_data = copy(dict.entries[possibilities[0]]); } else { let possibilities = json_lookup_kana_exact(dict, foundtext); if(!other_settings.strict_epwing) { for(let reading of entry.r_ele) { if(possibilities.length == 0) possibilities = json_lookup_kana_exact(dict, reading.reb); } if(possibilities.length == 0) possibilities = json_lookup_kana_inexact(dict, foundtext); for(let reading of entry.r_ele) { if(possibilities.length == 0) possibilities = json_lookup_kana_inexact(dict, reading.reb); } } if(possibilities.length > 0) json_data = copy(dict.entries[possibilities[0]]); } if(json_data) { // add dictionary title json_data["z"] = dict.name; // add to lookup entry.json.push(json_data); } } } } return lookups; } function is_kana(object) { if(object.seq && object.k_ele) return false; if(object.seq) return true; if(typeof object !== "string") return false; for(let i = 0; i < object.length; i++) { let codepoint = object.codePointAt(i); if(!(codepoint >= 0x3040 && codepoint <= 0x30FF)) return false; } return true; } function prefers_kana(object) { if(object.seq) { for(let sense of object.sense) { if(sense.misc) for(let misc of sense.misc) { if(clip(misc) == "uk" || clip(misc) == "ek") return true; } } } return false; } function prefers_kanji(object) { if(object.seq) { for(let sense of object.sense) { if(sense.misc) for(let misc of sense.misc) { if(clip(misc) == "uK" || clip(misc) == "eK") return true; } } } return false; } function all_senses_have_a_tag(object, tag) { if(object.seq) { for(let sense of object.sense) { let anymatch = false; if(sense.misc) { for(let misc of sense.misc) { if(tag.length) { for(let actualtag of tag) if(clip(misc) == actualtag) anymatch = true; } else if(clip(misc) == tag) anymatch = true; } } if(!anymatch) return false; } return true; } return false; } function has_pri(object) { if(object.seq) { for(let sense of object.sense) { if(object.k_ele) for(let ele of object.k_ele) { if(ele.pri) return true; } if(object.r_ele) for(let ele of object.r_ele) { if(ele.pri) return true; } } } return false; } function weird_lookup(found) { if(found && found.inf) { for(let info of found.inf) { if(["ik", "iK", "io", "ok", "oK"].includes(clip(info))) return true; } } } function sort_results(text, results) { if(results == undefined) return; let reading_kana = is_kana(text); for(let entry of results) { let result_kana = is_kana(entry); entry.priority = (entry.seq-1000000)/-10000000; // divided by one more order of magnitude entry.priorityTags = []; if(weird_lookup(entry.found)) { entry.priority -= 50; entry.priorityTags.push("weird"); } if(reading_kana == result_kana && !entry.deconj) // !entry.deconj fixes なって { entry.priority += 100; entry.priorityTags.push("exact kana"); } if(has_pri(entry)) { entry.priority += 30; entry.priorityTags.push("pri"); } if(!reading_kana && prefers_kanji(entry)) { entry.priority += 12; entry.priorityTags.push("kanji prefers kanji"); } if(reading_kana && prefers_kana(entry)) { entry.priority += 10; entry.priorityTags.push("kana prefers kana"); } if(reading_kana && prefers_kanji(entry)) { entry.priority -= 12; entry.priorityTags.push("kanji disprefers kana"); } if(!reading_kana && prefers_kana(entry)) { entry.priority -= 10; entry.priorityTags.push("kana disprefers kanji"); } if(entry.sense && entry.sense.length >= 3) { entry.priority += 3; entry.priorityTags.push("many senses"); } // FIXME: affects words with only one obscure/rare/obsolete sense if(all_senses_have_a_tag(entry, ["obsc", "rare", "obs"])) { entry.priority -= 5; entry.priorityTags.push("obscure"); } if(entry.deconj && entry.deconj[0].process.length == 0) { entry.priority += 1; entry.priorityTags.push("no deconj"); } if(entry.deconj && entry.deconj[0].process.length > 2) { entry.priority -= 1; entry.priorityTags.push("long deconj"); } let boost = 0; // looked up and found kanji if(entry.found.keb) { for(let r of entry.r_ele) { for(let rule of priority_rules) { if(rule[0] == entry.found.keb && rule[1] == r.reb) { if(boost == 0) boost = rule[2]; else boost = Math.max(boost, rule[2]); } } } } // looked up kana and found a kanji word else if(entry.k_ele) { for(let k of entry.k_ele) { for(let rule of priority_rules) { // this fallback only happens when searching against text looked up in kana if(is_kana(rule[0])) { if(rule[0] == text && rule[1] == k.keb) { if(boost == 0) boost = rule[2]; else boost = Math.max(boost, rule[2]); } } else if(rule[0] == k.keb && rule[1] == entry.found.reb) { if(boost == 0) boost = rule[2]; else boost = Math.max(boost, rule[2]); } } } } // looked up kana and found a word with no kanji else { for(let rule of priority_rules) { if(rule[0] == "" && rule[1] == entry.found.reb) { if(boost == 0) boost = rule[2]; else boost = Math.max(boost, rule[2]); } } } if(boost != 0) entry.priorityTags.push("boost"); entry.priority += boost; } results.sort((a,b)=> { return b.priority - a.priority; //return a.priority - b.priority; }); return results; } function filter_kana_ish_results(results) { if(results == undefined) return; let newresults = []; for(let entry of results) { if(is_kana(entry) || prefers_kana(entry)) newresults.push(entry); } return newresults; } function copy(orig) { if(orig === undefined) return undefined; let mine; if (Set.prototype.isPrototypeOf(orig)) mine = new Set(orig); else mine = Object.assign({}, orig); for(let f in mine) { if(f !== orig) f = copy(f); } return mine; } function copy_gen(orig) { return orig.slice(0); } // Restrict the listed spellings/readings of a JMdict entry to the ones allowed by the looked-up text. function restrict_by_text(entry, text) { // deep clone lol (we should probably do this WAY earlier) let term = copy(entry); if(!term.found) // bogus lookup return term; // Example restrictions: // ゆう: 夕 only // さくや: 昨夜 only // evening: 夕・夕べ only // last night: ゆうべ・さくや only // Derived: // 昨夜: さくや・ゆうべ only // 夕: ゆう・ゆうべ only // 夕べ: ゆうべ only // evening: ゆう・ゆうべ only // last night: 夕べ・昨夜 only // if we didn't look up kanji, look for the first fitting kanji spellings in the entry (they're ordered sensibly) if there are kanji spellings term.orig_found = term.found; if(!!(term.found.reb) && term.k_ele) { let r_restr = undefined; // kanji to which the reb is restricted if(term.found.restr && term.found.restr.length > 0) r_restr = term.found.restr; if(!r_restr) r_restr = []; // find the first kanji-including spelling that isn't restricted to readings that aren't the one we looked up for(let j = 0; j < term.k_ele.length; j++) { // if this spelling is restricted to particular readings if(term.k_ele[j].restr) { for(let l = 0; l < term.k_ele[j].restr.length; l++) { // if the reading we looked up is one of the ones it's restricted to // if the reading we looked up isn't restricted to spellings other than this one if(term.k_ele[j].restr[l] == term.found.reb && (r_restr.length == 0 || r_restr.indexOf(term.k_ele[j].keb) > -1)) { // pretend we looked up this spelling term.found = term.k_ele[j]; break; } } } // if the spelling does not have restrictions // if the reading is not restricted to other spellings else if(r_restr.length == 0 || r_restr.indexOf(term.k_ele[j].keb) > -1) { // pretend we looked up this spelling term.found = term.k_ele[j]; break; } } } // eliminate unfitting kanji spellings if we originally looked up a reading if(!!(term.orig_found.reb) && term.k_ele) { let r_restr = undefined; // kanji to which the reb is restricted if(term.orig_found.restr && term.orig_found.restr.length > 0) r_restr = term.orig_found.restr; else r_restr = []; let new_k_ele = []; for(let j = 0; j < term.k_ele.length; j++) { if(r_restr.length > 0 && r_restr.indexOf(term.k_ele[j].keb) < 0) continue; // if this spelling is restricted to particular readings if(term.k_ele[j].restr) { for(let l = 0; l < term.k_ele[j].restr.length; l++) // if the reading we looked up is one of the ones it's restricted to // if the reading we looked up isn't restricted to spellings other than this one if(term.k_ele[j].restr[l] == term.orig_found.reb) new_k_ele.push(term.k_ele[j]); } // if the spelling does not have restrictions // if the reading is not restricted to other spellings else new_k_ele.push(term.k_ele[j]); } term.k_ele = new_k_ele; } // eliminate unfitting readings if we originally looked up a spelling if(!!(term.orig_found.keb) && term.r_ele) { let k_restr = undefined; // kanji to which the reb is restricted if(term.orig_found.restr && term.orig_found.restr.length > 0) k_restr = term.orig_found.restr; else k_restr = []; let new_r_ele = []; for(let j = 0; j < term.r_ele.length; j++) { if(k_restr.length > 0 && k_restr.indexOf(term.r_ele[j].reb) < 0) continue; if(term.r_ele[j].restr) { for(let l = 0; l < term.r_ele[j].restr.length; l++) if(term.r_ele[j].restr[l] == term.orig_found.keb) new_r_ele.push(term.r_ele[j]); } else new_r_ele.push(term.r_ele[j]); } term.r_ele = new_r_ele; } // eliminate unfitting definitions for the original lookup if(term.sense) { let new_sense = []; for(let j = 0; j < term.sense.length; j++) { if(term.orig_found.keb && term.sense[j].stagk) { for(let l = 0; l < term.sense[j].stagk.length; l++) if(term.sense[j].stagk[l] == term.orig_found.keb) new_sense.push(term.sense[j]); } else if(term.orig_found.reb && term.sense[j].stagr) { for(let l = 0; l < term.sense[j].stagr.length; l++) if(term.sense[j].stagr[l] == term.orig_found.reb) new_sense.push(term.sense[j]); } // if the spelling does not have restrictions // if the reading is not restricted to other spellings else new_sense.push(term.sense[j]); } term.sense = new_sense; } return term; } function add_extra_info(results, other_settings) { for(let lookup of results) { for(let entry of lookup.result) { entry.has_audio = []; if(!entry.k_ele || entry.k_ele.length == 0) { for(let r of entry.r_ele) { if(lookup_audio.has(r.reb)) entry.has_audio.push(r.reb); else if(lookup_audio_broken.has(r.reb)) entry.has_audio.push(lookup_audio_broken.get(r.reb)); } } else if(entry.k_ele) { for(let k of entry.k_ele) { for(let r of entry.r_ele) { let test_string = r.reb + ";" + k.keb; if(lookup_audio.has(test_string)) entry.has_audio.push(test_string); else if(lookup_audio_broken.has(test_string)) entry.has_audio.push(lookup_audio_broken.get(test_string)); } } } } } let ret = add_json_info(results, other_settings); if(settings.freqlist_mode > 0) { for(let lookup of results) { for(let entry of lookup.result) { let seen = new Set(); for(let r of entry.r_ele) seen.add(r.reb); if(entry.k_ele) for(let k of entry.k_ele) seen.add(k.keb); entry.freq = []; for(let r of entry.r_ele) { let reading = replace_kata_with_hira(r.reb); let freq = undefined; if(settings.freqlist_mode == 1) freq = freq_vns; else if(settings.freqlist_mode == 2) freq = freq_narou; else freq = freq_novels; if(freq[reading]) { for(let possibility of freq[reading]) { if(seen.has(possibility[0])) { if(reading == possibility[0] && entry.k_ele) continue; if(entry.freq.length == 0) entry.freq = [possibility[0], reading, possibility[1], possibility[2]]; else { if(entry.freq[2] > possibility[1]) // worse rank entry.freq = [possibility[0], reading, possibility[1], possibility[2]]; } } } } } } } } return ret; } // Skip JMdict entries reappearing in alternative lookups (so only the first one is shown) function skip_rereferenced_entries(results, other_settings) { let newresults = []; let seenseq = new Set(); for(let lookup of results) { let newlookup = []; for(let entry of lookup.result) { if(seenseq.has(entry.seq)) continue; seenseq.add(entry.seq); newlookup.push(restrict_by_text(entry, lookup.text)); } if(newlookup.length > 0) newresults.push({text:lookup.text, result:newlookup}); } return add_extra_info(newresults, other_settings); // add extra information like json results and audio data now } let last_lookup = ""; function lookup_indirect(text, time, divexisted, other_settings) { if(text == "") return; if(text == last_lookup && divexisted) return "itsthesame"; last_kanji_lookup = ""; last_lookup = text; // try to look up successively shorter substrings of text // deconjugate() returns possible deconjugations, one of which has zero deconjugations, i.e. the plain text // build_lookup_comb looks for dictionary definitions matching any deconjugation, returning a list of them // FIXME: later lookups using definitions already caught if(other_settings.alternatives_mode == 0 || other_settings.alternatives_mode == 1 || other_settings.alternatives_mode == 2) { let forms = deconjugate(text); let result = build_lookup_comb(forms); while(result === undefined && text.length > 0) { text = text.substring(0, text.length-1); forms = deconjugate(text); result = build_lookup_comb(forms); } if(result !== undefined && result.length > 0) { result = sort_results(text, result); if(other_settings.alternatives_mode == 0 || text.length <= 1) return skip_rereferenced_entries([{text:text, result:result}], other_settings); else if(other_settings.alternatives_mode == 1) // second longest too { let len = text.length-1; let short_text = text.substring(0, len); let short_forms = deconjugate(short_text); let short_result = build_lookup_comb(short_forms); while(short_result === undefined && len > 0) { len--; short_text = text.substring(0, len); short_forms = deconjugate(short_text); short_result = build_lookup_comb(short_forms); } if(other_settings.strict_alternatives && is_kana(short_text)) short_result = filter_kana_ish_results(short_result); if(short_result !== undefined && short_result.length > 0) { short_result = sort_results(short_text, short_result); return skip_rereferenced_entries([{text:text, result:result}, {text:short_text, result:short_result}], other_settings); } else return skip_rereferenced_entries([{text:text, result:result}], other_settings); } else if(other_settings.alternatives_mode == 2) // shortest too { let len = 1; let short_text = text.substring(0, len); let short_forms = deconjugate(short_text); let short_result = build_lookup_comb(short_forms); while(short_result === undefined && short_text.length+1 < text.length) { len++; short_text = text.substring(0, len); short_forms = deconjugate(short_text); short_result = build_lookup_comb(short_forms); } if(other_settings.strict_alternatives && is_kana(short_text)) short_result = filter_kana_ish_results(short_result); if(short_result !== undefined && short_result.length > 0) { short_result = sort_results(short_text, short_result); return skip_rereferenced_entries([{text:text, result:result}, {text:short_text, result:short_result}], other_settings); } else return skip_rereferenced_entries([{text:text, result:result}], other_settings); } } } else if(other_settings.alternatives_mode == 3) { let results = []; let first = true; while(text.length > 0) { let forms = deconjugate(text); let result = build_lookup_comb(forms); if(!first && other_settings.strict_alternatives && is_kana(text)) { result = filter_kana_ish_results(result); } if(result !== undefined && result.length > 0) { try { result = sort_results(text, result); } catch(e) { console.log("hit error sorting lookup results:"); console.log(e); console.log(result); } results.push({text:text, result:result}); } text = text.substring(0, text.length-1); if(results.length > 0) first = false; } if(results.length > 0) return skip_rereferenced_entries(results, other_settings); } } function lookup_indirect_for_tests(text) { let old_last_lookup = last_lookup; last_lookup = ""; let ret = lookup_indirect( text, Date.now(), false, { alternatives_mode: 3, strict_alternatives: true, strict_epwing: true } ); last_lookup = old_last_lookup; if (!ret || !Array.isArray(ret) || ret.length === 0 || !ret[0]) { console.warn("Nazeka test lookup returned no result for:", text, ret); return null; } return ret[0].result; } let last_kanji_lookup = ""; function lookup_kanji_character(c, divexisted) { if(c == "") return; if(c == last_kanji_lookup && divexisted) return "itsthesame"; last_kanji_lookup = c; last_lookup = ""; try { return copy(kanji_data.get(c)); } catch (err) { return undefined; } } async function update_icon(enabled) { if(enabled) { browser.browserAction.setTitle({title:"Nazeka (enabled)"}); try { browser.browserAction.setIcon({path:{ "16": "img/enabled16.png", "32": "img/enabled32.png", "512": "img/enabled512.png" }},); } catch (err) {} // don't care } else { browser.browserAction.setTitle({title:"Nazeka (disabled)"}); try { browser.browserAction.setIcon({path:{ "16": "img/action16.png", "32": "img/action32.png", "512": "img/action512.png" }},); } catch (err) {} // don't care } } async function toggle_enabled() { let enabled = (await browser.storage.local.get("enabled")).enabled; if(enabled == undefined) enabled = false; enabled = !enabled; browser.storage.local.set({enabled: enabled}); update_icon(enabled); } async function fixicon() { let enabled = (await browser.storage.local.get("enabled")).enabled; if(enabled == undefined) enabled = false; update_icon(enabled); } async function init_icon() { browser.browserAction.onClicked.addListener(toggle_enabled); fixicon(); } init_icon(); function open_options(info, tab) { try { browser.tabs.create({ url:browser.extension.getURL("options.html") }); } catch(err) {} // don't care } function open_reader(info, tab) { try { browser.windows.create({ url:browser.extension.getURL("reader.html"), type:"popup", width:settings.reader_width, height:settings.reader_height }); } catch(err) {} // don't care } function open_mining(info, tab) { try { browser.windows.create({ url:browser.extension.getURL("mining.html"), type:"popup" }); } catch(err) {} // don't care } function open_livemining(info, tab) { try { browser.windows.create({ url:browser.extension.getURL("livemining.html"), type:"popup" }); } catch(err) {} // don't care } function open_jsonconfig(info, tab) { try { browser.windows.create({ url:browser.extension.getURL("json_config.html"), type:"popup" }); } catch(err) {} // don't care } if(browser.contextMenus) { browser.contextMenus.create({ id: "nazeka-options", title: "Options", contexts: ["browser_action"], onclick: open_options }); browser.contextMenus.create({ id: "nazeka-reader", title: "Open Reader", contexts: ["browser_action"], onclick: open_reader }); browser.contextMenus.create({ id: "nazeka-mining", title: "View Mined Cards", contexts: ["browser_action"], onclick: open_mining }); browser.contextMenus.create({ id: "nazeka-livemining", title: "Configure Live Mining", contexts: ["browser_action"], onclick: open_livemining }); browser.contextMenus.create({ id: "nazeka-jsonconfig", title: "Manage JSON Dictionaries", contexts: ["browser_action"], onclick: open_jsonconfig }); } let origin_tab = undefined; function set_up_paste_overload() { document.querySelector("#nazeka-paste-target").addEventListener("paste", (event) => { // skip event if there's no plaintext; thanks github.com/kmltml/clipboard-inserter let text = event.clipboardData.getData("text/plain"); event.preventDefault(); if(origin_tab != undefined) browser.tabs.sendMessage(origin_tab.id, {type:"text",text:text}); }); } if (document.readyState == "complete") set_up_paste_overload(); else document.addEventListener("DOMContentLoaded", set_up_paste_overload); function clipboard_hook(tab) { origin_tab = tab; let target = document.querySelector("#nazeka-paste-target"); target.textContent = ""; target.focus(); document.execCommand("paste"); } let platform = ""; async function get_real_platform() { let platformInfo = await browser.runtime.getPlatformInfo(); if(platformInfo) platform = platformInfo.os; console.log(`platform is ${platform}`); } get_real_platform(); function send_error(tab, error) { try { browser.tabs.sendMessage(tab, {type:"error",error:error}); } catch (err) { console.log("failed to send error to origin tab"); console.log(err); } } browser.runtime.onMessage.addListener((req, sender) => { console.log("Nazeka background got message:", req); if (req.type == "search") { let asdf = lookup_indirect(req.text, req.time, req.divexisted, req.settings); return Promise.resolve({"response" : asdf}); } if (req.type == "search_kanji") { let asdf = lookup_kanji_character(req.text, req.divexisted); return Promise.resolve({"response" : asdf}); } else if (req.type == "platform") { return Promise.resolve({"response" : platform}); } else if (req.type == "fixicon") { fixicon(); } else if (req.type == "gimmetext") { clipboard_hook(sender.tab); } else if (req.type == "refreshjson") { refresh_json(); } else if (req.type == "reader_lookup") { browser.tabs.sendMessage(req.id, {type:req.type, x:req.x, y:req.y, text:req.text}); } else if (req.type == "reader_mode") { browser.tabs.sendMessage(req.id, {type:req.type}); } else if (req.type == "ankiconnect_mine") { console.log("handling ankiconnect mining request"); let xhr = new XMLHttpRequest(); xhr.open("POST", req.host, true); xhr.overrideMimeType('application/json'); //xhr.setRequestHeader('Content-type','application/json; charset=utf-8'); xhr.addEventListener('load', () => { let response = JSON.parse(xhr.responseText); try { if(response.error) send_error(sender.tab.id, "AnkiConnect mining failed: " + response.error); else { if(settings.ankiconnect_force_sync) { let newxhr = new XMLHttpRequest(); newxhr.open("POST", req.host, true); newxhr.send('{"action": "sync","version": 6}'); } } } catch (e) { console.log("error while interacting with ankiconnect:"); console.log(e); } }); xhr.addEventListener('error', (e) => { send_error(sender.tab.id, "AnkiConnect mining failed: unspecified error (Anki is probably not open)"); console.log(e); }); xhr.addEventListener('timeout', () => { send_error(sender.tab.id, "AnkiConnect mining failed: timed out"); }); xhr.send(req.command); } else if (req.type == "play_audio") { let audio = new Audio(req.host); audio.volume = req.volume; audio.play(); console.log("tried to play audio"); console.log(req.host); } else if (req.type == "get_audio_base64") { let f = async () => { let blob = await (await fetch(req.url)).blob(); let reader = new FileReader(); let result_base64 = await new Promise((resolve) => { let fileReader = new FileReader(); fileReader.onload = (e) => resolve(fileReader.result); fileReader.readAsDataURL(blob); }); return Promise.resolve({"response" : result_base64}); }; return f(); } return Promise.resolve(undefined); }); browser.contextMenus.create({ id: "nazeka-toggle", title: "Toggle Nazeka", contexts: ["page", "selection"], documentUrlPatterns: ["chrome-extension://*/reader.html"], onclick: toggle_enabled }); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function assert_id_location(text, id, loc_exp = 0) { let result = lookup_indirect_for_tests(text); console.log(result); let i = 0; let forceout = false; for(let entry of result) { if(entry.seq == id) forceout = true; if(forceout) break; i++; } try { console.assert(loc_exp(i), result, id, loc_exp); } catch(e) { console.assert(i == loc_exp, result, id, loc_exp); } return result.length; } function assert_reading_location(text, reading, loc_exp = 0) { let result = lookup_indirect_for_tests(text); let i = 0; let forceout = false; for(let entry of result) { for(let r_ele of entry.r_ele) { if(r_ele.reb == reading) forceout = true; if(forceout) break; } if(forceout) break; i++; } try { console.assert(loc_exp(i), result, reading, loc_exp); } catch(e) { console.assert(i == loc_exp, result, reading, loc_exp); } return result.length; } function assert_spelling_location(text, spelling, loc_exp = 0) { let result = lookup_indirect_for_tests(text); let i = 0; let forceout = false; for(let entry of result) { if(!entry.k_ele) continue; for(let k_ele of entry.k_ele) { if(k_ele.keb == spelling) forceout = true; if(forceout) break; } if(forceout) break; i++; } try { console.assert(loc_exp(i), result, spelling, loc_exp); } catch(e) { console.assert(i == loc_exp, result, spelling, loc_exp); } return result.length; } async function regression_tests() { await sleep(4 * 1000); console.log("enter regression tests"); assert_id_location("は", 2028920); assert_id_location("みたい", 2016410); assert_id_location("へ", 2029000); assert_id_location("で", 2028980); assert_id_location("し", 2086640); assert_reading_location("主", "あるじ", (a) => a < 6); assert_reading_location("家", "け", (a) => a > 3); assert_reading_location("前", "ぜん", 1); assert_reading_location("前", "まえ"); assert_reading_location("先", "さき"); assert_reading_location("浮かべる", "うかべる"); assert_reading_location("願い", "ねがい"); assert_reading_location("始めまして", "はじめまして"); assert_reading_location("初めまして", "はじめまして"); assert_reading_location("一分", "いちぶん", 2); assert_reading_location("持たせて", "もたせる"); assert_reading_location("会", "かい"); assert_reading_location("額", "ひたい"); assert_spelling_location("そうそう", "然う然う"); assert_spelling_location("かたくな", "頑な"); assert_spelling_location("にとって", "に取って"); assert_spelling_location("こと", "琴", (a) => a > 3); assert_spelling_location("はじめまして", "初めまして"); assert_spelling_location("初めまして", "そめる", (a) => a > 0); assert_spelling_location("それ", "逸れる", (a) => a > 1); console.log("exit regression tests"); } //regression_tests();