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