1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2012 Kris Maglione <maglione.k@gmail.com>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 defineModule("util", {
12 exports: ["DOM", "$", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
13 require: ["dom", "services"]
16 lazyRequire("overlay", ["overlay"]);
17 lazyRequire("storage", ["File", "storage"]);
18 lazyRequire("template", ["template"]);
20 var Magic = Class("Magic", {
21 init: function init(str) {
25 get message() this.str,
27 toString: function () this.str
30 var FailedAssertion = Class("FailedAssertion", ErrorBase, {
31 init: function init(message, level, noTrace) {
32 if (noTrace !== undefined)
33 this.noTrace = noTrace;
34 init.supercall(this, message, level);
42 var Point = Struct("Point", "x", "y");
44 var wrapCallback = function wrapCallback(fn, isEvent) {
46 fn.wrapper = function wrappedCallback() {
48 let res = fn.apply(this, arguments);
49 if (isEvent && res === false) {
50 arguments[0].preventDefault();
51 arguments[0].stopPropagation();
60 fn.wrapper.wrapped = fn;
64 var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
67 init: function init() {
70 this.addObserver(this);
74 activeWindow: deprecated("overlay.activeWindow", { get: function activeWindow() overlay.activeWindow }),
75 overlayObject: deprecated("overlay.overlayObject", { get: function overlayObject() overlay.closure.overlayObject }),
76 overlayWindow: deprecated("overlay.overlayWindow", { get: function overlayWindow() overlay.closure.overlayWindow }),
78 compileMatcher: deprecated("DOM.compileMatcher", { get: function compileMatcher() DOM.compileMatcher }),
79 computedStyle: deprecated("DOM#style", function computedStyle(elem) DOM(elem).style),
80 domToString: deprecated("DOM.stringify", { get: function domToString() DOM.stringify }),
81 editableInputs: deprecated("DOM.editableInputs", { get: function editableInputs(elem) DOM.editableInputs }),
82 escapeHTML: deprecated("DOM.escapeHTML", { get: function escapeHTML(elem) DOM.escapeHTML }),
83 evaluateXPath: deprecated("DOM.XPath",
84 function evaluateXPath(path, elem, asIterator) DOM.XPath(path, elem || util.activeWindow.content.document, asIterator)),
85 isVisible: deprecated("DOM#isVisible", function isVisible(elem) DOM(elem).isVisible),
86 makeXPath: deprecated("DOM.makeXPath", { get: function makeXPath(elem) DOM.makeXPath }),
87 namespaces: deprecated("DOM.namespaces", { get: function namespaces(elem) DOM.namespaces }),
88 namespaceNames: deprecated("DOM.namespaceNames", { get: function namespaceNames(elem) DOM.namespaceNames }),
89 parseForm: deprecated("DOM#formData", function parseForm(elem) values(DOM(elem).formData).toArray()),
90 scrollIntoView: deprecated("DOM#scrollIntoView", function scrollIntoView(elem, alignWithTop) DOM(elem).scrollIntoView(alignWithTop)),
91 validateMatcher: deprecated("DOM.validateMatcher", { get: function validateMatcher() DOM.validateMatcher }),
92 xmlToDom: deprecated("DOM.fromJSON", function xmlToDom() DOM.fromXML.apply(DOM, arguments)),
94 map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
95 writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
96 readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
98 chromePackages: deprecated("config.chromePackages", { get: function chromePackages() config.chromePackages }),
99 haveGecko: deprecated("config.haveGecko", { get: function haveGecko() config.closure.haveGecko }),
100 OS: deprecated("config.OS", { get: function OS() config.OS }),
102 dactyl: update(function dactyl(obj) {
104 var global = Class.objectGlobal(obj);
107 __noSuchMethod__: function __noSuchMethod__(meth, args) {
108 let win = overlay.activeWindow;
110 var dactyl = global && global.dactyl || win && win.dactyl;
114 let prop = dactyl[meth];
116 return prop.apply(dactyl, args);
121 __noSuchMethod__: function __noSuchMethod__() this().__noSuchMethod__.apply(null, arguments)
125 * Registers a obj as a new observer with the observer service. obj.observe
126 * must be an object where each key is the name of a target to observe and
127 * each value is a function(subject, data) to be called when the given
128 * target is broadcast. obj.observe will be replaced with a new opaque
129 * function. The observer is automatically unregistered on application
132 * @param {object} obj
134 addObserver: update(function addObserver(obj) {
136 obj.observers = obj.observe;
138 let cleanup = ["dactyl-cleanup-modules", "quit-application"];
140 function register(meth) {
141 for (let target in Set(cleanup.concat(Object.keys(obj.observers))))
143 services.observer[meth](obj, target, true);
148 Class.replaceProperty(obj, "observe",
149 function (subject, target, data) {
151 if (~cleanup.indexOf(target))
152 register("removeObserver");
153 if (obj.observers[target])
154 obj.observers[target].call(obj, subject, data);
157 if (typeof util === "undefined")
158 addObserver.dump("dactyl: error: " + e + "\n" + (e.stack || addObserver.Error().stack).replace(/^/gm, "dactyl: "));
164 obj.observe.unregister = function () register("removeObserver");
165 register("addObserver");
166 }, { dump: dump, Error: Error }),
169 * Tests a condition and throws a FailedAssertion error on
172 * @param {boolean} condition The condition to test.
173 * @param {string} message The message to present to the
176 assert: function assert(condition, message, quiet) {
178 throw FailedAssertion(message, 1, quiet === undefined ? true : quiet);
183 * CamelCases a -non-camel-cased identifier name.
185 * @param {string} name The name to mangle.
186 * @returns {string} The mangled name.
188 camelCase: function camelCase(name) String.replace(name, /-(.)/g, function (m, m1) m1.toUpperCase()),
191 * Capitalizes the first character of the given string.
192 * @param {string} str The string to capitalize
195 capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1).toLowerCase(),
198 * Returns a RegExp object that matches characters specified in the range
199 * expression *list*, or signals an appropriate error if *list* is invalid.
201 * @param {string} list Character list, e.g., "a b d-xA-Z" produces /[abd-xA-Z]/.
202 * @param {string} accepted Character range(s) to accept, e.g. "a-zA-Z" for
203 * ASCII letters. Used to validate *list*.
206 charListToRegexp: function charListToRegexp(list, accepted) {
207 list = list.replace(/\s+/g, "");
209 // check for chars not in the accepted range
210 this.assert(RegExp("^[" + accepted + "-]+$").test(list),
211 _("error.charactersOutsideRange", accepted.quote()));
213 // check for illegal ranges
214 for (let [match] in this.regexp.iterate(/.-./g, list))
215 this.assert(match.charCodeAt(0) <= match.charCodeAt(2),
216 _("error.invalidCharacterRange", list.slice(list.indexOf(match))));
218 return RegExp("[" + util.regexp.escape(list) + "]");
222 * Returns a shallow copy of *obj*.
224 * @param {Object} obj
227 cloneObject: function cloneObject(obj) {
231 for (let [k, v] in Iterator(obj))
237 * Clips a string to a given length. If the input string is longer
238 * than *length*, an ellipsis is appended.
240 * @param {string} str The string to truncate.
241 * @param {number} length The length of the returned string.
244 clip: function clip(str, length) {
245 return str.length <= length ? str : str.substr(0, length - 3) + "...";
249 * Compares two strings, case insensitively. Return values are as
250 * in String#localeCompare.
256 compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()),
258 compileFormat: function compileFormat(format) {
259 let stack = [frame()];
260 stack.__defineGetter__("top", function () this[this.length - 1]);
262 function frame() update(
264 _frame === stack.top || _frame.valid(obj) ?
265 _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
269 valid: function valid(obj) this.elements.every(function (e) !e.test || e.test(obj))
273 for (let match in util.regexp.iterate(/(.*?)%(.)/gy, format)) {
275 let [, prefix, char] = match;
276 end += match[0].length;
279 stack.top.elements.push(prefix);
281 stack.top.elements.push("%");
282 else if (char === "[") {
284 stack.top.elements.push(f);
287 else if (char === "]") {
289 util.assert(stack.length, /*L*/"Unmatched %] in format");
292 let quote = function quote(obj, char) obj[char];
293 if (char !== char.toLowerCase())
294 quote = function quote(obj, char) Commands.quote(obj[char]);
295 char = char.toLowerCase();
297 stack.top.elements.push(update(
298 function (obj) obj[char] != null ? quote(obj, char) : "",
299 { test: function test(obj) obj[char] != null }));
301 for (let elem in array.iterValues(stack))
302 elem.seen[char] = true;
305 if (end < format.length)
306 stack.top.elements.push(format.substr(end));
308 util.assert(stack.length === 1, /*L*/"Unmatched %[ in format");
313 * Compiles a macro string into a function which generates a string
314 * result based on the input *macro* and its parameters. The
315 * definitive documentation for macro strings resides in :help
318 * Macro parameters may have any of the following flags:
319 * e: The parameter is only tested for existence. Its
320 * interpolation is always empty.
321 * q: The result is quoted such that it is parsed as a single
322 * argument by the Ex argument parser.
324 * The returned function has the following additional properties:
326 * seen {set}: The set of parameters used in this macro.
328 * valid {function(object)}: Returns true if every parameter of
329 * this macro is provided by the passed object.
331 * @param {string} macro The macro string to compile.
332 * @param {boolean} keepUnknown If true, unknown macro parameters
333 * are left untouched. Otherwise, they are replaced with the null
335 * @returns {function}
337 compileMacro: function compileMacro(macro, keepUnknown) {
338 let stack = [frame()];
339 stack.__defineGetter__("top", function () this[this.length - 1]);
341 let unknown = util.identity;
343 unknown = function () "";
345 function frame() update(
347 _frame === stack.top || _frame.valid(obj) ?
348 _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
352 valid: function valid(obj) this.elements.every(function (e) !e.test || e.test(obj))
355 let defaults = { lt: "<", gt: ">" };
357 let re = util.regexp(literal(/*
361 (< ((?:[a-z]-)?[a-z-]+?) (?:\[([0-9]+)\])? >) | // 3 4 5
365 macro = String(macro);
367 for (let match in re.iterate(macro)) {
368 let [, prefix, open, full, macro, idx, close] = match;
369 end += match[0].length;
372 stack.top.elements.push(prefix);
375 stack.top.elements.push(f);
380 util.assert(stack.length, /*L*/"Unmatched }> in macro");
383 let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro);
386 let quote = util.identity;
388 quote = function quote(obj) typeof obj === "number" ? obj : String.quote(obj);
390 quote = function quote(obj) "";
392 if (Set.has(defaults, name))
393 stack.top.elements.push(quote(defaults[name]));
397 idx = Number(idx) - 1;
398 stack.top.elements.push(update(
399 function (obj) obj[name] != null && idx in obj[name] ? quote(obj[name][idx])
400 : Set.has(obj, name) ? "" : unknown(full),
402 test: function test(obj) obj[name] != null && idx in obj[name]
403 && obj[name][idx] !== false
404 && (!flags.e || obj[name][idx] != "")
408 stack.top.elements.push(update(
409 function (obj) obj[name] != null ? quote(obj[name])
410 : Set.has(obj, name) ? "" : unknown(full),
412 test: function test(obj) obj[name] != null
413 && obj[name] !== false
414 && (!flags.e || obj[name] != "")
418 for (let elem in array.iterValues(stack))
419 elem.seen[name] = true;
423 if (end < macro.length)
424 stack.top.elements.push(macro.substr(end));
426 util.assert(stack.length === 1, /*L*/"Unmatched <{ in macro");
431 * Converts any arbitrary string into an URI object. Returns null on
434 * @param {string} str
435 * @returns {nsIURI|null}
437 createURI: function createURI(str) {
439 let uri = services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
440 uri instanceof Ci.nsIURL;
449 * Expands brace globbing patterns in a string.
452 * "a{b,c}d" => ["abd", "acd"]
454 * @param {string|[string|Array]} pattern The pattern to deglob.
455 * @returns [string] The resulting strings.
457 debrace: function debrace(pattern) {
459 if (isArray(pattern)) {
460 // Jägermonkey hates us.
463 rec: function rec(acc) {
466 while (isString(vals = pattern[acc.length]))
469 if (acc.length == pattern.length)
470 this.res.push(acc.join(""));
472 for (let val in values(vals))
473 this.rec(acc.concat(val));
480 if (pattern.indexOf("{") == -1)
485 let split = function split(pattern, re, fn, dequote) {
486 let end = 0, match, res = [];
487 while (match = re.exec(pattern)) {
488 end = match.index + match[0].length;
493 res.push(pattern.substr(end));
494 return res.map(function (s) util.dequote(s, dequote));
498 let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
500 patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
504 let rec = function rec(acc) {
505 if (acc.length == patterns.length)
506 res.push(array(substrings).zip(acc).flatten().join(""));
508 for (let [, pattern] in Iterator(patterns[acc.length]))
509 rec(acc.concat(pattern));
514 catch (e if e.message && ~e.message.indexOf("res is undefined")) {
515 // prefs.safeSet() would be reset on :rehash
516 prefs.set("javascript.options.methodjit.chrome", false);
517 util.dactyl.warn(_(UTF8("error.damnYouJägermonkey")));
523 * Briefly delay the execution of the passed function.
525 * @param {function} callback The function to delay.
527 delay: function delay(callback) {
528 let { mainThread } = services.threading;
529 mainThread.dispatch(callback,
530 mainThread.DISPATCH_NORMAL);
534 * Removes certain backslash-quoted characters while leaving other
535 * backslash-quoting sequences untouched.
537 * @param {string} pattern The string to unquote.
538 * @param {string} chars The characters to unquote.
541 dequote: function dequote(pattern, chars)
542 pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0),
545 * Returns the nsIDocShell for the given window.
547 * @param {Window} win The window for which to get the docShell.
548 * @returns {nsIDocShell}
551 docShell: function docShell(win)
552 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
553 .QueryInterface(Ci.nsIDocShell),
556 * Prints a message to the console. If *msg* is an object it is pretty
559 * @param {string|Object} msg The message to print.
561 dump: defineModule.dump,
564 * Returns a list of reformatted stack frames from
565 * {@see Error#stack}.
567 * @param {string} stack The stack trace from an Error.
568 * @returns {[string]} The stack frames.
570 stackLines: function stackLines(stack) {
572 let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
573 while (match = re.exec(stack))
574 lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + "@" +
575 util.fixURI(match[2]));
580 * Dumps a stack trace to the console.
582 * @param {string} msg The trace message.
583 * @param {number} frames The number of frames to print.
585 dumpStack: function dumpStack(msg, frames) {
586 let stack = util.stackLines(Error().stack);
587 stack = stack.slice(1, 1 + (frames || stack.length)).join("\n").replace(/^/gm, " ");
588 util.dump((arguments.length == 0 ? "Stack" : msg) + "\n" + stack + "\n");
592 * Escapes quotes, newline and tab characters in *str*. The returned string
593 * is delimited by *delimiter* or " if *delimiter* is not specified.
594 * {@see String#quote}.
596 * @param {string} str
597 * @param {string} delimiter
600 escapeString: function escapeString(str, delimiter) {
601 if (delimiter == undefined)
603 return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
607 * Converts *bytes* to a pretty printed data size string.
609 * @param {number} bytes The number of bytes.
610 * @param {string} decimalPlaces The number of decimal places to use if
611 * *humanReadable* is true.
612 * @param {boolean} humanReadable Use byte multiples.
615 formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) {
616 const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
618 let tmpNum = parseInt(bytes, 10) || 0;
619 let strNum = [tmpNum + ""];
622 while (tmpNum >= 1024) {
624 if (++unitIndex > (unitVal.length - 1))
628 let decPower = Math.pow(10, decimalPlaces);
629 strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2);
634 while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces)
638 for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000
639 strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u);
641 if (unitIndex) // decimalPlaces only when > Bytes
642 strNum[0] += "." + strNum[1];
644 return strNum[0] + " " + unitVal[unitIndex];
648 * Converts *seconds* into a human readable time string.
650 * @param {number} seconds
653 formatSeconds: function formatSeconds(seconds) {
654 function pad(n, val) ("0000000" + val).substr(-Math.max(n, String(val).length));
655 function div(num, denom) [Math.floor(num / denom), Math.round(num % denom)];
656 let days, hours, minutes;
658 [minutes, seconds] = div(Math.round(seconds), 60);
659 [hours, minutes] = div(minutes, 60);
660 [days, hours] = div(hours, 24);
662 return /*L*/days + " days " + hours + " hours";
664 return /*L*/hours + "h " + minutes + "m";
666 return /*L*/minutes + ":" + pad(2, seconds);
667 return /*L*/seconds + "s";
671 * Returns the file which backs a given URL, if available.
673 * @param {nsIURI} uri The URI for which to find a file.
674 * @returns {File|null}
676 getFile: function getFile(uri) {
679 uri = util.newURI(uri);
681 if (uri instanceof Ci.nsIFileURL)
682 return File(uri.file);
684 if (uri instanceof Ci.nsIFile)
687 let channel = services.io.newChannelFromURI(uri);
688 try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
689 if (channel instanceof Ci.nsIFileChannel)
690 return File(channel.file);
697 * Returns the host for the given URL, or null if invalid.
699 * @param {string} url
700 * @returns {string|null}
702 getHost: function getHost(url) {
704 return util.createURI(url).host;
711 * Sends a synchronous or asynchronous HTTP request to *url* and returns
712 * the XMLHttpRequest object. If *callback* is specified the request is
713 * asynchronous and the *callback* is invoked with the object as its
716 * @param {string} url
717 * @param {object} params Optional parameters for this request:
718 * method: {string} The request method. @default "GET"
720 * params: {object} Parameters to append to *url*'s query string.
721 * data: {*} POST data to send to the server. Ordinary objects
722 * are converted to FormData objects, with one datum
723 * for each property/value pair.
725 * onload: {function(XMLHttpRequest, Event)} The request's load event handler.
726 * onerror: {function(XMLHttpRequest, Event)} The request's error event handler.
727 * callback: {function(XMLHttpRequest, Event)} An event handler
728 * called for either error or load events.
730 * background: {boolean} Whether to perform the request in the
731 * background. @default true
733 * mimeType: {string} Override the response mime type with the
735 * responseType: {string} Override the type of the "response"
738 * headers: {objects} Extra request headers.
740 * user: {string} The user name to send via HTTP Authentication.
741 * pass: {string} The password to send via HTTP Authentication.
743 * quiet: {boolean} If true, don't report errors.
745 * @returns {XMLHttpRequest}
747 httpGet: function httpGet(url, callback, self) {
748 let params = callback;
749 if (!isObject(params))
750 params = { callback: params && function () callback.apply(self, arguments) };
753 let xmlhttp = services.Xmlhttp();
754 xmlhttp.mozBackgroundRequest = Set.has(params, "background") ? params.background : true;
756 let async = params.callback || params.onload || params.onerror;
758 xmlhttp.addEventListener("load", function handler(event) { util.trapErrors(params.onload || params.callback, params, xmlhttp, event); }, false);
759 xmlhttp.addEventListener("error", function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event); }, false);
762 if (isObject(params.params)) {
763 let data = [encodeURIComponent(k) + "=" + encodeURIComponent(v)
764 for ([k, v] in iter(params.params))];
765 let uri = util.newURI(url);
766 uri.query += (uri.query ? "&" : "") + data.join("&");
771 if (isObject(params.data) && !(params.data instanceof Ci.nsISupports)) {
772 let data = services.FormData();
773 for (let [k, v] in iter(params.data))
779 xmlhttp.overrideMimeType(params.mimeType);
781 let args = [params.method || "GET", url, async];
782 if (params.user != null || params.pass != null)
783 args.push(params.user);
784 if (params.pass != null)
785 args.push(prams.pass);
786 xmlhttp.open.apply(xmlhttp, args);
788 for (let [header, val] in Iterator(params.headers || {}))
789 xmlhttp.setRequestHeader(header, val);
791 if (params.responseType)
792 xmlhttp.responseType = params.responseType;
794 if (params.notificationCallbacks)
795 xmlhttp.channel.notificationCallbacks = params.notificationCallbacks;
797 xmlhttp.send(params.data);
808 * The identity function.
813 identity: function identity(k) k,
816 * Returns the intersection of two rectangles.
822 intersection: function intersection(r1, r2) ({
823 get width() this.right - this.left,
824 get height() this.bottom - this.top,
825 left: Math.max(r1.left, r2.left),
826 right: Math.min(r1.right, r2.right),
827 top: Math.max(r1.top, r2.top),
828 bottom: Math.min(r1.bottom, r2.bottom)
832 * Returns true if the given stack frame resides in Dactyl code.
834 * @param {nsIStackFrame} frame
837 isDactyl: Class.Memoize(function () {
838 let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
839 let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
840 return function isDactyl(frame) re.test(frame.filename);
844 * Returns true if *url* is in the domain *domain*.
846 * @param {string} url
847 * @param {string} domain
850 isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
853 * Returns true if *host* is a subdomain of *domain*.
855 * @param {string} host The host to check.
856 * @param {string} domain The base domain to check the host against.
859 isSubdomain: function isSubdomain(host, domain) {
862 let idx = host.lastIndexOf(domain);
863 return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
867 * Iterates over all currently open documents, including all
868 * top-level window and sub-frames thereof.
870 iterDocuments: function iterDocuments(types) {
871 types = types ? types.map(function (s) "type" + util.capitalize(s))
872 : ["typeChrome", "typeContent"];
874 let windows = services.windowMediator.getXULWindowEnumerator(null);
875 while (windows.hasMoreElements()) {
876 let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
877 for each (let type in types) {
878 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
879 Ci.nsIDocShell.ENUMERATE_FORWARDS);
880 while (docShells.hasMoreElements())
881 let (viewer = docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer) {
883 yield viewer.DOMDocument;
889 // ripped from Firefox; modified
890 unsafeURI: Class.Memoize(function () util.regexp(String.replace(literal(/*
893 // Invisible characters (bug 452979)
894 U001C U001D U001E U001F // file/group/record/unit separator
898 U2062 U2063 // Invisible times/separator
899 U200B UFFFC // Zero-width space/no-break space
901 // Bidi formatting characters. (RFC 3987 sections 3.2 and 4.1 paragraph 6)
902 U200E U200F U202A U202B U202C U202D U202E
906 losslessDecodeURI: function losslessDecodeURI(url) {
907 return url.split("%25").map(function (url) {
908 // Non-UTF-8 compliant URLs cause "malformed URI sequence" errors.
910 return decodeURI(url).replace(this.unsafeURI, encodeURIComponent);
915 }, this).join("%25").replace(/[\s.,>)]$/, encodeURIComponent);
919 * Creates a DTD fragment from the given object. Each property of
920 * the object is converted to an ENTITY declaration. SGML special
921 * characters other than ' and % are left intact.
923 * @param {object} obj The object to convert.
924 * @returns {string} The DTD fragment containing entity declaration
927 makeDTD: let (map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" })
928 function makeDTD(obj) {
929 function escape(val) {
930 let isDOM = DOM.isJSONXML(val);
931 return String.replace(val == null ? "null" :
932 isDOM ? DOM.toXML(val)
936 function (m) map[m]);
939 return iter(obj).map(function ([k, v])
940 ["<!ENTITY ", k, " '", escape(v), "'>"].join(""))
945 * Converts a URI string into a URI object.
947 * @param {string} uri
950 newURI: function newURI(uri, charset, base) {
951 if (uri instanceof Ci.nsIURI)
952 var res = uri.clone();
954 let idx = uri.lastIndexOf(" -> ");
956 uri = uri.slice(idx + 4);
958 res = this.withProperErrors("newURI", services.io, uri, charset, base);
960 res instanceof Ci.nsIURL;
965 * Removes leading garbage prepended to URIs by the subscript
968 fixURI: function fixURI(url) String.replace(url, /.* -> /, ""),
971 * Pretty print a JavaScript object. Use HTML markup to color certain items
972 * if *color* is true.
974 * @param {Object} object The object to pretty print.
975 * @param {boolean} color Whether the output should be colored.
978 objectToString: function objectToString(object, color) {
980 return object + "\n";
982 if (!isObject(object))
983 return String(object);
985 if (object instanceof Ci.nsIDOMElement) {
987 if (elem.nodeType == elem.TEXT_NODE)
990 return DOM(elem).repr(color);
993 try { // for window.JSON
994 var obj = String(object);
997 obj = Object.prototype.toString.call(obj);
1001 obj = template.highlightFilter(util.clip(obj, 150), "\n",
1002 function () ["span", { highlight: "NonText" }, "^J"]);
1003 var head = ["span", { highlight: "Title Object" }, obj, "::\n"];
1006 head = util.clip(obj, 150).replace(/\n/g, "^J") + "::\n";
1010 // window.content often does not want to be queried with "var i in object"
1012 let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"]));
1013 if (object.dactyl && object.modules && object.modules.modules == object.modules) {
1014 object = Iterator(object);
1017 let keyIter = object;
1018 if ("__iterator__" in object && !callable(object.__iterator__))
1019 keyIter = keys(object);
1021 for (let i in keyIter) {
1022 let value = Magic("<no value>");
1028 if (isArray(i) && i.length == 2)
1039 else if (/^[A-Z_]+$/.test(i))
1043 value = template.highlight(value, true, 150, !color);
1044 else if (value instanceof Magic)
1045 value = String(value);
1047 value = util.clip(String(value).replace(/\n/g, "^J"), 150);
1052 val = [["span", { highlight: "Key" }, key], ": ", value];
1054 val = key + ": " + value;
1056 keys.push([i, val]);
1060 util.reportError(e);
1063 function compare(a, b) {
1064 if (!isNaN(a[0]) && !isNaN(b[0]))
1066 return String.localeCompare(a[0], b[0]);
1069 let vals = template.map(keys.sort(compare), function (f) f[1], "\n");
1071 return ["div", { style: "white-space: pre-wrap" }, head, vals];
1073 return head + vals.join("");
1076 prettifyJSON: function prettifyJSON(data, indent, invalidOK) {
1077 const INDENT = indent || " ";
1079 function rec(data, level, seen) {
1080 if (isObject(data)) {
1081 if (~seen.indexOf(data))
1082 throw Error("Recursive object passed");
1083 seen = seen.concat([data]);
1086 let prefix = level + INDENT;
1088 if (data === undefined)
1091 if (~["boolean", "number"].indexOf(typeof data) || data === null)
1092 res.push(String(data));
1093 else if (isinstance(data, ["String", _]))
1094 res.push(JSON.stringify(String(data)));
1095 else if (isArray(data)) {
1096 if (data.length == 0)
1100 for (let [i, val] in Iterator(data)) {
1104 rec(val, prefix, seen);
1106 res.push("\n", level, "]");
1109 else if (isObject(data)) {
1113 for (let [key, val] in Iterator(data)) {
1116 res.push(prefix, JSON.stringify(key), ": ");
1117 rec(val, prefix, seen);
1120 res.push("\n", level, "}");
1122 res[res.length - 1] = "{}";
1125 res.push({}.toString.call(data));
1127 throw Error("Invalid JSON object");
1132 return res.join("");
1136 "dactyl-cleanup-modules": function cleanupModules(subject, reason) {
1137 defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
1139 for (let module in values(defineModule.modules))
1140 if (module.cleanup) {
1141 util.dump("cleanup: " + module.constructor.className);
1142 util.trapErrors(module.cleanup, module, reason);
1145 JSMLoader.cleanup();
1147 if (!this.rehashing)
1148 services.observer.addObserver(this, "dactyl-rehash", true);
1150 "dactyl-rehash": function dactylRehash() {
1151 services.observer.removeObserver(this, "dactyl-rehash");
1153 defineModule.loadLog.push("dactyl: util: observe: dactyl-rehash");
1154 if (!this.rehashing)
1155 for (let module in values(defineModule.modules)) {
1156 defineModule.loadLog.push("dactyl: util: init(" + module + ")");
1163 "dactyl-purge": function dactylPurge() {
1169 * A generator that returns the values between *start* and *end*, in *step*
1172 * @param {number} start The interval's start value.
1173 * @param {number} end The interval's end value.
1174 * @param {boolean} step The value to step the range by. May be
1175 * negative. @default 1
1176 * @returns {Iterator(Object)}
1178 range: function range(start, end, step) {
1182 for (; start < end; start += step)
1187 yield start += step;
1192 * An interruptible generator that returns all values between *start* and
1193 * *end*. The thread yields every *time* milliseconds.
1195 * @param {number} start The interval's start value.
1196 * @param {number} end The interval's end value.
1197 * @param {number} time The time in milliseconds between thread yields.
1198 * @returns {Iterator(Object)}
1200 interruptibleRange: function interruptibleRange(start, end, time) {
1201 let endTime = Date.now() + time;
1202 while (start < end) {
1203 if (Date.now() > endTime) {
1204 util.threadYield(true, true);
1205 endTime = Date.now() + time;
1212 * Creates a new RegExp object based on the value of expr stripped
1213 * of all white space and interpolated with the values from tokens.
1214 * If tokens, any string in the form of <key> in expr is replaced
1215 * with the value of the property, 'key', from tokens, if that
1216 * property exists. If the property value is itself a RegExp, its
1217 * source is substituted rather than its string value.
1219 * Additionally, expr is stripped of all JavaScript comments.
1221 * This is similar to Perl's extended regular expression format.
1223 * @param {string|XML} expr The expression to compile into a RegExp.
1224 * @param {string} flags Flags to apply to the new RegExp.
1225 * @param {object} tokens The tokens to substitute. @optional
1226 * @returns {RegExp} A custom regexp object.
1228 regexp: update(function (expr, flags, tokens) {
1229 flags = flags || [k for ([k, v] in Iterator({ g: "global", i: "ignorecase", m: "multiline", y: "sticky" }))
1230 if (expr[v])].join("");
1232 if (isinstance(expr, ["RegExp"]))
1235 expr = String.replace(expr, /\\(.)/, function (m, m1) {
1237 flags = flags.replace(/i/g, "") + "i";
1238 else if (m1 === "C")
1239 flags = flags.replace(/i/g, "");
1245 // Replace replacement <tokens>.
1247 expr = String.replace(expr, /(\(?P)?<(\w+)>/g,
1248 function (m, n1, n2) !n1 && Set.has(tokens, n2) ? tokens[n2].dactylSource
1249 || tokens[n2].source
1253 // Strip comments and white space.
1254 if (/x/.test(flags))
1255 expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm, function (m, m1) m1 || "");
1257 // Replace (?P<named> parameters)
1258 if (/\(\?P</.test(expr)) {
1260 let groups = ["wholeMatch"];
1261 expr = expr.replace(/((?:[^[(\\]|\\.|\[(?:[^\]]|\\.)*\])*)\((?:\?P<([^>]+)>|(\?))?/gy,
1262 function (m0, m1, m2, m3) {
1264 groups.push(m2 || "-group-" + groups.length);
1265 return m1 + "(" + (m3 || "");
1267 var struct = Struct.apply(null, groups);
1270 let res = update(RegExp(expr, flags.replace("x", "")), {
1271 closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),
1272 dactylPropertyNames: ["exec", "match", "test", "toSource", "toString", "global", "ignoreCase", "lastIndex", "multiLine", "source", "sticky"],
1273 iterate: function iterate(str, idx) util.regexp.iterate(this, str, idx)
1276 // Return a struct with properties for named parameters if we
1280 exec: function exec() let (match = exec.superapply(this, arguments)) match && struct.fromArray(match),
1281 dactylSource: source, struct: struct
1286 * Escapes Regular Expression special characters in *str*.
1288 * @param {string} str
1291 escape: function regexp_escape(str) str.replace(/([\\{}()[\]^$.?*+|])/g, "\\$1"),
1294 * Given a RegExp, returns its source in the form showable to the user.
1296 * @param {RegExp} re The regexp showable source of which is to be returned.
1299 getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g, function (m0, m1) m1 === "/" ? "/" : m0),
1302 * Iterates over all matches of the given regexp in the given
1305 * @param {RegExp} regexp The regular expression to execute.
1306 * @param {string} string The string to search.
1307 * @param {number} lastIndex The index at which to begin searching. @optional
1309 iterate: function iterate(regexp, string, lastIndex) iter(function () {
1310 regexp.lastIndex = lastIndex = lastIndex || 0;
1312 while (match = regexp.exec(string)) {
1313 lastIndex = regexp.lastIndex;
1315 regexp.lastIndex = lastIndex;
1316 if (match[0].length == 0 || !regexp.global)
1323 * Flushes the startup or jar cache.
1325 flushCache: function flushCache(file) {
1327 services.observer.notifyObservers(file, "flush-cache-entry", "");
1329 services.observer.notifyObservers(null, "startupcache-invalidate", "");
1333 * Reloads dactyl in entirety by disabling the add-on and
1336 rehash: function rehash(args) {
1337 storage.storeForSession("commandlineArgs", args);
1338 this.timeout(function () {
1340 this.rehashing = true;
1341 let addon = config.addon;
1342 addon.userDisabled = true;
1343 addon.userDisabled = false;
1348 errors: Class.Memoize(function () []),
1351 * Reports an error to the Error Console and the standard output,
1352 * along with a stack trace and other relevant information. The
1353 * error is appended to {@see #errors}.
1355 reportError: function reportError(error) {
1359 if (isString(error))
1360 error = Error(error);
1362 Cu.reportError(error);
1367 let obj = update({}, error, {
1368 toString: function () String(error),
1369 stack: Magic(util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t"))
1372 services.console.logStringMessage(obj.stack);
1374 this.errors.push([new Date, obj + "\n" + obj.stack]);
1375 this.errors = this.errors.slice(-this.maxErrors);
1376 this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
1378 this.dump(String(error));
1384 this.dump(String(error));
1385 this.dump(util.stackLines(error.stack).join("\n"));
1387 catch (e) { dump(e + "\n"); }
1390 // ctypes.open("libc.so.6").declare("kill", ctypes.default_abi, ctypes.void_t, ctypes.int, ctypes.int)(
1391 // ctypes.open("libc.so.6").declare("getpid", ctypes.default_abi, ctypes.int)(), 2)
1395 * Given a domain, returns an array of all non-toplevel subdomains
1398 * @param {string} host The host for which to find subdomains.
1399 * @returns {[string]}
1401 subdomains: function subdomains(host) {
1402 if (/(^|\.)\d+$|:.*:/.test(host))
1403 // IP address or similar
1406 let base = host.replace(/.*\.(.+?\..+?)$/, "$1");
1408 base = services.tld.getBaseDomainFromHost(host);
1412 let ary = host.split(".");
1413 ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))];
1414 return ary.filter(function (h) h.length >= base.length);
1418 * Returns the selection controller for the given window.
1420 * @param {Window} window
1421 * @returns {nsISelectionController}
1423 selectionController: function selectionController(win)
1424 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1425 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
1426 .QueryInterface(Ci.nsISelectionController),
1429 * Escapes a string against shell meta-characters and argument
1432 shellEscape: function shellEscape(str) '"' + String.replace(str, /[\\"$`]/g, "\\$&") + '"',
1435 * Suspend execution for at least *delay* milliseconds. Functions by
1436 * yielding execution to the next item in the main event queue, and
1437 * so may lead to unexpected call graphs, and long delays if another
1438 * handler yields execution while waiting.
1440 * @param {number} delay The time period for which to sleep in milliseconds.
1442 sleep: function sleep(delay) {
1443 let mainThread = services.threading.mainThread;
1445 let end = Date.now() + delay;
1446 while (Date.now() < end)
1447 mainThread.processNextEvent(true);
1452 * Behaves like String.split, except that when *limit* is reached,
1453 * the trailing element contains the entire trailing portion of the
1456 * util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"]
1458 * @param {string} str The string to split.
1459 * @param {RegExp|string} re The regular expression on which to split the string.
1460 * @param {number} limit The maximum number of elements to return.
1461 * @returns {[string]}
1463 split: function split(str, re, limit) {
1466 re = RegExp(re.source || re, "g");
1467 let match, start = 0, res = [];
1468 while (--limit && (match = re.exec(str)) && match[0].length) {
1469 res.push(str.substring(start, match.index));
1470 start = match.index + match[0].length;
1472 res.push(str.substring(start));
1477 * Split a string on literal occurrences of a marker.
1479 * Specifically this ignores occurrences preceded by a backslash, or
1480 * contained within 'single' or "double" quotes.
1482 * It assumes backslash escaping on strings, and will thus not count quotes
1483 * that are preceded by a backslash or within other quotes as starting or
1484 * ending quoted sections of the string.
1486 * @param {string} str
1487 * @param {RegExp} marker
1488 * @returns {[string]}
1490 splitLiteral: function splitLiteral(str, marker) {
1492 let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source);
1497 str = str.replace(resep, function (match, before) {
1498 results.push(before);
1499 cont = match !== "";
1510 * Yields execution to the next event in the current thread's event
1511 * queue. This is a potentially dangerous operation, since any
1512 * yielders higher in the event stack will prevent execution from
1513 * returning to the caller until they have finished their wait. The
1514 * potential for deadlock is high.
1516 * @param {boolean} flush If true, flush all events in the event
1517 * queue before returning. Otherwise, wait for an event to
1518 * process before proceeding.
1519 * @param {boolean} interruptable If true, this yield may be
1520 * interrupted by pressing <C-c>, in which case,
1521 * Error("Interrupted") will be thrown.
1523 threadYield: function threadYield(flush, interruptable) {
1526 let mainThread = services.threading.mainThread;
1528 util.interrupted = false;
1530 mainThread.processNextEvent(!flush);
1531 if (util.interrupted)
1532 throw Error("Interrupted");
1534 while (flush === true && mainThread.hasPendingEvents());
1542 * Waits for the function *test* to return true, or *timeout*
1543 * milliseconds to expire.
1545 * @param {function} test The predicate on which to wait.
1546 * @param {object} self The 'this' object for *test*.
1547 * @param {Number} timeout The maximum number of milliseconds to
1550 * @param {boolean} interruptable If true, may be interrupted by
1551 * pressing <C-c>, in which case, Error("Interrupted") will be
1554 waitFor: function waitFor(test, self, timeout, interruptable) {
1555 let end = timeout && Date.now() + timeout, result;
1557 let timer = services.Timer(function () {}, 10, services.Timer.TYPE_REPEATING_SLACK);
1559 while (!(result = test.call(self)) && (!end || Date.now() < end))
1560 this.threadYield(false, interruptable);
1569 * Makes the passed function yieldable. Each time the function calls
1570 * yield, execution is suspended for the yielded number of
1574 * let func = yieldable(function () {
1575 * util.dump(Date.now()); // 0
1577 * util.dump(Date.now()); // 1500
1581 * @param {function} func The function to mangle.
1582 * @returns {function} A new function which may not execute
1585 yieldable: function yieldable(func)
1587 let gen = func.apply(this, arguments);
1590 util.timeout(next, gen.next());
1592 catch (e if e instanceof StopIteration) {};
1597 * Wraps a callback function such that its errors are not lost. This
1598 * is useful for DOM event listeners, which ordinarily eat errors.
1599 * The passed function has the property *wrapper* set to the new
1600 * wrapper function, while the wrapper has the property *wrapped*
1601 * set to the original callback.
1603 * @param {function} callback The callback to wrap.
1604 * @returns {function}
1606 wrapCallback: wrapCallback,
1609 * Returns the top-level chrome window for the given window.
1611 * @param {Window} win The child window.
1612 * @returns {Window} The top-level parent window.
1614 topWindow: function topWindow(win)
1615 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1616 .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
1617 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow),
1620 * Traps errors in the called function, possibly reporting them.
1622 * @param {function} func The function to call
1623 * @param {object} self The 'this' object for the function.
1625 trapErrors: function trapErrors(func, self, ...args) {
1627 if (!callable(func))
1629 return func.apply(self || this, args);
1632 this.reportError(e);
1638 * Returns the file path of a given *url*, for debugging purposes.
1639 * If *url* points to a file (even if indirectly), the native
1640 * filesystem path is returned. Otherwise, the URL itself is
1643 * @param {string} url The URL to mangle.
1644 * @returns {string} The path to the file.
1646 urlPath: function urlPath(url) {
1648 return util.getFile(url).path;
1656 * Returns a list of all domains and subdomains of documents in the
1657 * given window and all of its descendant frames.
1659 * @param {nsIDOMWindow} win The window for which to find domains.
1660 * @returns {[string]} The visible domains.
1662 visibleHosts: function visibleHosts(win) {
1663 let res = [], seen = {};
1664 (function rec(frame) {
1666 if (frame.location.hostname)
1667 res = res.concat(util.subdomains(frame.location.hostname));
1670 Array.forEach(frame.frames, rec);
1672 return res.filter(function (h) !Set.add(seen, h));
1676 * Returns a list of URIs of documents in the given window and all
1677 * of its descendant frames.
1679 * @param {nsIDOMWindow} win The window for which to find URIs.
1680 * @returns {[nsIURI]} The visible URIs.
1682 visibleURIs: function visibleURIs(win) {
1683 let res = [], seen = {};
1684 (function rec(frame) {
1686 res = res.concat(util.newURI(frame.location.href));
1689 Array.forEach(frame.frames, rec);
1691 return res.filter(function (h) !Set.add(seen, h.spec));
1695 * Like Cu.getWeakReference, but won't crash if you pass null.
1697 weakReference: function weakReference(jsval) {
1699 return { get: function get() null };
1700 return Cu.getWeakReference(jsval);
1704 * Wraps native exceptions thrown by the called function so that a
1705 * proper stack trace may be retrieved from them.
1707 * @param {function|string} meth The method to call.
1708 * @param {object} self The 'this' object of the method.
1709 * @param ... Arguments to pass to *meth*.
1711 withProperErrors: function withProperErrors(meth, self, ...args) {
1713 return (callable(meth) ? meth : self[meth]).apply(self, args);
1716 throw e.stack ? e : Error(e);
1724 * Math utility methods.
1727 var GlobalMath = Math;
1728 this.Math = update(Object.create(GlobalMath), {
1730 * Returns the specified *value* constrained to the range *min* - *max*.
1732 * @param {number} value The value to constrain.
1733 * @param {number} min The minimum constraint.
1734 * @param {number} max The maximum constraint.
1737 constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max)
1742 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1744 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: