X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fmodules%2Futil.jsm;h=7941ebf587249f94d4a90979432023c773cbe67a;hb=70740024f9c028c1fd63e1a1850ab062ff956054;hp=673366a099188f42f964583bc1a7f44a78d6f0b5;hpb=eeed0be1a8abf7e3c97f43b63c1d595e940fef21;p=dactyl.git diff --git a/common/modules/util.jsm b/common/modules/util.jsm index 673366a..7941ebf 100644 --- a/common/modules/util.jsm +++ b/common/modules/util.jsm @@ -13,7 +13,7 @@ Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("util", { exports: ["frag", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"], require: ["services"], - use: ["commands", "config", "highlight", "storage", "template"] + use: ["commands", "config", "highlight", "messages", "storage", "template"] }, this); var XBL = Namespace("xbl", "http://www.mozilla.org/xbl"); @@ -115,12 +115,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), * * @param {object} obj */ - addObserver: function (obj) { + addObserver: update(function addObserver(obj) { if (!obj.observers) obj.observers = obj.observe; function register(meth) { - for (let target in set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers)))) + for (let target in Set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers)))) try { services.observer[meth](obj, target, true); } @@ -137,7 +137,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), } catch (e) { if (typeof util === "undefined") - dump("dactyl: error: " + e + "\n" + (e.stack || Error().stack).replace(/^/gm, "dactyl: ")); + addObserver.dump("dactyl: error: " + e + "\n" + (e.stack || addObserver.Error().stack).replace(/^/gm, "dactyl: ")); else util.reportError(e); } @@ -145,7 +145,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), obj.observe.unregister = function () register("removeObserver"); register("addObserver"); - }, + }, { dump: dump, Error: Error }), /* * Tests a condition and throws a FailedAssertion error on @@ -158,6 +158,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), assert: function (condition, message, quiet) { if (!condition) throw FailedAssertion(message, 1, quiet === undefined ? true : quiet); + return condition; }, /** @@ -165,7 +166,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), * @param {string} str The string to capitalize * @returns {string} */ - capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1), + capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1).toLowerCase(), /** * Returns a RegExp object that matches characters specified in the range @@ -181,12 +182,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), // check for chars not in the accepted range this.assert(RegExp("^[" + accepted + "-]+$").test(list), - "Character list outside the range " + accepted.quote()); + _("error.charactersOutsideRange", accepted.quote())); // check for illegal ranges for (let [match] in this.regexp.iterate(/.-./g, list)) this.assert(match.charCodeAt(0) <= match.charCodeAt(2), - "Invalid character range: " + list.slice(list.indexOf(match))) + _("error.invalidCharacterRange", list.slice(list.indexOf(match)))); return RegExp("[" + util.regexp.escape(list) + "]"); }, @@ -308,7 +309,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), } else if (char === "]") { stack.pop(); - util.assert(stack.length, "Unmatched %] in format"); + util.assert(stack.length, /*L*/"Unmatched %] in format"); } else { let quote = function quote(obj, char) obj[char]; @@ -327,10 +328,35 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), if (end < format.length) stack.top.elements.push(format.substr(end)); - util.assert(stack.length === 1, "Unmatched %[ in format"); + util.assert(stack.length === 1, /*L*/"Unmatched %[ in format"); return stack.top; }, + /** + * Compiles a macro string into a function which generates a string + * result based on the input *macro* and its parameters. The + * definitive documentation for macro strings resides in :help + * macro-string. + * + * Macro parameters may have any of the following flags: + * e: The parameter is only tested for existence. Its + * interpolation is always empty. + * q: The result is quoted such that it is parsed as a single + * argument by the Ex argument parser. + * + * The returned function has the following additional properties: + * + * seen {set}: The set of parameters used in this macro. + * + * valid {function(object)}: Returns true if every parameter of + * this macro is provided by the passed object. + * + * @param {string} macro The macro string to compile. + * @param {boolean} keepUnknown If true, unknown macro parameters + * are left untouched. Otherwise, they are replaced with the null + * string. + * @returns {function} + */ compileMacro: function compileMacro(macro, keepUnknown) { let stack = [frame()]; stack.__defineGetter__("top", function () this[this.length - 1]); @@ -355,14 +381,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), ([^]*?) // 1 (?: (<\{) | // 2 - (< ((?:[a-z]-)?[a-z-]+?) >) | // 3 4 - (\}>) // 5 + (< ((?:[a-z]-)?[a-z-]+?) (?:\[([0-9]+)\])? >) | // 3 4 5 + (\}>) // 6 ) ]]>, "gixy"); macro = String(macro); let end = 0; for (let match in re.iterate(macro)) { - let [, prefix, open, full, macro, close] = match; + let [, prefix, open, full, macro, idx, close] = match; end += match[0].length; if (prefix) @@ -374,11 +400,11 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), } else if (close) { stack.pop(); - util.assert(stack.length, "Unmatched %] in macro"); + util.assert(stack.length, /*L*/"Unmatched %] in macro"); } else { let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro); - flags = set(flags); + flags = Set(flags); let quote = util.identity; if (flags.q) @@ -386,12 +412,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), if (flags.e) quote = function quote(obj) ""; - if (set.has(defaults, name)) + if (Set.has(defaults, name)) stack.top.elements.push(quote(defaults[name])); else { - stack.top.elements.push(update( - function (obj) obj[name] != null ? quote(obj[name]) : set.has(obj, name) ? "" : unknown(full), - { test: function (obj) obj[name] != null && obj[name] !== false && (!flags.e || obj[name] != "") })); + if (idx) { + idx = Number(idx) - 1; + stack.top.elements.push(update( + function (obj) obj[name] != null && idx in obj[name] ? quote(obj[name][idx]) : Set.has(obj, name) ? "" : unknown(full), + { test: function (obj) obj[name] != null && idx in obj[name] && obj[name][idx] !== false && (!flags.e || obj[name][idx] != "") })); + } + else { + stack.top.elements.push(update( + function (obj) obj[name] != null ? quote(obj[name]) : Set.has(obj, name) ? "" : unknown(full), + { test: function (obj) obj[name] != null && obj[name] !== false && (!flags.e || obj[name] != "") })); + } for (let elem in array.iterValues(stack)) elem.seen[name] = true; @@ -401,10 +435,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), if (end < macro.length) stack.top.elements.push(macro.substr(end)); - util.assert(stack.length === 1, "Unmatched <{ in macro"); + util.assert(stack.length === 1, /*L*/"Unmatched <{ in macro"); return stack.top; }, + /** + * Compiles a CSS spec and XPath pattern matcher based on the given + * list. List elements prefixed with "xpath:" are parsed as XPath + * patterns, while other elements are parsed as CSS specs. The + * returned function will, given a node, return an iterator of all + * descendants of that node which match the given specs. + * + * @param {[string]} list The list of patterns to match. + * @returns {function(Node)} + */ compileMatcher: function compileMatcher(list) { let xpath = [], css = []; for (let elem in values(list)) @@ -428,10 +472,18 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }); }, - validateMatcher: function validateMatcher(values) { + /** + * Validates a list as input for {@link #compileMatcher}. Returns + * true if and only if every element of the list is a valid XPath or + * CSS selector. + * + * @param {[string]} list The list of patterns to test + * @returns {boolean} True when the patterns are all valid. + */ + validateMatcher: function validateMatcher(list) { let evaluator = services.XPathEvaluator(); - let node = util.xmlToDom(
, document); - return this.testValues(values, function (value) { + let node = services.XMLDocument(); + return this.testValues(list, function (value) { if (/^xpath:/.test(value)) evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver); else @@ -483,10 +535,28 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), * Example: * "a{b,c}d" => ["abd", "acd"] * - * @param {string} pattern The pattern to deglob. + * @param {string|[string|Array]} pattern The pattern to deglob. * @returns [string] The resulting strings. */ debrace: function debrace(pattern) { + if (isArray(pattern)) { + let res = []; + let rec = function rec(acc) { + let vals; + + while (isString(vals = pattern[acc.length])) + acc.push(vals); + + if (acc.length == pattern.length) + res.push(acc.join("")) + else + for (let val in values(vals)) + rec(acc.concat(val)); + } + rec([]); + return res; + } + if (pattern.indexOf("{") == -1) return [pattern]; @@ -501,12 +571,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), res.push(pattern.substr(end)); return res.map(function (s) util.dequote(s, dequote)); } - let patterns = [], res = []; + let patterns = []; let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy, function (match) { patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy, null, ",{}")); }, "{}"); + + let res = []; function rec(acc) { if (acc.length == patterns.length) res.push(array(substrings).zip(acc).flatten().join("")); @@ -529,6 +601,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), dequote: function dequote(pattern, chars) pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0), + /** + * Converts a given DOM Node, Range, or Selection to a string. If + * *html* is true, the output is HTML, otherwise it is presentation + * text. + * + * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to + * stringify. + * @param {boolean} html Whether the output should be HTML rather + * than presentation text. + */ domToString: function (node, html) { if (node instanceof Ci.nsISelection && node.isCollapsed) return ""; @@ -538,7 +620,8 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), range.selectNode(node); node = range; } - let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer.ownerDocument; + let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer; + doc = doc.ownerDocument || doc; let encoder = services.HtmlEncoder(); encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted); @@ -564,6 +647,13 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), */ dump: defineModule.dump, + /** + * Returns a list of reformatted stack frames from + * {@see Error#stack}. + * + * @param {string} stack The stack trace from an Error. + * @returns {[string]} The stack frames. + */ stackLines: function (stack) { let lines = []; let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g; @@ -589,7 +679,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), * The set of input element type attribute values that mark the element as * an editable field. */ - editableInputs: set(["date", "datetime", "datetime-local", "email", "file", + editableInputs: Set(["date", "datetime", "datetime-local", "email", "file", "month", "number", "password", "range", "search", "tel", "text", "time", "url", "week"]), @@ -738,12 +828,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), [hours, minutes] = div(minutes, 60); [days, hours] = div(hours, 24); if (days) - return days + " days " + hours + " hours" + return /*L*/days + " days " + hours + " hours" if (hours) - return hours + "h " + minutes + "m"; + return /*L*/hours + "h " + minutes + "m"; if (minutes) - return minutes + ":" + pad(2, seconds); - return seconds + "s"; + return /*L*/minutes + ":" + pad(2, seconds); + return /*L*/seconds + "s"; }, /** @@ -758,12 +848,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), uri = util.newURI(util.fixURI(uri)); if (uri instanceof Ci.nsIFileURL) - return File(uri.QueryInterface(Ci.nsIFileURL).file); + return File(uri.file); let channel = services.io.newChannelFromURI(uri); channel.cancel(Cr.NS_BINDING_ABORTED); if (channel instanceof Ci.nsIFileChannel) - return File(channel.QueryInterface(Ci.nsIFileChannel).file); + return File(channel.file); } catch (e) {} return null; @@ -826,7 +916,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), return xmlhttp; } catch (e) { - util.dactyl.log("Error opening " + String.quote(url) + ": " + e, 1); + util.dactyl.log(_("error.cantOpen", String.quote(url), e), 1); return null; } }, @@ -939,6 +1029,35 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), } }, + // ripped from Firefox; modified + unsafeURI: Class.memoize(function () util.regexp(String.replace(, /U/g, "\\u"), + "gx")), + losslessDecodeURI: function losslessDecodeURI(url) { + return url.split("%25").map(function (url) { + // Non-UTF-8 compliant URLs cause "malformed URI sequence" errors. + try { + return decodeURI(url).replace(this.unsafeURI, encodeURIComponent); + } + catch (e) { + return url; + } + }, this).join("%25"); + }, + /** * Returns an XPath union expression constructed from the specified node * tests. An expression is built with node tests for both the null and @@ -949,10 +1068,27 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), */ makeXPath: function makeXPath(nodes) { return array(nodes).map(util.debrace).flatten() - .map(function (node) [node, "xhtml:" + node]).flatten() + .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten() .map(function (node) "//" + node).join(" | "); }, + /** + * Creates a DTD fragment from the given object. Each property of + * the object is converted to an ENTITY declaration. SGML special + * characters other than ' and % are left intact. + * + * @param {object} obj The object to convert. + * @returns {string} The DTD fragment containing entity declaration + * for *obj*. + */ + makeDTD: let (map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" }) + function makeDTD(obj) iter(obj) + .map(function ([k, v]) ["]/g, + function (m) map[m]), + "'>"].join("")) + .join("\n"), + map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()), writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)), readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)), @@ -964,7 +1100,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), * @returns {nsIURI} */ // FIXME: createURI needed too? - newURI: function (uri, charset, base) services.io.newURI(uri, charset, base), + newURI: function newURI(uri, charset, base) this.withProperErrors("newURI", services.io, uri, charset, base), /** * Removes leading garbage prepended to URIs by the subscript @@ -1029,7 +1165,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }; let tag = "<" + [namespaced(elem)].concat( - [namespaced(a) + "=" + template.highlight(a.value, true) + [namespaced(a) + "=" + template.highlight(a.value, true) for ([i, a] in array.iterItems(elem.attributes))]).join(" "); return tag + (!hasChildren ? "/>" : ">..."); } @@ -1090,13 +1226,13 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }, observers: { - "dactyl-cleanup-modules": function () { - defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules"); + "dactyl-cleanup-modules": function (subject, reason) { + defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason); for (let module in values(defineModule.modules)) if (module.cleanup) { util.dump("cleanup: " + module.constructor.className); - util.trapErrors(module.cleanup, module); + util.trapErrors(module.cleanup, module, reason); } JSMLoader.cleanup(); @@ -1120,6 +1256,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), "dactyl-purge": function () { this.rehashing = 1; }, + "toplevel-window-ready": function (window, data) { window.addEventListener("DOMContentLoaded", wrapCallback(function listener(event) { if (event.originalTarget === window.document) { @@ -1199,6 +1336,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }), true); }, + /** + * Overlays an object with the given property overrides. Each + * property in *overrides* is added to *object*, replacing any + * original value. Functions in *overrides* are augmented with the + * new properties *super*, *supercall*, and *superapply*, in the + * same manner as class methods, so that they man call their + * overridden counterparts. + * + * @param {object} object The object to overlay. + * @param {object} overrides An object containing properties to + * override. + * @returns {function} A function which, when called, will remove + * the overlay. + */ overlayObject: function (object, overrides) { let original = Object.create(object); overrides = update(Object.create(original), overrides); @@ -1208,24 +1359,26 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), if (desc.value instanceof Class.Property) desc = desc.value.init(k) || desc.value; - for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj)) - if (orig = Object.getOwnPropertyDescriptor(obj, k)) - Object.defineProperty(original, k, orig); + if (k in object) { + for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj)) + if (orig = Object.getOwnPropertyDescriptor(obj, k)) + Object.defineProperty(original, k, orig); + + if (!orig) + if (orig = Object.getPropertyDescriptor(object, k)) + Object.defineProperty(original, k, orig); + } // Guard against horrible add-ons that use eval-based monkey // patching. + let value = desc.value; if (callable(desc.value)) { - let value = desc.value; - - let sentinel = "(function DactylOverlay() {}())" - value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&"); - value.toSource = function toSource() toString.toSource.call(this).replace(/\}?$/, sentinel + "; $&"); delete desc.value; delete desc.writable; desc.get = function get() value; desc.set = function set(val) { - if (String(val).indexOf(sentinel) < 0) + if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0) Class.replaceProperty(this, k, val); else { let package_ = util.newURI(util.fixURI(Components.stack.caller.filename)).host; @@ -1235,12 +1388,37 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }; } - Object.defineProperty(object, k, desc); + try { + Object.defineProperty(object, k, desc); + + if (callable(value)) { + let sentinel = "(function DactylOverlay() {}())" + value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&"); + value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&"); + } + } + catch (e) { + try { + if (value) { + object[k] = value; + return; + } + } + catch (f) {} + util.reportError(e); + } }, this); return function unwrap() { for each (let k in Object.getOwnPropertyNames(original)) - Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k)); + if (Object.getOwnPropertyDescriptor(object, k).configurable) + Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k)); + else { + try { + object[k] = original[k]; + } + catch (e) {} + } }; }, @@ -1305,17 +1483,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit") elems.push(encode(field.name, field.value)); - for (let [, elem] in iter(form.elements)) { - if (set.has(util.editableInputs, elem.type) - || /^(?:hidden|textarea)$/.test(elem.type) - || elem.checked && /^(?:checkbox|radio)$/.test(elem.type)) - elems.push(encode(elem.name, elem.value, elem === field)); - else if (elem instanceof Ci.nsIDOMHTMLSelectElement) { - for (let [, opt] in Iterator(elem.options)) - if (opt.selected) - elems.push(encode(elem.name, opt.value)); + for (let [, elem] in iter(form.elements)) + if (elem.name && !elem.disabled) { + if (Set.has(util.editableInputs, elem.type) + || /^(?:hidden|textarea)$/.test(elem.type) + || elem.type == "submit" && elem == field + || elem.checked && /^(?:checkbox|radio)$/.test(elem.type)) + elems.push(encode(elem.name, elem.value, elem === field)); + else if (elem instanceof Ci.nsIDOMHTMLSelectElement) { + for (let [, opt] in Iterator(elem.options)) + if (opt.selected) + elems.push(encode(elem.name, opt.value)); + } } - } + if (post) return [url, elems.join('&'), charset, elems]; return [url + "?" + elems.join('&'), null, charset, elems]; @@ -1400,7 +1581,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), // Replace replacement . if (tokens) - expr = String.replace(expr, /(\(?P)?<(\w+)>/g, function (m, n1, n2) !n1 && set.has(tokens, n2) ? tokens[n2].dactylSource || tokens[n2].source || tokens[n2] : m); + expr = String.replace(expr, /(\(?P)?<(\w+)>/g, function (m, n1, n2) !n1 && Set.has(tokens, n2) ? tokens[n2].dactylSource || tokens[n2].source || tokens[n2] : m); // Strip comments and white space. if (/x/.test(flags)) @@ -1471,9 +1652,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }()) }), + /** + * Reloads dactyl in entirety by disabling the add-on and + * re-enabling it. + */ rehash: function (args) { - JSMLoader.commandlineArgs = args; + storage.session.commandlineArgs = args; this.timeout(function () { + services.observer.notifyObservers(null, "startupcache-invalidate", ""); this.rehashing = true; let addon = config.addon; addon.userDisabled = true; @@ -1484,6 +1670,11 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), errorCount: 0, errors: Class.memoize(function () []), maxErrors: 15, + /** + * Reports an error to the Error Console and the standard output, + * along with a stack trace and other relevant information. The + * error is appended to {@see #errors}. + */ reportError: function (error) { if (error.noTrace) return; @@ -1541,7 +1732,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), catch (e) {} let ary = host.split("."); - ary = [ary.slice(i).join(".") for (i in util.range(ary.length - 1, 0, -1))]; + ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))]; return ary.filter(function (h) h.length >= base.length); }, @@ -1586,20 +1777,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), return true; }, - highlightFilter: function highlightFilter(str, filter, highlight) { - return this.highlightSubstrings(str, (function () { - if (filter.length == 0) - return; - let lcstr = String.toLowerCase(str); - let lcfilter = filter.toLowerCase(); - let start = 0; - while ((start = lcstr.indexOf(lcfilter, start)) > -1) { - yield [start, filter.length]; - start += filter.length; - } - })(), highlight || template.filter); - }, - /** * Behaves like String.split, except that when *limit* is reached, * the trailing element contains the entire trailing portion of the @@ -1658,6 +1835,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }, yielders: 0, + /** + * Yields execution to the next event in the current thread's event + * queue. This is a potentially dangerous operation, since any + * yielders higher in the event stack will prevent execution from + * returning to the caller until they have finished their wait. The + * potential for deadlock is high. + * + * @param {boolean} flush If true, flush all events in the event + * queue before returning. Otherwise, wait for an event to + * process before proceeding. + * @param {boolean} interruptable If true, this yield may be + * interrupted by pressing , in which case, + * Error("Interrupted") will be thrown. + */ threadYield: function (flush, interruptable) { this.yielders++; try { @@ -1667,7 +1858,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), do { mainThread.processNextEvent(!flush); if (util.interrupted) - throw new Error("Interrupted"); + throw Error("Interrupted"); } while (flush === true && mainThread.hasPendingEvents()); } @@ -1676,6 +1867,19 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), } }, + /** + * Waits for the function *test* to return true, or *timeout* + * milliseconds to expire. + * + * @param {function} test The predicate on which to wait. + * @param {object} self The 'this' object for *test*. + * @param {Number} timeout The maximum number of milliseconds to + * wait. + * @optional + * @param {boolean} interruptable If true, may be interrupted by + * pressing , in which case, Error("Interrupted") will be + * thrown. + */ waitFor: function waitFor(test, self, timeout, interruptable) { let end = timeout && Date.now() + timeout, result; @@ -1690,6 +1894,23 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), return result; }, + /** + * Makes the passed function yieldable. Each time the function calls + * yield, execution is suspended for the yielded number of + * milliseconds. + * + * Example: + * let func = yieldable(function () { + * util.dump(Date.now()); // 0 + * yield 1500; + * util.dump(Date.now()); // 1500 + * }); + * func(); + * + * @param {function} func The function to mangle. + * @returns {function} A new function which may not execute + * synchronously. + */ yieldable: function yieldable(func) function magic() { let gen = func.apply(this, arguments); @@ -1701,6 +1922,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), })(); }, + /** + * Wraps a callback function such that its errors are not lost. This + * is useful for DOM event listeners, which ordinarily eat errors. + * The passed function has the property *wrapper* set to the new + * wrapper function, while the wrapper has the property *wrapped* + * set to the original callback. + * + * @param {function} callback The callback to wrap. + * @returns {function} + */ wrapCallback: wrapCallback, /** @@ -1722,7 +1953,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), */ trapErrors: function trapErrors(func, self) { try { - if (isString(func)) + if (!callable(func)) func = self[func]; return func.apply(self || this, Array.slice(arguments, 2)); } @@ -1732,6 +1963,15 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), } }, + /** + * Returns the file path of a given *url*, for debugging purposes. + * If *url* points to a file (even if indirectly), the native + * filesystem path is returned. Otherwise, the URL itself is + * returned. + * + * @param {string} url The URL to mangle. + * @returns {string} The path to the file. + */ urlPath: function urlPath(url) { try { return util.getFile(url).path; @@ -1741,18 +1981,33 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), } }, + /** + * Returns a list of all domains and subdomains of documents in the + * given window and all of its descendant frames. + * + * @param {nsIDOMWindow} win The window for which to find domains. + * @returns {[string]} The visible domains. + */ visibleHosts: function (win) { let res = [], seen = {}; (function rec(frame) { try { - res = res.concat(util.subdomains(frame.location.host)); + if (frame.location.hostname) + res = res.concat(util.subdomains(frame.location.hostname)); } catch (e) {} Array.forEach(frame.frames, rec); })(win); - return res.filter(function (h) !set.add(seen, h)); + return res.filter(function (h) !Set.add(seen, h)); }, + /** + * Returns a list of URIs of documents in the given window and all + * of its descendant frames. + * + * @param {nsIDOMWindow} win The window for which to find URIs. + * @returns {[nsIURI]} The visible URIs. + */ visibleURIs: function (win) { let res = [], seen = {}; (function rec(frame) { @@ -1762,7 +2017,24 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), catch (e) {} Array.forEach(frame.frames, rec); })(win); - return res.filter(function (h) !set.add(seen, h.spec)); + return res.filter(function (h) !Set.add(seen, h.spec)); + }, + + /** + * Wraps native exceptions thrown by the called function so that a + * proper stack trace may be retrieved from them. + * + * @param {function|string} meth The method to call. + * @param {object} self The 'this' object of the method. + * @param ... Arguments to pass to *meth*. + */ + withProperErrors: function withProperErrors(meth, self) { + try { + return (callable(meth) ? meth : self[meth]).apply(self, Array.slice(arguments, withProperErrors.length)); + } + catch (e) { + throw e.stack ? e : Error(e); + } }, /**