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 ? "/>" : ">..." + namespaced(elem) + ">"); } @@ -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