diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..953cc9b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dict/JMDict*.json diff --git a/background-script.js b/background-script.js index b59727f..90d7f76 100644 --- a/background-script.js +++ b/background-script.js @@ -2,6 +2,15 @@ 'use strict'; +if (navigator.userAgent.indexOf('Chrome') > -1) +{ + var ischrome = true; +} +else +{ + var ischrome = false; +} + // updated by a timer looping function, based on local storage set by the options page // we only use a tiny number of settings here @@ -55,16 +64,19 @@ let lookup_epwing_kana = new Map(); async function refresh_epwing() { let myjson = (await browser.storage.local.get("epwing"))["epwing"]; - epwing = JSON.parse(myjson); - lookup_epwing_kan = new Map(); - lookup_epwing_kana = new Map(); - for (let i = 1; i < epwing.length; i++) + if(myjson !== undefined) { - if(typeof epwing[i] === 'string' || epwing[i] instanceof String) - continue; - add_epwing_id(epwing[i]["r"], i); - for(let spelling of epwing[i]["s"]) - add_epwing_id(spelling, i); + epwing = JSON.parse(myjson); + lookup_epwing_kan = new Map(); + lookup_epwing_kana = new Map(); + for (let i = 1; i < epwing.length; i++) + { + if(typeof epwing[i] === 'string' || epwing[i] instanceof String) + continue; + add_epwing_id(epwing[i]["r"], i); + for(let spelling of epwing[i]["s"]) + add_epwing_id(spelling, i); + } } } refresh_epwing(); @@ -359,6 +371,7 @@ function load_deconjugation_rules(filename) { if (req.readyState === 4) { + console.log("loaded deconjugation rules"); if (req.status === 200) rules = JSON.parse(req.responseText); else @@ -532,12 +545,11 @@ tetrap: tetrap_check, saspecial: saspecial_check, }; -Set.prototype.union = function(setB) +function union(setA, setB) { - let union = new Set(this); for (let elem of setB) - union.add(elem); - return union; + setA.add(elem); + return setA; } // Returns a set of objects, each containing the "final form" of a series of deconjugations @@ -585,7 +597,7 @@ function deconjugate(mytext) new_novel.add(newform); } } - processed = processed.union(novel); + processed = union(processed, novel); novel = new_novel; } @@ -596,6 +608,7 @@ function deconjugate(mytext) // building a list of all matches function build_lookup_comb(forms) { + console.log("in build_lookup_comb"); // 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. @@ -644,18 +657,16 @@ function build_lookup_comb(forms) { if(entry.allpos.has(form.tags[form.tags.length-1])) { - if(entry.deconj) - entry.deconj.add(form); - else - entry.deconj = new Set([form]); + if(entry.deconj === undefined) + entry.deconj = new Set(); + entry.deconj.add(form); } } else { - if(entry.deconj) - entry.deconj.add(form); - else - entry.deconj = new Set([form]); + if(entry.deconj === undefined) + entry.deconj = new Set(); + entry.deconj.add(form); } } } @@ -670,6 +681,7 @@ function build_lookup_comb(forms) { if(entry.deconj) { + entry.deconj = Array.from(entry.deconj); merger.push(entry); } } @@ -683,6 +695,7 @@ function build_lookup_comb(forms) function add_epwing_info(lookups) { + console.log("in add_epwing_info"); for(let lookup of lookups) { for(let entry of lookup.result) @@ -835,6 +848,7 @@ function add_epwing_info(lookups) entry.epwing["z"] = epwing[0]; } } + console.log("about to exit add_epwing_info"); return lookups; } @@ -1010,15 +1024,17 @@ function filter_kana_ish_results(results) return newresults; } -// https://stackoverflow.com/questions/4459928/how-to-deep-clone-in-javascript function copy(orig) { - let mine = orig; - - mine = Object.assign({}, orig) + let mine; + if (Set.prototype.isPrototypeOf(orig)) + mine = new Set(orig); + else + mine = Object.assign({}, orig); for(let f in mine) { - f = Object.assign({}, f) + if(f !== orig) + f = copy(f); } return mine; } @@ -1217,11 +1233,9 @@ function skip_rereferenced_entries(results) let newresults = []; let seenseq = new Set(); - //console.log(results); for(let lookup of results) { let newlookup = []; - //console.log(lookup); for(let entry of lookup.result) { if(seenseq.has(entry.seq)) @@ -1251,7 +1265,6 @@ function lookup_indirect(text, time, divexisted, alternatives_mode, strict_alter // 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 - //console.log("trying to look up " + text); // FIXME: later lookups using definitions already caught if(alternatives_mode == 0 || alternatives_mode == 1 || alternatives_mode == 2) { @@ -1260,14 +1273,11 @@ function lookup_indirect(text, time, divexisted, alternatives_mode, strict_alter while(result === undefined && text.length > 0) { text = text.substring(0, text.length-1); - //console.log("trying to look up " + text); forms = deconjugate(text); result = build_lookup_comb(forms); } if(result !== undefined && result.length > 0) { - //console.log("found lookup"); - //console.log(text) result = sort_results(text, result); if(alternatives_mode == 0 || text.length <= 1) return skip_rereferenced_entries([{text:text, result:result}]); @@ -1473,24 +1483,34 @@ function clipboard_hook(tab) let platform = ""; async function get_real_platform() { - let platformInfo = await browser.runtime.getPlatformInfo(); - if(platformInfo) - platform = platformInfo.os; - console.log(`platform is ${platform}`); + if (!ischrome) + { + let platformInfo = await browser.runtime.getPlatformInfo(); + if(platformInfo) + platform = platformInfo.os; + console.log(`platform is ${platform}`); + } + else + { + platform = "win"; + console.log(`on chrome, didn't check platform; assuming desktop`); + } } get_real_platform(); //console.log("setting message listener"); -browser.runtime.onMessage.addListener((req, sender, sendResponse) => +browser.runtime.onMessage.addListener((req, sender) => { - //console.log("received message to background script"); if (req.type == "search") { - sendResponse(lookup_indirect(req.text, req.time, req.divexisted, req.alternatives_mode, req.strict_alternatives)); + let asdf = lookup_indirect(req.text, req.time, req.divexisted, req.alternatives_mode, req.strict_alternatives); + console.log("got out of lookup_indirect"); + console.log(asdf); + return Promise.resolve({"response" : asdf}); } else if (req.type == "platform") { - sendResponse(platform); + return Promise.resolve({"response" : platform}); } else if (req.type == "fixicon") { @@ -1518,17 +1538,27 @@ browser.runtime.onMessage.addListener((req, sender, sendResponse) => }); xhr.send(req.command); } - else - { - return; - } + return Promise.resolve(undefined); }); //console.log("set message listener"); -browser.contextMenus.create({ - id: "nazeka-toggle", - title: "Toggle Nazeka", - contexts: ["page", "selection"], - documentUrlPatterns: ["moz-extension://*/reader.html"], - onclick: toggle_enabled -}); +if (!ischrome) +{ + browser.contextMenus.create({ + id: "nazeka-toggle", + title: "Toggle Nazeka", + contexts: ["page", "selection"], + documentUrlPatterns: ["moz-extension://*/reader.html"], + onclick: toggle_enabled + }); +} +else +{ + browser.contextMenus.create({ + id: "nazeka-toggle", + title: "Toggle Nazeka", + contexts: ["page", "selection"], + documentUrlPatterns: ["chrome-extension://*/reader.html"], + onclick: toggle_enabled + }); +} diff --git a/background.html b/background.html index e88b0b8..6081ac4 100644 --- a/background.html +++ b/background.html @@ -1,6 +1,7 @@ + diff --git a/dict/deconjugator.json b/dict/deconjugator.json index 0c96425..b55393f 100644 --- a/dict/deconjugator.json +++ b/dict/deconjugator.json @@ -1016,5 +1016,5 @@ "con_tag": "uninflectable", "detail": "slurred" }, - "// TODO: a bunch of missing contractions, and suppoer for other verb types like vk, vs_i, v5aru, v4r, ..." + "// TODO: a bunch of missing contractions, and support for other verb types like vk (くる), vs_i (true する verbs), v5aru (なさる, くださる, etc), v4r (ござる, etc), ..." ] diff --git a/etc/chrome/manifest.json b/etc/chrome/manifest.json new file mode 100644 index 0000000..6a1ae13 --- /dev/null +++ b/etc/chrome/manifest.json @@ -0,0 +1,76 @@ +{ + "manifest_version": 2, + "name": "nazeka", + "version": "0.2.4.0", + "author": "Wareya", + "homepage_url": "https://github.com/wareya/nazeka", + "description": "A rikai replacement.", + + "permissions": [ + "contextMenus", + "storage", + "tabs", + "clipboardRead", + "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?*" + ], + + "background": { + "page": "background.html" + }, + + "web_accessible_resources": [ + "include/browser-polyfill.min.js", + "img/closebutton24.png", + "img/leftarrow24.png", + "img/rightarrow24.png", + "dict/JMdict1.json", + "dict/JMdict2.json", + "dict/JMdict3.json", + "dict/JMdict4.json", + "dict/JMdict5.json", + "dict/JMdict6.json", + "dict/JMdict7.json", + "dict/JMdict8.json", + "dict/JMdict9.json", + "dict/JMdict10.json", + "dict/JMdict11.json", + "dict/jdic audio.txt", + "dict/deconjugator.json", + "reader.js", + "reader.html", + "mining.js", + "mining.html", + "livemining.js", + "livemining.html", + "jmdictabbreviations.html" + ], + + "content_scripts": [ + { + "matches": ["http://*/*", "https://*/*", "ftp://*/*", "file:///*"], + "js": ["include/browser-polyfill.min.js", "texthook.js"], + "all_frames": true + } + ], + + "options_ui": { + "page": "options.html" + }, + + "browser_action": { + "browser_style": false, + "default_icon": { + "16": "img/action16.png", + "32": "img/action32.png", + "512": "img/action512.png" + }, + "default_title": "Nazeka (disabled)", + "default_area": "navbar" + }, + + "icons": { + "16": "img/enabled16.png", + "32": "img/enabled32.png", + "512": "img/enabled512.png" + } +} diff --git a/manifest.json b/etc/firefox/manifest.json similarity index 94% rename from manifest.json rename to etc/firefox/manifest.json index 4ae3db5..984fe1e 100644 --- a/manifest.json +++ b/etc/firefox/manifest.json @@ -26,6 +26,7 @@ }, "web_accessible_resources": [ + "include/browser-polyfill.min.js", "img/closebutton24.png", "img/leftarrow24.png", "img/rightarrow24.png", @@ -54,7 +55,7 @@ "content_scripts": [ { "matches": ["http://*/*", "https://*/*", "ftp://*/*", "file:///*"], - "js": ["texthook.js"], + "js": ["include/browser-polyfill.min.js", "texthook.js"], "all_frames": true } ], diff --git a/etc/popupbase.css b/etc/popupbase.css deleted file mode 100644 index 74fc4e5..0000000 --- a/etc/popupbase.css +++ /dev/null @@ -1,3 +0,0 @@ -.nazeka_main_keb{font-family: IPAGothic,TakaoGothic,Noto Sans CJK JP Regular,Meiryo,sans-serif;font-size:18px;color:#9DF} -.nazeka_main_reb{font-family: IPAGothic,TakaoGothic,Noto Sans CJK JP Regular,Meiryo,sans-serif;font-size:18px;color:#9DF} -.nazeka_original{font-style: oblique; float: right; margin-right: 2px; margin-left:2px;} diff --git a/etc/process.py b/etc/process.py new file mode 100644 index 0000000..feb308d --- /dev/null +++ b/etc/process.py @@ -0,0 +1,161 @@ +#!python +from lxml import etree +from collections import OrderedDict +import json +import sys +from io import open + +entries = [] + +XML_LANG = '{http://www.w3.org/XML/1998/namespace}lang' + +# whether to only output english definitions - if so, glosses are just the gloss text, not a dictionary +eng_only = True + +parser = etree.XMLParser(resolve_entities=False) +root = etree.parse("JMdict", parser); +for ent in root.iter(etree.Entity): + if ent.getparent() is not None: + if not ent.getparent().text: + ent.getparent().text = "" + if ent.text: + ent.getparent().text += ent.text + if ent.tail: + ent.getparent().text += ent.tail +root = root.getroot(); +for entry in root.iter("entry"): + myentry = OrderedDict() + seq = int(entry.find("ent_seq").text) + k_eles = [] + for k_ele in entry.iter("k_ele"): + keb = k_ele.find("keb").text + ke_infs = [] + for ke_inf in k_ele.iter("ke_inf"): + ke_infs += [ke_inf.text] + ke_pris = [] + for ke_pri in k_ele.iter("ke_pri"): + ke_pris += [ke_pri.text] + element = OrderedDict() + element["keb"] = keb + if(len(ke_infs) > 0): element["inf"] = ke_infs + if(len(ke_pris) > 0): element["pri"] = ke_pris + k_eles += [element] + r_eles = [] + for r_ele in entry.iter("r_ele"): + reb = r_ele.find("reb").text + re_nokanji = None + if r_ele.find("re_nokanji") is not None: + re_nokanji = r_ele.find("re_nokanji").text + re_restrs = [] + for re_restr in r_ele.iter("re_restr"): + re_restrs += [re_restr.text] + re_infs = [] + for re_inf in r_ele.iter("re_inf"): + re_infs += [re_inf.text] + re_pris = [] + for re_pri in r_ele.iter("re_pri"): + re_pris += [re_pri.text] + element = OrderedDict() + element["reb"] = reb + if(re_nokanji != None): element["nokanji"] = re_nokanji + if(len(re_restrs) > 0): element["restr"] = re_restrs + if(len(re_infs) > 0): element["inf"] = re_infs + if(len(re_pris) > 0): element["pri"] = re_pris + r_eles += [element] + senses = [] + for sense in entry.iter("sense"): + stagks = [] + for stagk in sense.iter("stagk"): + stagks += [stagk.text] + stagrs = [] + for stagr in sense.iter("stagr"): + stagrs += [stagr.text] + poss = [] + for pos in sense.iter("pos"): + #poss += [pos.getchildren()[0].text] + poss += [pos.text] + xrefs = [] + for xref in sense.iter("xref"): + xrefs += [xref.text] + ants = [] + for ant in sense.iter("ant"): + ants += [ant.text] + fields = [] + for field in sense.iter("field"): + fields += [field.text] + miscs = [] + for misc in sense.iter("misc"): + miscs += [misc.text] + s_infs = [] + for s_inf in sense.iter("s_inf"): + s_infs += [s_inf.text] + lsources = [] + for lsource in sense.iter("lsource"): + source = OrderedDict() + source["text"] = lsource.text + source["lang"] = "eng" + if lsource.get(XML_LANG): source["lang"] = lsource.get(XML_LANG) + source["type"] = "full" + if lsource.get("ls_type"): source["type"] = lsource.get("ls_type") + source["wasei"] = "n" + if lsource.get("ls_wasei"): source["wasei"] = lsource.get("ls_wasei") + lsources += [source] + dials = [] + for dial in sense.iter("dial"): + dials += [dial.text] + glosss = [] + for gloss in sense.iter("gloss"): + definition = OrderedDict() + definition["text"] = gloss.text + definition["lang"] = "eng" + if gloss.get(XML_LANG): definition["lang"] = gloss.get(XML_LANG) + if eng_only and definition["lang"] != "eng": continue + if eng_only: + glosss += [gloss.text] + else: + glosss += [definition] + element = OrderedDict() + if(len(stagks) > 0): element["stagk"] = stagks + if(len(stagrs) > 0): element["stagr"] = stagrs + if(len(poss) > 0): element["pos"] = poss + if(len(xrefs) > 0): element["xref"] = xrefs + if(len(ants) > 0): element["ant"] = ants + if(len(fields) > 0): element["field"] = fields + if(len(miscs) > 0): element["misc"] = miscs + if(len(s_infs) > 0): element["inf"] = s_infs + if(len(lsources) > 0): element["lsource"] = lsources + if(len(dials) > 0): element["dial"] = dials + if(len(glosss) > 0): element["gloss"] = glosss + + if(len(element) > 0): senses += [element] + + myentry["seq"] = seq + if(len(k_eles) > 0): myentry["k_ele"] = k_eles + myentry["r_ele"] = r_eles + myentry["sense"] = senses + entries += [myentry] + +#print(entries) + +#print(json.dumps(entries, ensure_ascii=False)) +#sys.stdout.buffer.write(json.dumps(entries, ensure_ascii=False, separators=(',',':')).encode("utf-8")) +i = 0 +under = 1 +copylist = [] +basename = "JMdict" +extension = ".json" +limit = 18000 +for entry in entries: + i += 1 + copylist += [entry] + if i >= limit: + f = open(basename+str(under)+extension, "w", newline="\n", encoding="utf-8") + f.write(json.dumps(copylist, ensure_ascii=False, separators=(',',':'))); + copylist = [] + i = 0 + under += 1 + +f = open(basename+str(under)+extension, "w", newline="\n", encoding="utf-8") +f.write(json.dumps(copylist, ensure_ascii=False, separators=(',',':'))); + + diff --git a/include/browser-polyfill.min.js b/include/browser-polyfill.min.js new file mode 100644 index 0000000..b3dd82d --- /dev/null +++ b/include/browser-polyfill.min.js @@ -0,0 +1,1186 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define("webextension-polyfill", ["module"], factory); + } else if (typeof exports !== "undefined") { + factory(module); + } else { + var mod = { + exports: {} + }; + factory(mod); + global.browser = mod.exports; + } +})(this, function (module) { + /* webextension-polyfill - v0.3.1 - Tue Aug 21 2018 10:09:34 */ + /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ + /* vim: set sts=2 sw=2 et tw=80: */ + /* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + "use strict"; + + if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.prototype) { + const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received."; + const SEND_RESPONSE_DEPRECATION_WARNING = "Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)"; + + // Wrapping the bulk of this polyfill in a one-time-use function is a minor + // optimization for Firefox. Since Spidermonkey does not fully parse the + // contents of a function until the first time it's called, and since it will + // never actually need to be called, this allows the polyfill to be included + // in Firefox nearly for free. + const wrapAPIs = () => { + // NOTE: apiMetadata is associated to the content of the api-metadata.json file + // at build time by replacing the following "include" with the content of the + // JSON file. + const apiMetadata = { + "alarms": { + "clear": { + "minArgs": 0, + "maxArgs": 1 + }, + "clearAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "get": { + "minArgs": 0, + "maxArgs": 1 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "bookmarks": { + "create": { + "minArgs": 1, + "maxArgs": 1 + }, + "get": { + "minArgs": 1, + "maxArgs": 1 + }, + "getChildren": { + "minArgs": 1, + "maxArgs": 1 + }, + "getRecent": { + "minArgs": 1, + "maxArgs": 1 + }, + "getSubTree": { + "minArgs": 1, + "maxArgs": 1 + }, + "getTree": { + "minArgs": 0, + "maxArgs": 0 + }, + "move": { + "minArgs": 2, + "maxArgs": 2 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeTree": { + "minArgs": 1, + "maxArgs": 1 + }, + "search": { + "minArgs": 1, + "maxArgs": 1 + }, + "update": { + "minArgs": 2, + "maxArgs": 2 + } + }, + "browserAction": { + "disable": { + "minArgs": 0, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "enable": { + "minArgs": 0, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "getBadgeBackgroundColor": { + "minArgs": 1, + "maxArgs": 1 + }, + "getBadgeText": { + "minArgs": 1, + "maxArgs": 1 + }, + "getPopup": { + "minArgs": 1, + "maxArgs": 1 + }, + "getTitle": { + "minArgs": 1, + "maxArgs": 1 + }, + "openPopup": { + "minArgs": 0, + "maxArgs": 0 + }, + "setBadgeBackgroundColor": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "setBadgeText": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "setIcon": { + "minArgs": 1, + "maxArgs": 1 + }, + "setPopup": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "setTitle": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + } + }, + "browsingData": { + "remove": { + "minArgs": 2, + "maxArgs": 2 + }, + "removeCache": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeCookies": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeDownloads": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeFormData": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeHistory": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeLocalStorage": { + "minArgs": 1, + "maxArgs": 1 + }, + "removePasswords": { + "minArgs": 1, + "maxArgs": 1 + }, + "removePluginData": { + "minArgs": 1, + "maxArgs": 1 + }, + "settings": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "commands": { + "getAll": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "contextMenus": { + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "update": { + "minArgs": 2, + "maxArgs": 2 + } + }, + "cookies": { + "get": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAll": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAllCookieStores": { + "minArgs": 0, + "maxArgs": 0 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "set": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "devtools": { + "inspectedWindow": { + "eval": { + "minArgs": 1, + "maxArgs": 2 + } + }, + "panels": { + "create": { + "minArgs": 3, + "maxArgs": 3, + "singleCallbackArg": true + } + } + }, + "downloads": { + "cancel": { + "minArgs": 1, + "maxArgs": 1 + }, + "download": { + "minArgs": 1, + "maxArgs": 1 + }, + "erase": { + "minArgs": 1, + "maxArgs": 1 + }, + "getFileIcon": { + "minArgs": 1, + "maxArgs": 2 + }, + "open": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "pause": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeFile": { + "minArgs": 1, + "maxArgs": 1 + }, + "resume": { + "minArgs": 1, + "maxArgs": 1 + }, + "search": { + "minArgs": 1, + "maxArgs": 1 + }, + "show": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + } + }, + "extension": { + "isAllowedFileSchemeAccess": { + "minArgs": 0, + "maxArgs": 0 + }, + "isAllowedIncognitoAccess": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "history": { + "addUrl": { + "minArgs": 1, + "maxArgs": 1 + }, + "deleteAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "deleteRange": { + "minArgs": 1, + "maxArgs": 1 + }, + "deleteUrl": { + "minArgs": 1, + "maxArgs": 1 + }, + "getVisits": { + "minArgs": 1, + "maxArgs": 1 + }, + "search": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "i18n": { + "detectLanguage": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAcceptLanguages": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "identity": { + "launchWebAuthFlow": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "idle": { + "queryState": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "management": { + "get": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "getSelf": { + "minArgs": 0, + "maxArgs": 0 + }, + "setEnabled": { + "minArgs": 2, + "maxArgs": 2 + }, + "uninstallSelf": { + "minArgs": 0, + "maxArgs": 1 + } + }, + "notifications": { + "clear": { + "minArgs": 1, + "maxArgs": 1 + }, + "create": { + "minArgs": 1, + "maxArgs": 2 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "getPermissionLevel": { + "minArgs": 0, + "maxArgs": 0 + }, + "update": { + "minArgs": 2, + "maxArgs": 2 + } + }, + "pageAction": { + "getPopup": { + "minArgs": 1, + "maxArgs": 1 + }, + "getTitle": { + "minArgs": 1, + "maxArgs": 1 + }, + "hide": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "setIcon": { + "minArgs": 1, + "maxArgs": 1 + }, + "setPopup": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "setTitle": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + }, + "show": { + "minArgs": 1, + "maxArgs": 1, + "fallbackToNoCallback": true + } + }, + "permissions": { + "contains": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "request": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "runtime": { + "getBackgroundPage": { + "minArgs": 0, + "maxArgs": 0 + }, + "getBrowserInfo": { + "minArgs": 0, + "maxArgs": 0 + }, + "getPlatformInfo": { + "minArgs": 0, + "maxArgs": 0 + }, + "openOptionsPage": { + "minArgs": 0, + "maxArgs": 0 + }, + "requestUpdateCheck": { + "minArgs": 0, + "maxArgs": 0 + }, + "sendMessage": { + "minArgs": 1, + "maxArgs": 3 + }, + "sendNativeMessage": { + "minArgs": 2, + "maxArgs": 2 + }, + "setUninstallURL": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "sessions": { + "getDevices": { + "minArgs": 0, + "maxArgs": 1 + }, + "getRecentlyClosed": { + "minArgs": 0, + "maxArgs": 1 + }, + "restore": { + "minArgs": 0, + "maxArgs": 1 + } + }, + "storage": { + "local": { + "clear": { + "minArgs": 0, + "maxArgs": 0 + }, + "get": { + "minArgs": 0, + "maxArgs": 1 + }, + "getBytesInUse": { + "minArgs": 0, + "maxArgs": 1 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "set": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "managed": { + "get": { + "minArgs": 0, + "maxArgs": 1 + }, + "getBytesInUse": { + "minArgs": 0, + "maxArgs": 1 + } + }, + "sync": { + "clear": { + "minArgs": 0, + "maxArgs": 0 + }, + "get": { + "minArgs": 0, + "maxArgs": 1 + }, + "getBytesInUse": { + "minArgs": 0, + "maxArgs": 1 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "set": { + "minArgs": 1, + "maxArgs": 1 + } + } + }, + "tabs": { + "captureVisibleTab": { + "minArgs": 0, + "maxArgs": 2 + }, + "create": { + "minArgs": 1, + "maxArgs": 1 + }, + "detectLanguage": { + "minArgs": 0, + "maxArgs": 1 + }, + "discard": { + "minArgs": 0, + "maxArgs": 1 + }, + "duplicate": { + "minArgs": 1, + "maxArgs": 1 + }, + "executeScript": { + "minArgs": 1, + "maxArgs": 2 + }, + "get": { + "minArgs": 1, + "maxArgs": 1 + }, + "getCurrent": { + "minArgs": 0, + "maxArgs": 0 + }, + "getZoom": { + "minArgs": 0, + "maxArgs": 1 + }, + "getZoomSettings": { + "minArgs": 0, + "maxArgs": 1 + }, + "highlight": { + "minArgs": 1, + "maxArgs": 1 + }, + "insertCSS": { + "minArgs": 1, + "maxArgs": 2 + }, + "move": { + "minArgs": 2, + "maxArgs": 2 + }, + "query": { + "minArgs": 1, + "maxArgs": 1 + }, + "reload": { + "minArgs": 0, + "maxArgs": 2 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeCSS": { + "minArgs": 1, + "maxArgs": 2 + }, + "sendMessage": { + "minArgs": 2, + "maxArgs": 3 + }, + "setZoom": { + "minArgs": 1, + "maxArgs": 2 + }, + "setZoomSettings": { + "minArgs": 1, + "maxArgs": 2 + }, + "update": { + "minArgs": 1, + "maxArgs": 2 + } + }, + "topSites": { + "get": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "webNavigation": { + "getAllFrames": { + "minArgs": 1, + "maxArgs": 1 + }, + "getFrame": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "webRequest": { + "handlerBehaviorChanged": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "windows": { + "create": { + "minArgs": 0, + "maxArgs": 1 + }, + "get": { + "minArgs": 1, + "maxArgs": 2 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 1 + }, + "getCurrent": { + "minArgs": 0, + "maxArgs": 1 + }, + "getLastFocused": { + "minArgs": 0, + "maxArgs": 1 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "update": { + "minArgs": 2, + "maxArgs": 2 + } + } + }; + + if (Object.keys(apiMetadata).length === 0) { + throw new Error("api-metadata.json has not been included in browser-polyfill"); + } + + /** + * A WeakMap subclass which creates and stores a value for any key which does + * not exist when accessed, but behaves exactly as an ordinary WeakMap + * otherwise. + * + * @param {function} createItem + * A function which will be called in order to create the value for any + * key which does not exist, the first time it is accessed. The + * function receives, as its only argument, the key being created. + */ + class DefaultWeakMap extends WeakMap { + constructor(createItem, items = undefined) { + super(items); + this.createItem = createItem; + } + + get(key) { + if (!this.has(key)) { + this.set(key, this.createItem(key)); + } + + return super.get(key); + } + } + + /** + * Returns true if the given object is an object with a `then` method, and can + * therefore be assumed to behave as a Promise. + * + * @param {*} value The value to test. + * @returns {boolean} True if the value is thenable. + */ + const isThenable = value => { + return value && typeof value === "object" && typeof value.then === "function"; + }; + + /** + * Creates and returns a function which, when called, will resolve or reject + * the given promise based on how it is called: + * + * - If, when called, `chrome.runtime.lastError` contains a non-null object, + * the promise is rejected with that value. + * - If the function is called with exactly one argument, the promise is + * resolved to that value. + * - Otherwise, the promise is resolved to an array containing all of the + * function's arguments. + * + * @param {object} promise + * An object containing the resolution and rejection functions of a + * promise. + * @param {function} promise.resolve + * The promise's resolution function. + * @param {function} promise.rejection + * The promise's rejection function. + * @param {object} metadata + * Metadata about the wrapped method which has created the callback. + * @param {integer} metadata.maxResolvedArgs + * The maximum number of arguments which may be passed to the + * callback created by the wrapped async function. + * + * @returns {function} + * The generated callback function. + */ + const makeCallback = (promise, metadata) => { + return (...callbackArgs) => { + if (chrome.runtime.lastError) { + promise.reject(chrome.runtime.lastError); + } else if (metadata.singleCallbackArg || callbackArgs.length <= 1) { + promise.resolve(callbackArgs[0]); + } else { + promise.resolve(callbackArgs); + } + }; + }; + + const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments"; + + /** + * Creates a wrapper function for a method with the given name and metadata. + * + * @param {string} name + * The name of the method which is being wrapped. + * @param {object} metadata + * Metadata about the method being wrapped. + * @param {integer} metadata.minArgs + * The minimum number of arguments which must be passed to the + * function. If called with fewer than this number of arguments, the + * wrapper will raise an exception. + * @param {integer} metadata.maxArgs + * The maximum number of arguments which may be passed to the + * function. If called with more than this number of arguments, the + * wrapper will raise an exception. + * @param {integer} metadata.maxResolvedArgs + * The maximum number of arguments which may be passed to the + * callback created by the wrapped async function. + * + * @returns {function(object, ...*)} + * The generated wrapper function. + */ + const wrapAsyncFunction = (name, metadata) => { + return function asyncFunctionWrapper(target, ...args) { + if (args.length < metadata.minArgs) { + throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); + } + + if (args.length > metadata.maxArgs) { + throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); + } + + return new Promise((resolve, reject) => { + if (metadata.fallbackToNoCallback) { + // This API method has currently no callback on Chrome, but it return a promise on Firefox, + // and so the polyfill will try to call it with a callback first, and it will fallback + // to not passing the callback if the first call fails. + try { + target[name](...args, makeCallback({ resolve, reject }, metadata)); + } catch (cbError) { + console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError); + + target[name](...args); + + // Update the API method metadata, so that the next API calls will not try to + // use the unsupported callback anymore. + metadata.fallbackToNoCallback = false; + metadata.noCallback = true; + + resolve(); + } + } else if (metadata.noCallback) { + target[name](...args); + resolve(); + } else { + target[name](...args, makeCallback({ resolve, reject }, metadata)); + } + }); + }; + }; + + /** + * Wraps an existing method of the target object, so that calls to it are + * intercepted by the given wrapper function. The wrapper function receives, + * as its first argument, the original `target` object, followed by each of + * the arguments passed to the original method. + * + * @param {object} target + * The original target object that the wrapped method belongs to. + * @param {function} method + * The method being wrapped. This is used as the target of the Proxy + * object which is created to wrap the method. + * @param {function} wrapper + * The wrapper function which is called in place of a direct invocation + * of the wrapped method. + * + * @returns {Proxy} + * A Proxy object for the given method, which invokes the given wrapper + * method in its place. + */ + const wrapMethod = (target, method, wrapper) => { + return new Proxy(method, { + apply(targetMethod, thisObj, args) { + return wrapper.call(thisObj, target, ...args); + } + }); + }; + + let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); + + /** + * Wraps an object in a Proxy which intercepts and wraps certain methods + * based on the given `wrappers` and `metadata` objects. + * + * @param {object} target + * The target object to wrap. + * + * @param {object} [wrappers = {}] + * An object tree containing wrapper functions for special cases. Any + * function present in this object tree is called in place of the + * method in the same location in the `target` object tree. These + * wrapper methods are invoked as described in {@see wrapMethod}. + * + * @param {object} [metadata = {}] + * An object tree containing metadata used to automatically generate + * Promise-based wrapper functions for asynchronous. Any function in + * the `target` object tree which has a corresponding metadata object + * in the same location in the `metadata` tree is replaced with an + * automatically-generated wrapper function, as described in + * {@see wrapAsyncFunction} + * + * @returns {Proxy} + */ + const wrapObject = (target, wrappers = {}, metadata = {}) => { + let cache = Object.create(null); + let handlers = { + has(proxyTarget, prop) { + return prop in target || prop in cache; + }, + + get(proxyTarget, prop, receiver) { + if (prop in cache) { + return cache[prop]; + } + + if (!(prop in target)) { + return undefined; + } + + let value = target[prop]; + + if (typeof value === "function") { + // This is a method on the underlying object. Check if we need to do + // any wrapping. + + if (typeof wrappers[prop] === "function") { + // We have a special-case wrapper for this method. + value = wrapMethod(target, target[prop], wrappers[prop]); + } else if (hasOwnProperty(metadata, prop)) { + // This is an async method that we have metadata for. Create a + // Promise wrapper for it. + let wrapper = wrapAsyncFunction(prop, metadata[prop]); + value = wrapMethod(target, target[prop], wrapper); + } else { + // This is a method that we don't know or care about. Return the + // original method, bound to the underlying object. + value = value.bind(target); + } + } else if (typeof value === "object" && value !== null && (hasOwnProperty(wrappers, prop) || hasOwnProperty(metadata, prop))) { + // This is an object that we need to do some wrapping for the children + // of. Create a sub-object wrapper for it with the appropriate child + // metadata. + value = wrapObject(value, wrappers[prop], metadata[prop]); + } else { + // We don't need to do any wrapping for this property, + // so just forward all access to the underlying object. + Object.defineProperty(cache, prop, { + configurable: true, + enumerable: true, + get() { + return target[prop]; + }, + set(value) { + target[prop] = value; + } + }); + + return value; + } + + cache[prop] = value; + return value; + }, + + set(proxyTarget, prop, value, receiver) { + if (prop in cache) { + cache[prop] = value; + } else { + target[prop] = value; + } + return true; + }, + + defineProperty(proxyTarget, prop, desc) { + return Reflect.defineProperty(cache, prop, desc); + }, + + deleteProperty(proxyTarget, prop) { + return Reflect.deleteProperty(cache, prop); + } + }; + + // Per contract of the Proxy API, the "get" proxy handler must return the + // original value of the target if that value is declared read-only and + // non-configurable. For this reason, we create an object with the + // prototype set to `target` instead of using `target` directly. + // Otherwise we cannot return a custom object for APIs that + // are declared read-only and non-configurable, such as `chrome.devtools`. + // + // The proxy handlers themselves will still use the original `target` + // instead of the `proxyTarget`, so that the methods and properties are + // dereferenced via the original targets. + let proxyTarget = Object.create(target); + return new Proxy(proxyTarget, handlers); + }; + + /** + * Creates a set of wrapper functions for an event object, which handles + * wrapping of listener functions that those messages are passed. + * + * A single wrapper is created for each listener function, and stored in a + * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` + * retrieve the original wrapper, so that attempts to remove a + * previously-added listener work as expected. + * + * @param {DefaultWeakMap} wrapperMap + * A DefaultWeakMap object which will create the appropriate wrapper + * for a given listener function when one does not exist, and retrieve + * an existing one when it does. + * + * @returns {object} + */ + const wrapEvent = wrapperMap => ({ + addListener(target, listener, ...args) { + target.addListener(wrapperMap.get(listener), ...args); + }, + + hasListener(target, listener) { + return target.hasListener(wrapperMap.get(listener)); + }, + + removeListener(target, listener) { + target.removeListener(wrapperMap.get(listener)); + } + }); + + // Keep track if the deprecation warning has been logged at least once. + let loggedSendResponseDeprecationWarning = false; + + const onMessageWrappers = new DefaultWeakMap(listener => { + if (typeof listener !== "function") { + return listener; + } + + /** + * Wraps a message listener function so that it may send responses based on + * its return value, rather than by returning a sentinel value and calling a + * callback. If the listener function returns a Promise, the response is + * sent when the promise either resolves or rejects. + * + * @param {*} message + * The message sent by the other end of the channel. + * @param {object} sender + * Details about the sender of the message. + * @param {function(*)} sendResponse + * A callback which, when called with an arbitrary argument, sends + * that value as a response. + * @returns {boolean} + * True if the wrapped listener returned a Promise, which will later + * yield a response. False otherwise. + */ + return function onMessage(message, sender, sendResponse) { + let didCallSendResponse = false; + + let wrappedSendResponse; + let sendResponsePromise = new Promise(resolve => { + wrappedSendResponse = function (response) { + if (!loggedSendResponseDeprecationWarning) { + console.warn(SEND_RESPONSE_DEPRECATION_WARNING, new Error().stack); + loggedSendResponseDeprecationWarning = true; + } + didCallSendResponse = true; + resolve(response); + }; + }); + + let result; + try { + result = listener(message, sender, wrappedSendResponse); + } catch (err) { + result = Promise.reject(err); + } + + const isResultThenable = result !== true && isThenable(result); + + // If the listener didn't returned true or a Promise, or called + // wrappedSendResponse synchronously, we can exit earlier + // because there will be no response sent from this listener. + if (result !== true && !isResultThenable && !didCallSendResponse) { + return false; + } + + // A small helper to send the message if the promise resolves + // and an error if the promise rejects (a wrapped sendMessage has + // to translate the message into a resolved promise or a rejected + // promise). + const sendPromisedResult = promise => { + promise.then(msg => { + // send the message value. + sendResponse(msg); + }, error => { + // Send a JSON representation of the error if the rejected value + // is an instance of error, or the object itself otherwise. + let message; + if (error && (error instanceof Error || typeof error.message === "string")) { + message = error.message; + } else { + message = "An unexpected error occurred"; + } + + sendResponse({ + __mozWebExtensionPolyfillReject__: true, + message + }); + }).catch(err => { + // Print an error on the console if unable to send the response. + console.error("Failed to send onMessage rejected reply", err); + }); + }; + + // If the listener returned a Promise, send the resolved value as a + // result, otherwise wait the promise related to the wrappedSendResponse + // callback to resolve and send it as a response. + if (isResultThenable) { + sendPromisedResult(result); + } else { + sendPromisedResult(sendResponsePromise); + } + + // Let Chrome know that the listener is replying. + return true; + }; + }); + + const wrappedSendMessageCallback = ({ reject, resolve }, reply) => { + if (chrome.runtime.lastError) { + // Detect when none of the listeners replied to the sendMessage call and resolve + // the promise to undefined as in Firefox. + // See https://github.com/mozilla/webextension-polyfill/issues/130 + if (chrome.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) { + resolve(); + } else { + reject(chrome.runtime.lastError); + } + } else if (reply && reply.__mozWebExtensionPolyfillReject__) { + // Convert back the JSON representation of the error into + // an Error instance. + reject(new Error(reply.message)); + } else { + resolve(reply); + } + }; + + const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => { + if (args.length < metadata.minArgs) { + throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); + } + + if (args.length > metadata.maxArgs) { + throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); + } + + return new Promise((resolve, reject) => { + const wrappedCb = wrappedSendMessageCallback.bind(null, { resolve, reject }); + args.push(wrappedCb); + apiNamespaceObj.sendMessage(...args); + }); + }; + + const staticWrappers = { + runtime: { + onMessage: wrapEvent(onMessageWrappers), + onMessageExternal: wrapEvent(onMessageWrappers), + sendMessage: wrappedSendMessage.bind(null, "sendMessage", { minArgs: 1, maxArgs: 3 }) + }, + tabs: { + sendMessage: wrappedSendMessage.bind(null, "sendMessage", { minArgs: 2, maxArgs: 3 }) + } + }; + const settingMetadata = { + clear: { minArgs: 1, maxArgs: 1 }, + get: { minArgs: 1, maxArgs: 1 }, + set: { minArgs: 1, maxArgs: 1 } + }; + apiMetadata.privacy = { + network: { + networkPredictionEnabled: settingMetadata, + webRTCIPHandlingPolicy: settingMetadata + }, + services: { + passwordSavingEnabled: settingMetadata + }, + websites: { + hyperlinkAuditingEnabled: settingMetadata, + referrersEnabled: settingMetadata + } + }; + + return wrapObject(chrome, staticWrappers, apiMetadata); + }; + + // The build process adds a UMD wrapper around this file, which makes the + // `module` variable available. + module.exports = wrapAPIs(); // eslint-disable-line no-undef + } else { + module.exports = browser; // eslint-disable-line no-undef + } +}); +//# sourceMappingURL=browser-polyfill.js.map diff --git a/livemining.html b/livemining.html index c7bba87..65aa974 100644 --- a/livemining.html +++ b/livemining.html @@ -4,6 +4,7 @@ Nazeka: Configuration for Live Mining + diff --git a/mining.html b/mining.html index 5d0fd64..2d1540b 100644 --- a/mining.html +++ b/mining.html @@ -11,6 +11,7 @@ body font-size: 1em; } + diff --git a/options.html b/options.html index 7a458d2..97d1560 100644 --- a/options.html +++ b/options.html @@ -4,6 +4,7 @@ Nazeka: options + diff --git a/options.js b/options.js index 41a7188..a6863da 100644 --- a/options.js +++ b/options.js @@ -77,8 +77,14 @@ function defaults() settings.push({ id: "ignore_linebreaks", kind: "checkbox", + default: true, + label: "Ignore hard line breaks (disable for text with preformatted linewraps)" + }); + settings.push({ + id: "ignore_divs", + kind: "checkbox", default: false, - label: "Ignore hard line breaks" + label: "Search out from within block-like elements like divs (enable only for very poorly-constructed web pages; usually causes performance problems)" }); settings.push({ id: "live_mining", @@ -146,7 +152,7 @@ function defaults() id: "fixedwidthpositioning", kind: "checkbox", default: false, - label: "Position as though fixed width" + label: "Position as though fixed width (currently nonfunctional; will fix later)" }); settings.push({ id: "corner", diff --git a/reader.html b/reader.html index ac315dc..b55588b 100644 --- a/reader.html +++ b/reader.html @@ -20,6 +20,7 @@ p margin-bottom: 5px; } + diff --git a/readme.txt b/readme.md similarity index 69% rename from readme.txt rename to readme.md index 4bbb7c7..8da13c1 100644 --- a/readme.txt +++ b/readme.md @@ -1,10 +1,12 @@ Nazeka is a rikai replacement. -*Nazeka is not ready for general use yet; it does not yet have all the necessary behaviors that rikai variants have.* +*(Nazeka is not ready for general use yet; it does not yet have all the necessary behaviors that rikai variants have.** This is my first nontrivial javascript work and the code is probably terrible. -Options: +# Options + +Nazeka has added many options since it was first created, and they're documented within the options page. The following is an example of what options it has: - Enabled: whether to search pages for text - Compact: definitions on same line or with linebreaks @@ -24,13 +26,17 @@ Options: Nazeka has added more options since this was written. -Building: +# Building Build process requires python and lxml, or an existing copy of the extension to rip JMdict#.json from. +- Copy the appropriate manifest.json from etc/ (either etc/firefox/manifest.json or etc/chrome/manifest.json) into the root of the repository - Download JMdict.gz from http://www.edrdg.org/jmdict/edict_doc.html -- Convert it to json with https://gist.github.com/wareya/c2175520db5f1927e4f6ba839487dd8c -- Place under dict/ so that [...]/dict/JMdict1.json and others exist in that location relative to [...]/manifest.json +- Convert it to json with etc/process.py +- Move the output to under dict/ so that [...]/dict/JMdict1.json and others exist in that location relative to [...]/manifest.json +- Make sure every .json file is listed as available to the extension in manifest.json - if jmdict added a lot of words, there might be more than 11 files now - Package as an extension for your browser of choice, or load it as a temporary/indev extension using your browser's development tools +# Copyright and License + Copyright 2017~2018; Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0 diff --git a/texthook.js b/texthook.js index d1b1ae0..2aab3c3 100644 --- a/texthook.js +++ b/texthook.js @@ -14,6 +14,15 @@ * */ +if (navigator.userAgent.indexOf('Chrome') > -1) +{ + var ischrome = true; +} +else +{ + var ischrome = false; +} + 'use strict'; // updated by a timer looping function, based on local storage set by the options page @@ -41,7 +50,8 @@ hlfont: "", corner: 0, xoffset: 5, yoffset: 22, -ignore_linebreaks: false, +ignore_linebreaks: true, +ignore_divs: false, sticky: false, popup_follows_mouse: true, popup_requires_key: 0, // 0: none; 1: ctrl; 2: shift @@ -66,11 +76,15 @@ 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){} while(my_platform == "") { try { my_platform = await browser.runtime.sendMessage({type:"platform"}); + if(my_platform) + my_platform = my_platform["response"]; }catch(err){} } platform = my_platform; @@ -1023,7 +1037,8 @@ async function settings_init() getvar("xoffset", 5); getvar("yoffset", 22); - getvar("ignore_linebreaks", false); + getvar("ignore_linebreaks", true); + getvar("ignore_divs", false); getvar("sticky", false); getvar("popup_follows_mouse", true); getvar("popup_requires_key", 0); @@ -1072,100 +1087,67 @@ browser.storage.onChanged.addListener((updates, storageArea) => } }); -// look up words on a timer loop that only uses the most recent lookup request and ignores all the others - -let lookup_timer = undefined; -let lookup_queue = []; -//let lookup_rate = 8; - -let last_lookup = []; - +let last_lookup = undefined; let last_manual_interaction = Date.now(); - -let lookup_loop_cancel = false; let lookup_last_time = Date.now(); -async function lookup_loop() +async function send_lookup(lookup) { - //console.log("running lookup queue"); lookup_last_time = Date.now(); - let t_start = Date.now(); - if(lookup_queue.length > 0) - { - //console.log("queue not empty"); - let lookup = lookup_queue.pop(); - lookup_queue = []; - last_lookup = lookup; - //console.log("asking background to search the dictionary"); - let response = await browser.runtime.sendMessage({type:"search", text:lookup[0], time:Date.now(), divexisted:exists_div(), alternatives_mode:settings.alternatives_mode, strict_alternatives:settings.strict_alternatives}); - //console.log("got response"); - if(response && response != "itsthesame") - { - if(!response.length) - { - let mydiv = build_div(response.text, response.result, lookup[5], lookup[6]); - if(mydiv) - display_div(mydiv, lookup[3], lookup[4]); - } - else - { - let mydiv = build_div_compound(response, lookup[5], lookup[6]); - if(mydiv) - display_div(mydiv, lookup[3], lookup[4]); - } - } - //else if(response != "itsthesame" && settings.popup_follows_mouse && exists_div() && !lastMoreText.includes(lookup[0].substring(0, Math.max(0, lookup[0].length-2)))) - else if(response != "itsthesame" && exists_div() && !lastMoreText.includes(lookup[0].substring(0, Math.max(1, lookup[0].length-2)))) - lookup_cancel(); - } - let t_end = Date.now(); - let t_to_wait = settings.lookuprate - (t_end - t_start); - if(t_to_wait < 0) t_to_wait = 0; - if(t_to_wait > settings.lookuprate) t_to_wait = settings.lookuprate; + last_lookup = lookup; - if(lookup_loop_cancel) + let response = await browser.runtime.sendMessage( { - //console.log("queue setup"); - lookup_loop_cancel = false; - lookup_timer = undefined; - return; + type:"search", + text:lookup[0], + time:Date.now(), + divexisted:exists_div(), + alternatives_mode:settings.alternatives_mode, + strict_alternatives:settings.strict_alternatives + }); + if(response) + response = response["response"]; + + if(response && response != "itsthesame") + { + if(!response.length) + { + let mydiv = build_div(response.text, response.result, lookup[5], lookup[6]); + if(mydiv) + display_div(mydiv, lookup[3], lookup[4]); + } + else + { + let mydiv = build_div_compound(response, lookup[5], lookup[6]); + if(mydiv) + display_div(mydiv, lookup[3], lookup[4]); + } } - else - lookup_timer = setTimeout(lookup_loop, t_to_wait); + else if(response != "itsthesame" && exists_div() && !lastMoreText.includes(lookup[0].substring(0, Math.max(1, lookup[0].length-2)))) + lookup_cancel(); } - -lookup_timer = setTimeout(lookup_loop, settings.lookuprate); - function lookup_enqueue(text, x, y, x2, y2, moreText, index) { - //console.log("trying to enqueue lookup"); - lookup_queue = [[text, x, y, x2, y2, moreText, index]]; - //console.log("enqueued lookup"); - if(!lookup_timer || lookup_last_time+settings.lookuprate*100 < Date.now()) + if(lookup_last_time+settings.lookuprate < Date.now()) { - if(lookup_timer) - clearTimeout(lookup_timer); - lookup_timer = setTimeout(lookup_loop, settings.lookuprate); + send_lookup([text, x, y, x2, y2, moreText, index]); } } function lookup_cancel() { - lookup_queue = []; if(!settings.sticky || get_doc().body.getElementsByClassName("nazeka_mining_ui").length != 0 || platform == "android") delete_div(); } function manual_close() { - lookup_queue = []; delete_div(); last_manual_interaction = Date.now(); } function lookup_cancel_force() { - lookup_queue = []; delete_div(); } @@ -1183,8 +1165,8 @@ function lookup_left() index--; text = text.substring(index); - lookup_queue = [[text, last_lookup[1], last_lookup[2], last_lookup[3], last_lookup[4], last_lookup[5], 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]); } function lookup_right() @@ -1198,8 +1180,8 @@ function lookup_right() index++; text = text.substring(index); - lookup_queue = [[text, last_lookup[1], last_lookup[2], last_lookup[3], last_lookup[4], last_lookup[5], 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]); } let time_of_last = Date.now(); @@ -1250,18 +1232,25 @@ function grab_more_text(textNode, direction = 1) iters += 1; if(current_node == undefined) break; // search up parent element only if current element is inline-like - try + if (!settings.ignore_divs) { - let display = getComputedStyle(current_node).display; - let tagname = current_node.tagName.toLowerCase(); - // FIXME get real inline vs block detection - if(!(display.contains("inline") || display == "ruby") || tagname == "rt" || tagname == "rp") + try { - break; + 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"; + + let ruby_interior = tagname == "rt" || tagname == "rp"; + console.log(display); + console.log(inline_like); + console.log(ruby_interior); + if(ruby_interior || !inline_like) + break; } + catch(err){} } - catch(err){} - if(iters > 100) { console.log("too many iterations"); @@ -1272,6 +1261,8 @@ function grab_more_text(textNode, direction = 1) let parent = current_node.parentNode; if(parent == undefined || parent == current_node) break; let i = Array.prototype.indexOf.call(current_node.parentNode.childNodes, current_node); + console.log(current_node); + console.log(i); if(i < 0) break; i += direction; while(i < current_node.parentNode.childNodes.length && i >= 0 && text.length < settings.contextlength && (!text.includes("\n") || settings.ignore_linebreaks)) @@ -1373,6 +1364,16 @@ function grab_text(textNode, offset, elemental) 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; @@ -1448,6 +1449,7 @@ function update(event) yoffset = searchoffset; else xoffset = searchoffset; + // firefox if (document.caretPositionFromPoint) { let caretdata = document.caretPositionFromPoint(x+xoffset, y+yoffset); @@ -1462,14 +1464,28 @@ function update(event) range.detach(); } } + // chrome else if (document.caretRangeFromPoint) { let range = document.caretRangeFromPoint(x+xoffset, y+yoffset); if(range) { + try + { + range.setEnd(range.endContainer, range.endOffset+1); + } + catch(err) + { + try + { + range.setEnd(true_next_sibling(range.endContainer), 0); + } + catch(err){} + } textNode = range.startContainer; offset = range.startOffset; hitrect = range.getBoundingClientRect(); + console.log(hitrect); } } if(ele && !ele.contains(textNode) && platform != "android" && (!settings.sticky || get_doc().body.getElementsByClassName("nazeka_mining_ui").length != 0)) // sticky mode and android need to break out on parent detection @@ -1498,7 +1514,7 @@ function update(event) return; } - if(!(textNode == undefined)) + 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 @@ -1533,9 +1549,6 @@ function update(event) let elemental = acceptable_element(textNode); if (textNode && (textNode.nodeType == 3 || elemental)) { - //print_object(textNode); - //print_object(textNode.parentNode); - let rect = undefined; let fud = 5; if(elemental) @@ -1544,13 +1557,15 @@ function update(event) 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) + if(!ischrome) { - lookup_cancel(); - return; + 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); @@ -1558,33 +1573,15 @@ function update(event) let moreText = found[1]; let index = found[2]; - //text = text.trim(); - - //print_object(text); text = text.substring(0, Math.min(text.length, settings.length)); - //if(text != "") - //lookup_indirect(text, event.clientX, event.clientY, time_of_last); if(text != "") - { - //console.log("calling lookup_enqueue"); lookup_enqueue(text, event.clientX, event.clientY, event.pageX, event.pageY, moreText, index); - } else - { -// console.log("no text"); lookup_cancel(); - } } else - { -// console.log("no text node"); -// console.log("actual type:"); -// console.log(textNode.nodeType); -// console.log("offset:"); -// console.log(offset); lookup_cancel(); - } } function update_touch(event)