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-2014 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", "promises", "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.bound.overlayObject }),
76 overlayWindow: deprecated("overlay.overlayWindow", { get: function overlayWindow() overlay.bound.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 }),
93 map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
94 writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
95 readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
97 chromePackages: deprecated("config.chromePackages", { get: function chromePackages() config.chromePackages }),
98 haveGecko: deprecated("config.haveGecko", { get: function haveGecko() config.bound.haveGecko }),
99 OS: deprecated("config.OS", { get: function OS() config.OS }),
101 dactyl: update(function dactyl(obj) {
103 var global = Class.objectGlobal(obj);
106 __noSuchMethod__: function __noSuchMethod__(meth, args) {
107 let win = overlay.activeWindow;
109 var dactyl = global && global.dactyl || win && win.dactyl;
113 let prop = dactyl[meth];
115 return prop.apply(dactyl, args);
120 __noSuchMethod__: function __noSuchMethod__() this().__noSuchMethod__.apply(null, arguments)
124 * Registers a obj as a new observer with the observer service. obj.observe
125 * must be an object where each key is the name of a target to observe and
126 * each value is a function(subject, data) to be called when the given
127 * target is broadcast. obj.observe will be replaced with a new opaque
128 * function. The observer is automatically unregistered on application
131 * @param {object} obj
133 addObserver: update(function addObserver(obj) {
135 obj.observers = obj.observe;
137 let cleanup = ["dactyl-cleanup-modules", "quit-application"];
139 function register(meth) {
140 for (let target of RealSet(cleanup.concat(Object.keys(obj.observers))))
142 services.observer[meth](obj, target, true);
147 Class.replaceProperty(obj, "observe",
148 function (subject, target, data) {
150 if (~cleanup.indexOf(target))
151 register("removeObserver");
152 if (obj.observers[target])
153 obj.observers[target].call(obj, subject, data);
156 if (typeof util === "undefined")
157 addObserver.dump("dactyl: error: " + e + "\n" + (e.stack || addObserver.Error().stack).replace(/^/gm, "dactyl: "));
163 obj.observe.unregister = () => register("removeObserver");
164 register("addObserver");
165 }, { dump: dump, Error: Error }),
168 * Tests a condition and throws a FailedAssertion error on
171 * @param {boolean} condition The condition to test.
172 * @param {string} message The message to present to the
175 assert: function assert(condition, message, quiet) {
177 throw FailedAssertion(message, 1, quiet === undefined ? true : quiet);
182 * CamelCases a -non-camel-cased identifier name.
184 * @param {string} name The name to mangle.
185 * @returns {string} The mangled name.
187 camelCase: function camelCase(name) String.replace(name, /-(.)/g,
188 (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(e => callable(e) ? e(obj) : e)
271 valid: function valid(obj) this.elements.every(e => !e.test || e.test(obj))
275 for (let match in util.regexp.iterate(/(.*?)%(.)/gy, format)) {
277 let [, prefix, char] = match;
278 end += match[0].length;
281 stack.top.elements.push(prefix);
283 stack.top.elements.push("%");
284 else if (char === "[") {
286 stack.top.elements.push(f);
289 else if (char === "]") {
291 util.assert(stack.length, /*L*/"Unmatched %] in format");
294 let quote = function quote(obj, char) obj[char];
295 if (char !== char.toLowerCase())
296 quote = function quote(obj, char) Commands.quote(obj[char]);
297 char = char.toLowerCase();
299 stack.top.elements.push(update(
300 function (obj) obj[char] != null ? quote(obj, char)
302 { test: function test(obj) obj[char] != null }));
304 for (let elem in array.iterValues(stack))
305 elem.seen[char] = true;
308 if (end < format.length)
309 stack.top.elements.push(format.substr(end));
311 util.assert(stack.length === 1, /*L*/"Unmatched %[ in format");
316 * Compiles a macro string into a function which generates a string
317 * result based on the input *macro* and its parameters. The
318 * definitive documentation for macro strings resides in :help
321 * Macro parameters may have any of the following flags:
322 * e: The parameter is only tested for existence. Its
323 * interpolation is always empty.
324 * q: The result is quoted such that it is parsed as a single
325 * argument by the Ex argument parser.
327 * The returned function has the following additional properties:
329 * seen {set}: The set of parameters used in this macro.
331 * valid {function(object)}: Returns true if every parameter of
332 * this macro is provided by the passed object.
334 * @param {string} macro The macro string to compile.
335 * @param {boolean} keepUnknown If true, unknown macro parameters
336 * are left untouched. Otherwise, they are replaced with the null
338 * @returns {function}
340 compileMacro: function compileMacro(macro, keepUnknown) {
341 let stack = [frame()];
342 stack.__defineGetter__("top", function () this[this.length - 1]);
344 let unknown = util.identity;
348 function frame() update(
350 _frame === stack.top || _frame.valid(obj)
351 ? _frame.elements.map(e => callable(e) ? e(obj) : e)
357 valid: function valid(obj) this.elements.every(e => (!e.test || e.test(obj)))
360 let defaults = { lt: "<", gt: ">" };
362 let re = util.regexp(literal(/*
366 (< ((?:[a-z]-)?[a-z-]+?) (?:\[([0-9]+)\])? >) | // 3 4 5
370 macro = String(macro);
372 for (let match in re.iterate(macro)) {
373 let [, prefix, open, full, macro, idx, close] = match;
374 end += match[0].length;
377 stack.top.elements.push(prefix);
380 stack.top.elements.push(f);
385 util.assert(stack.length, /*L*/"Unmatched }> in macro");
388 let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro);
389 flags = RealSet(flags);
391 let quote = util.identity;
393 quote = function quote(obj) typeof obj === "number" ? obj : String.quote(obj);
395 quote = function quote(obj) "";
397 if (hasOwnProperty(defaults, name))
398 stack.top.elements.push(quote(defaults[name]));
402 idx = Number(idx) - 1;
403 stack.top.elements.push(update(
404 obj => obj[name] != null && idx in obj[name] ? quote(obj[name][idx])
405 : hasOwnProperty(obj, name) ? "" : unknown(full),
407 test: function test(obj) obj[name] != null && idx in obj[name]
408 && obj[name][idx] !== false
409 && (!flags.e || obj[name][idx] != "")
413 stack.top.elements.push(update(
414 obj => obj[name] != null ? quote(obj[name])
415 : hasOwnProperty(obj, name) ? "" : unknown(full),
417 test: function test(obj) obj[name] != null
418 && obj[name] !== false
419 && (!flags.e || obj[name] != "")
423 for (let elem in array.iterValues(stack))
428 if (end < macro.length)
429 stack.top.elements.push(macro.substr(end));
431 util.assert(stack.length === 1, /*L*/"Unmatched <{ in macro");
436 * Converts any arbitrary string into an URI object. Returns null on
439 * @param {string} str
440 * @returns {nsIURI|null}
442 createURI: function createURI(str) {
444 let uri = services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
445 uri instanceof Ci.nsIURL;
454 * Expands brace globbing patterns in a string.
457 * "a{b,c}d" => ["abd", "acd"]
459 * @param {string|[string|Array]} pattern The pattern to deglob.
460 * @returns [string] The resulting strings.
462 debrace: function debrace(pattern) {
464 if (isArray(pattern)) {
465 // Jägermonkey hates us.
468 rec: function rec(acc) {
471 while (isString(vals = pattern[acc.length]))
474 if (acc.length == pattern.length)
475 this.res.push(acc.join(""));
477 for (let val in values(vals))
478 this.rec(acc.concat(val));
485 if (!pattern.contains("{"))
490 let split = function split(pattern, re, fn, dequote) {
491 let end = 0, match, res = [];
492 while (match = re.exec(pattern)) {
493 end = match.index + match[0].length;
498 res.push(pattern.substr(end));
499 return res.map(s => util.dequote(s, dequote));
503 let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
505 patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
509 let rec = function rec(acc) {
510 if (acc.length == patterns.length)
511 res.push(array(substrings).zip(acc).flatten().join(""));
513 for (let [, pattern] in Iterator(patterns[acc.length]))
514 rec(acc.concat(pattern));
519 catch (e if e.message && e.message.contains("res is undefined")) {
520 // prefs.safeSet() would be reset on :rehash
521 prefs.set("javascript.options.methodjit.chrome", false);
522 util.dactyl.warn(_(UTF8("error.damnYouJägermonkey")));
528 * Briefly delay the execution of the passed function.
530 * @param {function} callback The function to delay.
532 delay: function delay(callback) {
533 let { mainThread } = services.threading;
534 mainThread.dispatch(callback,
535 mainThread.DISPATCH_NORMAL);
539 * Removes certain backslash-quoted characters while leaving other
540 * backslash-quoting sequences untouched.
542 * @param {string} pattern The string to unquote.
543 * @param {string} chars The characters to unquote.
546 dequote: function dequote(pattern, chars)
547 pattern.replace(/\\(.)/, (m0, m1) => chars.contains(m1) ? m1 : m0),
550 * Returns the nsIDocShell for the given window.
552 * @param {Window} win The window for which to get the docShell.
553 * @returns {nsIDocShell}
556 docShell: function docShell(win)
557 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
558 .QueryInterface(Ci.nsIDocShell),
561 * Prints a message to the console. If *msg* is an object it is pretty
564 * @param {string|Object} msg The message to print.
566 dump: defineModule.dump,
569 * Returns a list of reformatted stack frames from
570 * {@see Error#stack}.
572 * @param {string} stack The stack trace from an Error.
573 * @returns {[string]} The stack frames.
575 stackLines: function stackLines(stack) {
577 let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
578 while (match = re.exec(stack))
579 lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + "@" +
580 util.fixURI(match[2]));
585 * Dumps a stack trace to the console.
587 * @param {string} msg The trace message.
588 * @param {number} frames The number of frames to print.
590 dumpStack: function dumpStack(msg="Stack", frames=null) {
591 let stack = util.stackLines(Error().stack);
592 stack = stack.slice(1, 1 + (frames || stack.length)).join("\n").replace(/^/gm, " ");
593 util.dump(msg + "\n" + stack + "\n");
597 * Escapes quotes, newline and tab characters in *str*. The returned string
598 * is delimited by *delimiter* or " if *delimiter* is not specified.
599 * {@see String#quote}.
601 * @param {string} str
602 * @param {string} delimiter
605 escapeString: function escapeString(str, delimiter) {
606 if (delimiter == undefined)
608 return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
612 * Converts *bytes* to a pretty printed data size string.
614 * @param {number} bytes The number of bytes.
615 * @param {string} decimalPlaces The number of decimal places to use if
616 * *humanReadable* is true.
617 * @param {boolean} humanReadable Use byte multiples.
620 formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) {
621 const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
623 let tmpNum = parseInt(bytes, 10) || 0;
624 let strNum = [tmpNum + ""];
627 while (tmpNum >= 1024) {
629 if (++unitIndex > (unitVal.length - 1))
633 let decPower = Math.pow(10, decimalPlaces);
634 strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2);
639 while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces)
643 for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000
644 strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u);
646 if (unitIndex) // decimalPlaces only when > Bytes
647 strNum[0] += "." + strNum[1];
649 return strNum[0] + " " + unitVal[unitIndex];
653 * Converts *seconds* into a human readable time string.
655 * @param {number} seconds
658 formatSeconds: function formatSeconds(seconds) {
659 function pad(n, val) ("0000000" + val).substr(-Math.max(n, String(val).length));
660 function div(num, denom) [Math.floor(num / denom), Math.round(num % denom)];
661 let days, hours, minutes;
663 [minutes, seconds] = div(Math.round(seconds), 60);
664 [hours, minutes] = div(minutes, 60);
665 [days, hours] = div(hours, 24);
667 return /*L*/days + " days " + hours + " hours";
669 return /*L*/hours + "h " + minutes + "m";
671 return /*L*/minutes + ":" + pad(2, seconds);
672 return /*L*/seconds + "s";
676 * Returns the file which backs a given URL, if available.
678 * @param {nsIURI} uri The URI for which to find a file.
679 * @returns {File|null}
681 getFile: function getFile(uri) {
684 uri = util.newURI(uri);
686 if (uri instanceof Ci.nsIFileURL)
687 return File(uri.file);
689 if (uri instanceof Ci.nsIFile)
692 let channel = services.io.newChannelFromURI(uri);
693 try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
694 if (channel instanceof Ci.nsIFileChannel)
695 return File(channel.file);
702 * Returns the host for the given URL, or null if invalid.
704 * @param {string} url
705 * @returns {string|null}
707 getHost: function getHost(url) {
709 return util.createURI(url).host;
716 * Sends a synchronous or asynchronous HTTP request to *url* and returns
717 * the XMLHttpRequest object. If *callback* is specified the request is
718 * asynchronous and the *callback* is invoked with the object as its
721 * @param {string} url
722 * @param {object} params Optional parameters for this request:
723 * method: {string} The request method. @default "GET"
725 * params: {object} Parameters to append to *url*'s query string.
726 * data: {*} POST data to send to the server. Ordinary objects
727 * are converted to FormData objects, with one datum
728 * for each property/value pair.
730 * onload: {function(XMLHttpRequest, Event)} The request's load event handler.
731 * onerror: {function(XMLHttpRequest, Event)} The request's error event handler.
732 * callback: {function(XMLHttpRequest, Event)} An event handler
733 * called for either error or load events.
735 * background: {boolean} Whether to perform the request in the
736 * background. @default true
738 * mimeType: {string} Override the response mime type with the
740 * responseType: {string} Override the type of the "response"
743 * headers: {objects} Extra request headers.
745 * user: {string} The user name to send via HTTP Authentication.
746 * pass: {string} The password to send via HTTP Authentication.
748 * quiet: {boolean} If true, don't report errors.
750 * @returns {XMLHttpRequest}
752 httpGet: function httpGet(url, params={}, self) {
753 if (callable(params))
755 params = { callback: params.bind(self) };
758 let xmlhttp = services.Xmlhttp();
759 xmlhttp.mozBackgroundRequest = hasOwnProperty(params, "background") ? params.background : true;
761 let async = params.callback || params.onload || params.onerror;
763 xmlhttp.addEventListener("load", event => { util.trapErrors(params.onload || params.callback, params, xmlhttp, event); }, false);
764 xmlhttp.addEventListener("error", event => { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event); }, false);
767 if (isObject(params.params)) {
768 let data = [encodeURIComponent(k) + "=" + encodeURIComponent(v)
769 for ([k, v] in iter(params.params))];
770 let uri = util.newURI(url);
771 uri.query += (uri.query ? "&" : "") + data.join("&");
776 if (isObject(params.data) && !(params.data instanceof Ci.nsISupports)) {
777 let data = services.FormData();
778 for (let [k, v] in iter(params.data))
784 xmlhttp.overrideMimeType(params.mimeType);
786 let args = [params.method || "GET", url, async];
787 if (params.user != null || params.pass != null)
788 args.push(params.user);
789 if (params.pass != null)
790 args.push(prams.pass);
791 xmlhttp.open.apply(xmlhttp, args);
793 for (let [header, val] in Iterator(params.headers || {}))
794 xmlhttp.setRequestHeader(header, val);
796 if (params.responseType)
797 xmlhttp.responseType = params.responseType;
799 if (params.notificationCallbacks)
800 xmlhttp.channel.notificationCallbacks = params.notificationCallbacks;
802 xmlhttp.send(params.data);
813 * Like #httpGet, but returns a promise rather than accepting
816 * @param {string} url The URL to fetch.
817 * @param {object} params Parameter object, as in #httpGet.
819 fetchUrl: promises.withCallbacks(function fetchUrl([accept, reject, deferred], url, params) {
820 params = update({}, params);
821 params.onload = accept;
822 params.onerror = reject;
824 let req = this.httpGet(url, params);
825 promises.oncancel(deferred, req.cancel);
829 * The identity function.
834 identity: function identity(k) k,
837 * Returns the intersection of two rectangles.
843 intersection: function intersection(r1, r2) ({
844 get width() this.right - this.left,
845 get height() this.bottom - this.top,
846 left: Math.max(r1.left, r2.left),
847 right: Math.min(r1.right, r2.right),
848 top: Math.max(r1.top, r2.top),
849 bottom: Math.min(r1.bottom, r2.bottom)
853 * Returns true if the given stack frame resides in Dactyl code.
855 * @param {nsIStackFrame} frame
858 isDactyl: Class.Memoize(function () {
859 let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
860 let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
861 return function isDactyl(frame) re.test(frame.filename);
865 * Returns true if *url* is in the domain *domain*.
867 * @param {string} url
868 * @param {string} domain
871 isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
874 * Returns true if *host* is a subdomain of *domain*.
876 * @param {string} host The host to check.
877 * @param {string} domain The base domain to check the host against.
880 isSubdomain: function isSubdomain(host, domain) {
883 let idx = host.lastIndexOf(domain);
884 return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
888 * Iterates over all currently open documents, including all
889 * top-level window and sub-frames thereof.
891 iterDocuments: function iterDocuments(types) {
892 types = types ? types.map(s => "type" + util.capitalize(s))
893 : ["typeChrome", "typeContent"];
895 let windows = services.windowMediator.getXULWindowEnumerator(null);
896 while (windows.hasMoreElements()) {
897 let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
898 for (let type of types) {
899 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
900 Ci.nsIDocShell.ENUMERATE_FORWARDS);
901 while (docShells.hasMoreElements())
902 let (viewer = docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer) {
904 yield viewer.DOMDocument;
910 // ripped from Firefox; modified
911 unsafeURI: Class.Memoize(() => util.regexp(String.replace(literal(/*
914 // Invisible characters (bug 452979)
915 U001C U001D U001E U001F // file/group/record/unit separator
919 U2062 U2063 // Invisible times/separator
920 U200B UFFFC // Zero-width space/no-break space
922 // Bidi formatting characters. (RFC 3987 sections 3.2 and 4.1 paragraph 6)
923 U200E U200F U202A U202B U202C U202D U202E
927 losslessDecodeURI: function losslessDecodeURI(url) {
928 return url.split("%25").map(function (url) {
929 // Non-UTF-8 compliant URLs cause "malformed URI sequence" errors.
931 return decodeURI(url).replace(this.unsafeURI, encodeURIComponent);
936 }, this).join("%25").replace(/[\s.,>)]$/, encodeURIComponent);
940 * Creates a DTD fragment from the given object. Each property of
941 * the object is converted to an ENTITY declaration. SGML special
942 * characters other than ' and % are left intact.
944 * @param {object} obj The object to convert.
945 * @returns {string} The DTD fragment containing entity declaration
948 makeDTD: let (map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" })
949 function makeDTD(obj) {
950 function escape(val) {
951 let isDOM = DOM.isJSONXML(val);
952 return String.replace(val == null ? "null" :
953 isDOM ? DOM.toXML(val)
960 return iter(obj).map(([k, v]) =>
961 ["<!ENTITY ", k, " '", escape(v), "'>"].join(""))
966 * Converts a URI string into a URI object.
968 * @param {string} uri
971 newURI: function newURI(uri, charset, base) {
972 if (uri instanceof Ci.nsIURI)
973 var res = uri.clone();
975 let idx = uri.lastIndexOf(" -> ");
977 uri = uri.slice(idx + 4);
979 res = this.withProperErrors("newURI", services.io, uri, charset, base);
981 res instanceof Ci.nsIURL;
986 * Removes leading garbage prepended to URIs by the subscript
989 fixURI: function fixURI(url) String.replace(url, /.* -> /, ""),
992 * Pretty print a JavaScript object. Use HTML markup to color certain items
993 * if *color* is true.
995 * @param {Object} object The object to pretty print.
996 * @param {boolean} color Whether the output should be colored.
999 objectToString: function objectToString(object, color) {
1001 return object + "\n";
1003 if (!isObject(object))
1004 return String(object);
1006 if (object instanceof Ci.nsIDOMElement) {
1008 if (elem.nodeType == elem.TEXT_NODE)
1011 return DOM(elem).repr(color);
1014 try { // for window.JSON
1015 var obj = String(object);
1018 obj = Object.prototype.toString.call(obj);
1022 obj = template.highlightFilter(util.clip(obj, 150), "\n",
1023 () => ["span", { highlight: "NonText" },
1026 var head = ["span", { highlight: "Title Object" }, obj, "::\n"];
1029 head = util.clip(obj, 150).replace(/\n/g, "^J") + "::\n";
1033 // window.content often does not want to be queried with "var i in object"
1035 let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"]));
1037 if (object.dactyl && object.modules && object.modules.modules == object.modules) {
1038 object = Iterator(object);
1042 let keyIter = object;
1043 if (iter.iteratorProp in object) {
1044 keyIter = (k for (k of object));
1047 else if ("__iterator__" in object && !callable(object.__iterator__))
1048 keyIter = keys(object);
1050 for (let i in keyIter) {
1051 let value = Magic("<no value>");
1058 if (isArray(i) && i.length == 2)
1069 else if (/^[A-Z_]+$/.test(i))
1073 value = template.highlight(value, true, 150, !color);
1074 else if (value instanceof Magic)
1075 value = String(value);
1077 value = util.clip(String(value).replace(/\n/g, "^J"), 150);
1082 val = [["span", { highlight: "Key" }, key], ": ", value];
1084 val = key + ": " + value;
1086 keys.push([i, val]);
1090 util.reportError(e);
1093 function compare(a, b) {
1094 if (!isNaN(a[0]) && !isNaN(b[0]))
1096 return String.localeCompare(a[0], b[0]);
1099 let vals = template.map(keys.sort(compare), f => f[1],
1103 return ["div", { style: "white-space: pre-wrap" }, head, vals];
1105 return head + vals.join("");
1108 prettifyJSON: function prettifyJSON(data, indent, invalidOK) {
1109 const INDENT = indent || " ";
1111 function rec(data, level, seen) {
1112 if (isObject(data)) {
1113 seen = RealSet(seen);
1115 throw Error("Recursive object passed");
1118 let prefix = level + INDENT;
1120 if (data === undefined)
1123 if (~["boolean", "number"].indexOf(typeof data) || data === null)
1124 res.push(String(data));
1125 else if (isinstance(data, ["String", _]))
1126 res.push(JSON.stringify(String(data)));
1127 else if (isArray(data)) {
1128 if (data.length == 0)
1132 for (let [i, val] in Iterator(data)) {
1136 rec(val, prefix, seen);
1138 res.push("\n", level, "]");
1141 else if (isObject(data)) {
1145 for (let [key, val] in Iterator(data)) {
1148 res.push(prefix, JSON.stringify(key), ": ");
1149 rec(val, prefix, seen);
1152 res.push("\n", level, "}");
1154 res[res.length - 1] = "{}";
1157 res.push({}.toString.call(data));
1159 throw Error("Invalid JSON object");
1163 rec(data, "", RealSet());
1164 return res.join("");
1168 "dactyl-cleanup-modules": function cleanupModules(subject, reason) {
1169 defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
1171 for (let module in values(defineModule.modules))
1172 if (module.cleanup) {
1173 util.dump("cleanup: " + module.constructor.className);
1174 util.trapErrors(module.cleanup, module, reason);
1180 * A generator that returns the values between *start* and *end*, in *step*
1183 * @param {number} start The interval's start value.
1184 * @param {number} end The interval's end value.
1185 * @param {boolean} step The value to step the range by. May be
1186 * negative. @default 1
1187 * @returns {Iterator(Object)}
1189 range: function range(start, end, step) {
1193 for (; start < end; start += step)
1198 yield start += step;
1203 * An interruptible generator that returns all values between *start* and
1204 * *end*. The thread yields every *time* milliseconds.
1206 * @param {number} start The interval's start value.
1207 * @param {number} end The interval's end value.
1208 * @param {number} time The time in milliseconds between thread yields.
1209 * @returns {Iterator(Object)}
1211 interruptibleRange: function interruptibleRange(start, end, time) {
1212 let endTime = Date.now() + time;
1213 while (start < end) {
1214 if (Date.now() > endTime) {
1215 util.threadYield(true, true);
1216 endTime = Date.now() + time;
1223 * Creates a new RegExp object based on the value of expr stripped
1224 * of all white space and interpolated with the values from tokens.
1225 * If tokens, any string in the form of <key> in expr is replaced
1226 * with the value of the property, 'key', from tokens, if that
1227 * property exists. If the property value is itself a RegExp, its
1228 * source is substituted rather than its string value.
1230 * Additionally, expr is stripped of all JavaScript comments.
1232 * This is similar to Perl's extended regular expression format.
1234 * @param {string} expr The expression to compile into a RegExp.
1235 * @param {string} flags Flags to apply to the new RegExp.
1236 * @param {object} tokens The tokens to substitute. @optional
1237 * @returns {RegExp} A custom regexp object.
1239 regexp: update(function (expr, flags, tokens) {
1240 flags = flags || [k for ([k, v] in Iterator({ g: "global", i: "ignorecase", m: "multiline", y: "sticky" }))
1241 if (expr[v])].join("");
1243 if (isinstance(expr, ["RegExp"]))
1246 expr = String.replace(expr, /\\(.)/, function (m, m1) {
1248 flags = flags.replace(/i/g, "") + "i";
1249 else if (m1 === "C")
1250 flags = flags.replace(/i/g, "");
1256 // Replace replacement <tokens>.
1258 expr = String.replace(expr, /(\(?P)?<(\w+)>/g,
1259 (m, n1, n2) => !n1 && hasOwnProperty(tokens, n2) ? tokens[n2].dactylSource
1260 || tokens[n2].source
1264 // Strip comments and white space.
1265 if (/x/.test(flags))
1266 expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm,
1267 (m, m1) => m1 || "");
1269 // Replace (?P<named> parameters)
1270 if (/\(\?P</.test(expr)) {
1272 let groups = ["wholeMatch"];
1273 expr = expr.replace(/((?:[^[(\\]|\\.|\[(?:[^\]]|\\.)*\])*)\((?:\?P<([^>]+)>|(\?))?/gy,
1274 function (m0, m1, m2, m3) {
1276 groups.push(m2 || "-group-" + groups.length);
1277 return m1 + "(" + (m3 || "");
1279 var struct = Struct.apply(null, groups);
1282 let res = update(RegExp(expr, flags.replace("x", "")), {
1283 bound: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "bound")),
1284 closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "bound")),
1285 dactylPropertyNames: ["exec", "match", "test", "toSource", "toString", "global", "ignoreCase", "lastIndex", "multiLine", "source", "sticky"],
1286 iterate: function iterate(str, idx) util.regexp.iterate(this, str, idx)
1289 // Return a struct with properties for named parameters if we
1293 exec: function exec() let (match = exec.superapply(this, arguments)) match && struct.fromArray(match),
1294 dactylSource: source, struct: struct
1299 * Escapes Regular Expression special characters in *str*.
1301 * @param {string} str
1304 escape: function regexp_escape(str) str.replace(/([\\{}()[\]^$.?*+|])/g, "\\$1"),
1307 * Given a RegExp, returns its source in the form showable to the user.
1309 * @param {RegExp} re The regexp showable source of which is to be returned.
1312 getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g,
1313 (m0, m1) => m1 === "/" ? m1
1317 * Iterates over all matches of the given regexp in the given
1320 * @param {RegExp} regexp The regular expression to execute.
1321 * @param {string} string The string to search.
1322 * @param {number} lastIndex The index at which to begin searching. @optional
1324 iterate: function iterate(regexp, string, lastIndex) iter(function () {
1325 regexp.lastIndex = lastIndex = lastIndex || 0;
1327 while (match = regexp.exec(string)) {
1328 lastIndex = regexp.lastIndex;
1330 regexp.lastIndex = lastIndex;
1331 if (match[0].length == 0 || !regexp.global)
1338 * Flushes the startup or jar cache.
1340 flushCache: function flushCache(file) {
1342 services.observer.notifyObservers(file, "flush-cache-entry", "");
1344 services.observer.notifyObservers(null, "startupcache-invalidate", "");
1348 * Reloads dactyl in entirety by disabling the add-on and
1351 rehash: function rehash(args) {
1352 storage.storeForSession("commandlineArgs", args);
1353 this.timeout(function () {
1355 cache.flush(bind("test", /^literal:/));
1356 let addon = config.addon;
1357 addon.userDisabled = true;
1358 addon.userDisabled = false;
1363 errors: Class.Memoize(() => []),
1366 * Reports an error to the Error Console and the standard output,
1367 * along with a stack trace and other relevant information. The
1368 * error is appended to {@see #errors}.
1370 reportError: function reportError(error) {
1374 if (isString(error))
1375 error = Error(error);
1377 Cu.reportError(error);
1382 let obj = update({}, error, {
1383 toString: function () String(error),
1384 stack: Magic(util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t"))
1387 services.console.logStringMessage(obj.stack);
1389 this.errors.push([new Date, obj + "\n" + obj.stack]);
1390 this.errors = this.errors.slice(-this.maxErrors);
1391 this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
1393 this.dump(String(error));
1399 this.dump(String(error));
1400 this.dump(util.stackLines(error.stack).join("\n"));
1402 catch (e) { dump(e + "\n"); }
1405 // ctypes.open("libc.so.6").declare("kill", ctypes.default_abi, ctypes.void_t, ctypes.int, ctypes.int)(
1406 // ctypes.open("libc.so.6").declare("getpid", ctypes.default_abi, ctypes.int)(), 2)
1410 * Given a domain, returns an array of all non-toplevel subdomains
1413 * @param {string} host The host for which to find subdomains.
1414 * @returns {[string]}
1416 subdomains: function subdomains(host) {
1417 if (/(^|\.)\d+$|:.*:/.test(host))
1418 // IP address or similar
1421 let base = host.replace(/.*\.(.+?\..+?)$/, "$1");
1423 base = services.tld.getBaseDomainFromHost(host);
1427 let ary = host.split(".");
1428 ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))];
1429 return ary.filter(h => h.length >= base.length);
1433 * Returns the selection controller for the given window.
1435 * @param {Window} window
1436 * @returns {nsISelectionController}
1438 selectionController: function selectionController(win)
1439 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1440 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
1441 .QueryInterface(Ci.nsISelectionController),
1444 * Escapes a string against shell meta-characters and argument
1447 shellEscape: function shellEscape(str) '"' + String.replace(str, /[\\"$`]/g, "\\$&") + '"',
1450 * Suspend execution for at least *delay* milliseconds. Functions by
1451 * yielding execution to the next item in the main event queue, and
1452 * so may lead to unexpected call graphs, and long delays if another
1453 * handler yields execution while waiting.
1455 * @param {number} delay The time period for which to sleep in milliseconds.
1457 sleep: function sleep(delay) {
1458 let mainThread = services.threading.mainThread;
1460 let end = Date.now() + delay;
1461 while (Date.now() < end)
1462 mainThread.processNextEvent(true);
1467 * Behaves like String.split, except that when *limit* is reached,
1468 * the trailing element contains the entire trailing portion of the
1471 * util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"]
1473 * @param {string} str The string to split.
1474 * @param {RegExp|string} re The regular expression on which to split the string.
1475 * @param {number} limit The maximum number of elements to return.
1476 * @returns {[string]}
1478 split: function split(str, re, limit) {
1481 re = RegExp(re.source || re, "g");
1482 let match, start = 0, res = [];
1483 while (--limit && (match = re.exec(str)) && match[0].length) {
1484 res.push(str.substring(start, match.index));
1485 start = match.index + match[0].length;
1487 res.push(str.substring(start));
1492 * Split a string on literal occurrences of a marker.
1494 * Specifically this ignores occurrences preceded by a backslash, or
1495 * contained within 'single' or "double" quotes.
1497 * It assumes backslash escaping on strings, and will thus not count quotes
1498 * that are preceded by a backslash or within other quotes as starting or
1499 * ending quoted sections of the string.
1501 * @param {string} str
1502 * @param {RegExp} marker
1503 * @returns {[string]}
1505 splitLiteral: function splitLiteral(str, marker) {
1507 let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source);
1512 str = str.replace(resep, function (match, before) {
1513 results.push(before);
1514 cont = match !== "";
1525 * Yields execution to the next event in the current thread's event
1526 * queue. This is a potentially dangerous operation, since any
1527 * yielders higher in the event stack will prevent execution from
1528 * returning to the caller until they have finished their wait. The
1529 * potential for deadlock is high.
1531 * @param {boolean} flush If true, flush all events in the event
1532 * queue before returning. Otherwise, wait for an event to
1533 * process before proceeding.
1534 * @param {boolean} interruptable If true, this yield may be
1535 * interrupted by pressing <C-c>, in which case,
1536 * Error("Interrupted") will be thrown.
1538 threadYield: function threadYield(flush, interruptable) {
1541 let mainThread = services.threading.mainThread;
1543 util.interrupted = false;
1545 mainThread.processNextEvent(!flush);
1546 if (util.interrupted)
1547 throw Error("Interrupted");
1549 while (flush === true && mainThread.hasPendingEvents());
1557 * Waits for the function *test* to return true, or *timeout*
1558 * milliseconds to expire.
1560 * @param {function|Promise} test The predicate on which to wait.
1561 * @param {object} self The 'this' object for *test*.
1562 * @param {Number} timeout The maximum number of milliseconds to
1565 * @param {boolean} interruptable If true, may be interrupted by
1566 * pressing <C-c>, in which case, Error("Interrupted") will be
1569 waitFor: function waitFor(test, self, timeout, interruptable) {
1570 if (!callable(test)) {
1574 promise.then((arg) => { retVal = arg; done = true; },
1575 (arg) => { retVal = arg; done = true; });
1579 let end = timeout && Date.now() + timeout, result;
1581 let timer = services.Timer(function () {}, 10, services.Timer.TYPE_REPEATING_SLACK);
1583 while (!(result = test.call(self)) && (!end || Date.now() < end))
1584 this.threadYield(false, interruptable);
1589 return promise ? retVal: result;
1593 * Makes the passed function yieldable. Each time the function calls
1594 * yield, execution is suspended for the yielded number of
1598 * let func = yieldable(function () {
1599 * util.dump(Date.now()); // 0
1601 * util.dump(Date.now()); // 1500
1605 * @param {function} func The function to mangle.
1606 * @returns {function} A new function which may not execute
1609 yieldable: deprecated("Task.spawn", function yieldable(func)
1611 let gen = func.apply(this, arguments);
1614 util.timeout(next, gen.next());
1616 catch (e if e instanceof StopIteration) {};
1621 * Wraps a callback function such that its errors are not lost. This
1622 * is useful for DOM event listeners, which ordinarily eat errors.
1623 * The passed function has the property *wrapper* set to the new
1624 * wrapper function, while the wrapper has the property *wrapped*
1625 * set to the original callback.
1627 * @param {function} callback The callback to wrap.
1628 * @returns {function}
1630 wrapCallback: wrapCallback,
1633 * Returns the top-level chrome window for the given window.
1635 * @param {Window} win The child window.
1636 * @returns {Window} The top-level parent window.
1638 topWindow: function topWindow(win)
1639 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1640 .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
1641 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow),
1644 * Traps errors in the called function, possibly reporting them.
1646 * @param {function} func The function to call
1647 * @param {object} self The 'this' object for the function.
1649 trapErrors: function trapErrors(func, self, ...args) {
1651 if (!callable(func))
1653 return func.apply(self || this, args);
1656 this.reportError(e);
1662 * Returns the file path of a given *url*, for debugging purposes.
1663 * If *url* points to a file (even if indirectly), the native
1664 * filesystem path is returned. Otherwise, the URL itself is
1667 * @param {string} url The URL to mangle.
1668 * @returns {string} The path to the file.
1670 urlPath: function urlPath(url) {
1672 return util.getFile(url).path;
1680 * Returns a list of all domains and subdomains of documents in the
1681 * given window and all of its descendant frames.
1683 * @param {nsIDOMWindow} win The window for which to find domains.
1684 * @returns {[string]} The visible domains.
1686 visibleHosts: function visibleHosts(win) {
1689 (function rec(frame) {
1691 if (frame.location.hostname)
1692 res = res.concat(util.subdomains(frame.location.hostname));
1695 Array.forEach(frame.frames, rec);
1697 return res.filter(h => !seen.add(h));
1701 * Returns a list of URIs of documents in the given window and all
1702 * of its descendant frames.
1704 * @param {nsIDOMWindow} win The window for which to find URIs.
1705 * @returns {[nsIURI]} The visible URIs.
1707 visibleURIs: function visibleURIs(win) {
1710 (function rec(frame) {
1712 res = res.concat(util.newURI(frame.location.href));
1715 Array.forEach(frame.frames, rec);
1717 return res.filter(h => !seen.add(h.spec));
1721 * Like Cu.getWeakReference, but won't crash if you pass null.
1723 weakReference: function weakReference(jsval) {
1725 return { get: function get() null };
1726 return Cu.getWeakReference(jsval);
1730 * Wraps native exceptions thrown by the called function so that a
1731 * proper stack trace may be retrieved from them.
1733 * @param {function|string} meth The method to call.
1734 * @param {object} self The 'this' object of the method.
1735 * @param ... Arguments to pass to *meth*.
1737 withProperErrors: function withProperErrors(meth, self, ...args) {
1739 return (callable(meth) ? meth : self[meth]).apply(self, args);
1742 throw e.stack ? e : Error(e);
1750 * Math utility methods.
1753 var GlobalMath = Math;
1754 this.Math = update(Object.create(GlobalMath), {
1756 * Returns the specified *value* constrained to the range *min* - *max*.
1758 * @param {number} value The value to constrain.
1759 * @param {number} min The minimum constraint.
1760 * @param {number} max The maximum constraint.
1763 constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max)
1768 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1770 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: