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-2011 by 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 Components.utils.import("resource://dactyl/bootstrap.jsm");
13 defineModule("util", {
14 exports: ["frag", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
15 require: ["services"],
16 use: ["commands", "config", "highlight", "messages", "storage", "template"]
19 var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
20 var XHTML = Namespace("html", "http://www.w3.org/1999/xhtml");
21 var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
22 var NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator");
23 default xml namespace = XHTML;
25 var FailedAssertion = Class("FailedAssertion", ErrorBase, {
26 init: function init(message, level, noTrace) {
27 if (noTrace !== undefined)
28 this.noTrace = noTrace;
29 init.supercall(this, message, level);
37 var Point = Struct("x", "y");
39 var wrapCallback = function wrapCallback(fn) {
40 fn.wrapper = function wrappedCallback () {
42 return fn.apply(this, arguments);
49 fn.wrapper.wrapped = fn;
53 var getAttr = function getAttr(elem, ns, name)
54 elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
55 var setAttr = function setAttr(elem, ns, name, val) {
57 elem.removeAttributeNS(ns, name);
59 elem.setAttributeNS(ns, name, val);
62 var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
66 this.addObserver(this);
70 cleanup: function cleanup() {
71 for (let { document: doc } in iter(services.windowMediator.getEnumerator(null))) {
72 for (let elem in values(doc.dactylOverlayElements || []))
74 elem.parentNode.removeChild(elem);
76 for (let [elem, ns, name, orig, value] in values(doc.dactylOverlayAttributes || []))
77 if (getAttr(elem, ns, name) === value)
78 setAttr(elem, ns, name, orig);
80 delete doc.dactylOverlayElements;
81 delete doc.dactylOverlayAttributes;
82 delete doc.dactylOverlays;
86 // FIXME: Only works for Pentadactyl
87 get activeWindow() services.windowMediator.getMostRecentWindow("navigator:browser"),
88 dactyl: update(function dactyl(obj) {
90 var global = Class.objectGlobal(obj);
92 __noSuchMethod__: function (meth, args) {
93 let win = util.activeWindow;
94 var dactyl = global && global.dactyl || win && win.dactyl;
98 let prop = dactyl[meth];
100 return prop.apply(dactyl, args);
105 __noSuchMethod__: function () this().__noSuchMethod__.apply(null, arguments)
109 * Registers a obj as a new observer with the observer service. obj.observe
110 * must be an object where each key is the name of a target to observe and
111 * each value is a function(subject, data) to be called when the given
112 * target is broadcast. obj.observe will be replaced with a new opaque
113 * function. The observer is automatically unregistered on application
116 * @param {object} obj
118 addObserver: update(function addObserver(obj) {
120 obj.observers = obj.observe;
122 function register(meth) {
123 for (let target in Set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers))))
125 services.observer[meth](obj, target, true);
130 Class.replaceProperty(obj, "observe",
131 function (subject, target, data) {
133 if (target == "quit-application" || target == "dactyl-cleanup-modules")
134 register("removeObserver");
135 if (obj.observers[target])
136 obj.observers[target].call(obj, subject, data);
139 if (typeof util === "undefined")
140 addObserver.dump("dactyl: error: " + e + "\n" + (e.stack || addObserver.Error().stack).replace(/^/gm, "dactyl: "));
146 obj.observe.unregister = function () register("removeObserver");
147 register("addObserver");
148 }, { dump: dump, Error: Error }),
151 * Tests a condition and throws a FailedAssertion error on
154 * @param {boolean} condition The condition to test.
155 * @param {string} message The message to present to the
158 assert: function (condition, message, quiet) {
160 throw FailedAssertion(message, 1, quiet === undefined ? true : quiet);
165 * Capitalizes the first character of the given string.
166 * @param {string} str The string to capitalize
169 capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1).toLowerCase(),
172 * Returns a RegExp object that matches characters specified in the range
173 * expression *list*, or signals an appropriate error if *list* is invalid.
175 * @param {string} list Character list, e.g., "a b d-xA-Z" produces /[abd-xA-Z]/.
176 * @param {string} accepted Character range(s) to accept, e.g. "a-zA-Z" for
177 * ASCII letters. Used to validate *list*.
180 charListToRegexp: function charListToRegexp(list, accepted) {
181 list = list.replace(/\s+/g, "");
183 // check for chars not in the accepted range
184 this.assert(RegExp("^[" + accepted + "-]+$").test(list),
185 _("error.charactersOutsideRange", accepted.quote()));
187 // check for illegal ranges
188 for (let [match] in this.regexp.iterate(/.-./g, list))
189 this.assert(match.charCodeAt(0) <= match.charCodeAt(2),
190 _("error.invalidCharacterRange", list.slice(list.indexOf(match))));
192 return RegExp("[" + util.regexp.escape(list) + "]");
195 get chromePackages() {
198 function process(manifest) {
199 for each (let line in manifest.split(/\n+/)) {
200 let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
202 res[match[2]] = true;
205 function processJar(file) {
206 let jar = services.ZipReader(file);
208 if (jar.hasEntry("chrome.manifest"))
209 process(File.readStream(jar.getInputStream("chrome.manifest")));
214 for each (let dir in ["UChrm", "AChrom"]) {
215 dir = File(services.directory.get(dir, Ci.nsIFile));
216 if (dir.exists() && dir.isDirectory())
217 for (let file in dir.iterDirectory())
218 if (/\.manifest$/.test(file.leafName))
219 process(file.read());
221 dir = File(dir.parent);
222 if (dir.exists() && dir.isDirectory())
223 for (let file in dir.iterDirectory())
224 if (/\.jar$/.test(file.leafName))
227 dir = dir.child("extensions");
228 if (dir.exists() && dir.isDirectory())
229 for (let ext in dir.iterDirectory()) {
230 if (/\.xpi$/.test(ext.leafName))
234 ext = File(ext.read().replace(/\n*$/, ""));
235 let mf = ext.child("chrome.manifest");
241 return Object.keys(res).sort();
245 * Returns a shallow copy of *obj*.
247 * @param {Object} obj
250 cloneObject: function cloneObject(obj) {
254 for (let [k, v] in Iterator(obj))
260 * Clips a string to a given length. If the input string is longer
261 * than *length*, an ellipsis is appended.
263 * @param {string} str The string to truncate.
264 * @param {number} length The length of the returned string.
267 clip: function clip(str, length) {
268 return str.length <= length ? str : str.substr(0, length - 3) + "...";
272 * Compares two strings, case insensitively. Return values are as
273 * in String#localeCompare.
279 compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()),
281 compileFormat: function compileFormat(format) {
282 let stack = [frame()];
283 stack.__defineGetter__("top", function () this[this.length - 1]);
285 function frame() update(
287 _frame === stack.top || _frame.valid(obj) ?
288 _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
292 valid: function (obj) this.elements.every(function (e) !e.test || e.test(obj))
296 for (let match in util.regexp.iterate(/(.*?)%(.)/gy, format)) {
298 let [, prefix, char] = match;
299 end += match[0].length;
302 stack.top.elements.push(prefix);
304 stack.top.elements.push("%");
305 else if (char === "[") {
307 stack.top.elements.push(f);
310 else if (char === "]") {
312 util.assert(stack.length, /*L*/"Unmatched %] in format");
315 let quote = function quote(obj, char) obj[char];
316 if (char !== char.toLowerCase())
317 quote = function quote(obj, char) Commands.quote(obj[char]);
318 char = char.toLowerCase();
320 stack.top.elements.push(update(
321 function (obj) obj[char] != null ? quote(obj, char) : "",
322 { test: function (obj) obj[char] != null }));
324 for (let elem in array.iterValues(stack))
325 elem.seen[char] = true;
328 if (end < format.length)
329 stack.top.elements.push(format.substr(end));
331 util.assert(stack.length === 1, /*L*/"Unmatched %[ in format");
336 * Compiles a macro string into a function which generates a string
337 * result based on the input *macro* and its parameters. The
338 * definitive documentation for macro strings resides in :help
341 * Macro parameters may have any of the following flags:
342 * e: The parameter is only tested for existence. Its
343 * interpolation is always empty.
344 * q: The result is quoted such that it is parsed as a single
345 * argument by the Ex argument parser.
347 * The returned function has the following additional properties:
349 * seen {set}: The set of parameters used in this macro.
351 * valid {function(object)}: Returns true if every parameter of
352 * this macro is provided by the passed object.
354 * @param {string} macro The macro string to compile.
355 * @param {boolean} keepUnknown If true, unknown macro parameters
356 * are left untouched. Otherwise, they are replaced with the null
358 * @returns {function}
360 compileMacro: function compileMacro(macro, keepUnknown) {
361 let stack = [frame()];
362 stack.__defineGetter__("top", function () this[this.length - 1]);
364 let unknown = util.identity;
366 unknown = function () "";
368 function frame() update(
370 _frame === stack.top || _frame.valid(obj) ?
371 _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
375 valid: function (obj) this.elements.every(function (e) !e.test || e.test(obj))
378 let defaults = { lt: "<", gt: ">" };
380 let re = util.regexp(<![CDATA[
384 (< ((?:[a-z]-)?[a-z-]+?) (?:\[([0-9]+)\])? >) | // 3 4 5
388 macro = String(macro);
390 for (let match in re.iterate(macro)) {
391 let [, prefix, open, full, macro, idx, close] = match;
392 end += match[0].length;
395 stack.top.elements.push(prefix);
398 stack.top.elements.push(f);
403 util.assert(stack.length, /*L*/"Unmatched %] in macro");
406 let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro);
409 let quote = util.identity;
411 quote = function quote(obj) typeof obj === "number" ? obj : String.quote(obj);
413 quote = function quote(obj) "";
415 if (Set.has(defaults, name))
416 stack.top.elements.push(quote(defaults[name]));
419 idx = Number(idx) - 1;
420 stack.top.elements.push(update(
421 function (obj) obj[name] != null && idx in obj[name] ? quote(obj[name][idx]) : Set.has(obj, name) ? "" : unknown(full),
422 { test: function (obj) obj[name] != null && idx in obj[name] && obj[name][idx] !== false && (!flags.e || obj[name][idx] != "") }));
425 stack.top.elements.push(update(
426 function (obj) obj[name] != null ? quote(obj[name]) : Set.has(obj, name) ? "" : unknown(full),
427 { test: function (obj) obj[name] != null && obj[name] !== false && (!flags.e || obj[name] != "") }));
430 for (let elem in array.iterValues(stack))
431 elem.seen[name] = true;
435 if (end < macro.length)
436 stack.top.elements.push(macro.substr(end));
438 util.assert(stack.length === 1, /*L*/"Unmatched <{ in macro");
443 * Compiles a CSS spec and XPath pattern matcher based on the given
444 * list. List elements prefixed with "xpath:" are parsed as XPath
445 * patterns, while other elements are parsed as CSS specs. The
446 * returned function will, given a node, return an iterator of all
447 * descendants of that node which match the given specs.
449 * @param {[string]} list The list of patterns to match.
450 * @returns {function(Node)}
452 compileMatcher: function compileMatcher(list) {
453 let xpath = [], css = [];
454 for (let elem in values(list))
455 if (/^xpath:/.test(elem))
456 xpath.push(elem.substr(6));
461 function matcher(node) {
463 for (let elem in util.evaluateXPath(matcher.xpath, node))
467 for (let [, elem] in iter(node.querySelectorAll(matcher.css)))
471 xpath: xpath.join(" | ")
476 * Validates a list as input for {@link #compileMatcher}. Returns
477 * true if and only if every element of the list is a valid XPath or
480 * @param {[string]} list The list of patterns to test
481 * @returns {boolean} True when the patterns are all valid.
483 validateMatcher: function validateMatcher(list) {
484 let evaluator = services.XPathEvaluator();
485 let node = services.XMLDocument();
486 return this.testValues(list, function (value) {
487 if (/^xpath:/.test(value))
488 evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver);
490 node.querySelector(value);
496 * Returns an object representing a Node's computed CSS style.
501 computedStyle: function computedStyle(node) {
502 while (!(node instanceof Ci.nsIDOMElement) && node.parentNode)
503 node = node.parentNode;
505 var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
509 util.dumpStack(_("error.nullComputedStyle", node));
510 Cu.reportError(Error(_("error.nullComputedStyle", node)));
517 * Converts any arbitrary string into an URI object. Returns null on
520 * @param {string} str
521 * @returns {nsIURI|null}
523 createURI: function createURI(str) {
525 return services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
533 * Expands brace globbing patterns in a string.
536 * "a{b,c}d" => ["abd", "acd"]
538 * @param {string|[string|Array]} pattern The pattern to deglob.
539 * @returns [string] The resulting strings.
541 debrace: function debrace(pattern) {
542 if (isArray(pattern)) {
544 let rec = function rec(acc) {
547 while (isString(vals = pattern[acc.length]))
550 if (acc.length == pattern.length)
551 res.push(acc.join(""))
553 for (let val in values(vals))
554 rec(acc.concat(val));
560 if (pattern.indexOf("{") == -1)
563 function split(pattern, re, fn, dequote) {
564 let end = 0, match, res = [];
565 while (match = re.exec(pattern)) {
566 end = match.index + match[0].length;
571 res.push(pattern.substr(end));
572 return res.map(function (s) util.dequote(s, dequote));
575 let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
577 patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
583 if (acc.length == patterns.length)
584 res.push(array(substrings).zip(acc).flatten().join(""));
586 for (let [, pattern] in Iterator(patterns[acc.length]))
587 rec(acc.concat(pattern));
594 * Removes certain backslash-quoted characters while leaving other
595 * backslash-quoting sequences untouched.
597 * @param {string} pattern The string to unquote.
598 * @param {string} chars The characters to unquote.
601 dequote: function dequote(pattern, chars)
602 pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0),
605 * Converts a given DOM Node, Range, or Selection to a string. If
606 * *html* is true, the output is HTML, otherwise it is presentation
609 * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
611 * @param {boolean} html Whether the output should be HTML rather
612 * than presentation text.
614 domToString: function (node, html) {
615 if (node instanceof Ci.nsISelection && node.isCollapsed)
618 if (node instanceof Ci.nsIDOMNode) {
619 let range = node.ownerDocument.createRange();
620 range.selectNode(node);
623 let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
624 doc = doc.ownerDocument || doc;
626 let encoder = services.HtmlEncoder();
627 encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
628 if (node instanceof Ci.nsISelection)
629 encoder.setSelection(node);
630 else if (node instanceof Ci.nsIDOMRange)
631 encoder.setRange(node);
633 let str = services.String(encoder.encodeToString());
637 let [result, length] = [{}, {}];
638 services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
639 return result.value.QueryInterface(Ci.nsISupportsString).data;
643 * Prints a message to the console. If *msg* is an object it is pretty
646 * @param {string|Object} msg The message to print.
648 dump: defineModule.dump,
651 * Returns a list of reformatted stack frames from
652 * {@see Error#stack}.
654 * @param {string} stack The stack trace from an Error.
655 * @returns {[string]} The stack frames.
657 stackLines: function (stack) {
659 let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
660 while (match = re.exec(stack))
661 lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + "@" +
662 util.fixURI(match[2]));
667 * Dumps a stack trace to the console.
669 * @param {string} msg The trace message.
670 * @param {number} frames The number of frames to print.
672 dumpStack: function dumpStack(msg, frames) {
673 let stack = util.stackLines(Error().stack);
674 stack = stack.slice(1, 1 + (frames || stack.length)).join("\n").replace(/^/gm, " ");
675 util.dump((arguments.length == 0 ? "Stack" : msg) + "\n" + stack + "\n");
679 * The set of input element type attribute values that mark the element as
682 editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
683 "month", "number", "password", "range", "search",
684 "tel", "text", "time", "url", "week"]),
687 * Converts HTML special characters in *str* to the equivalent HTML
690 * @param {string} str
693 escapeHTML: function escapeHTML(str) {
694 return str.replace(/&/g, "&").replace(/</g, "<");
698 * Escapes quotes, newline and tab characters in *str*. The returned string
699 * is delimited by *delimiter* or " if *delimiter* is not specified.
700 * {@see String#quote}.
702 * @param {string} str
703 * @param {string} delimiter
706 escapeString: function escapeString(str, delimiter) {
707 if (delimiter == undefined)
709 return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
713 * Evaluates an XPath expression in the current or provided
714 * document. It provides the xhtml, xhtml2 and dactyl XML
715 * namespaces. The result may be used as an iterator.
717 * @param {string} expression The XPath expression to evaluate.
718 * @param {Node} elem The context element.
719 * @default The current document.
720 * @param {boolean} asIterator Whether to return the results as an
722 * @returns {Object} Iterable result of the evaluation.
724 evaluateXPath: update(
725 function evaluateXPath(expression, elem, asIterator) {
728 elem = util.activeWindow.content.document;
729 let doc = elem.ownerDocument || elem;
730 if (isArray(expression))
731 expression = util.makeXPath(expression);
733 let result = doc.evaluate(expression, elem,
734 evaluateXPath.resolver,
735 asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
739 return Object.create(result, {
741 value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
742 : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
747 throw e.stack ? e : Error(e);
751 resolver: function lookupNamespaceURI(prefix) ({
754 xhtml2: "http://www.w3.org/2002/06/xhtml2",
759 extend: function extend(dest) {
760 Array.slice(arguments, 1).filter(util.identity).forEach(function (src) {
761 for (let [k, v] in Iterator(src)) {
762 let get = src.__lookupGetter__(k),
763 set = src.__lookupSetter__(k);
767 dest.__defineGetter__(k, get);
769 dest.__defineSetter__(k, set);
776 * Converts *bytes* to a pretty printed data size string.
778 * @param {number} bytes The number of bytes.
779 * @param {string} decimalPlaces The number of decimal places to use if
780 * *humanReadable* is true.
781 * @param {boolean} humanReadable Use byte multiples.
784 formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) {
785 const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
787 let tmpNum = parseInt(bytes, 10) || 0;
788 let strNum = [tmpNum + ""];
791 while (tmpNum >= 1024) {
793 if (++unitIndex > (unitVal.length - 1))
797 let decPower = Math.pow(10, decimalPlaces);
798 strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2);
803 while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces)
807 for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000
808 strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u);
810 if (unitIndex) // decimalPlaces only when > Bytes
811 strNum[0] += "." + strNum[1];
813 return strNum[0] + " " + unitVal[unitIndex];
817 * Converts *seconds* into a human readable time string.
819 * @param {number} seconds
822 formatSeconds: function formatSeconds(seconds) {
823 function pad(n, val) ("0000000" + val).substr(-Math.max(n, String(val).length));
824 function div(num, denom) [Math.round(num / denom), Math.round(num % denom)];
825 let days, hours, minutes;
827 [minutes, seconds] = div(seconds, 60);
828 [hours, minutes] = div(minutes, 60);
829 [days, hours] = div(hours, 24);
831 return /*L*/days + " days " + hours + " hours"
833 return /*L*/hours + "h " + minutes + "m";
835 return /*L*/minutes + ":" + pad(2, seconds);
836 return /*L*/seconds + "s";
840 * Returns the file which backs a given URL, if available.
842 * @param {nsIURI} uri The URI for which to find a file.
843 * @returns {File|null}
845 getFile: function getFile(uri) {
848 uri = util.newURI(util.fixURI(uri));
850 if (uri instanceof Ci.nsIFileURL)
851 return File(uri.file);
853 let channel = services.io.newChannelFromURI(uri);
854 channel.cancel(Cr.NS_BINDING_ABORTED);
855 if (channel instanceof Ci.nsIFileChannel)
856 return File(channel.file);
863 * Returns the host for the given URL, or null if invalid.
865 * @param {string} url
866 * @returns {string|null}
868 getHost: function (url) {
870 return util.createURI(url).host;
877 * Returns true if the current Gecko runtime is of the given version
880 * @param {string} ver The required version.
883 haveGecko: function (ver) services.versionCompare.compare(services.runtime.platformVersion, ver) >= 0,
886 * Sends a synchronous or asynchronous HTTP request to *url* and returns
887 * the XMLHttpRequest object. If *callback* is specified the request is
888 * asynchronous and the *callback* is invoked with the object as its
891 * @param {string} url
892 * @param {function(XMLHttpRequest)} callback
893 * @returns {XMLHttpRequest}
895 httpGet: function httpGet(url, callback, self) {
896 let params = callback;
897 if (!isObject(params))
898 params = { callback: params && function () callback.apply(self, arguments) };
901 let xmlhttp = services.Xmlhttp();
902 xmlhttp.mozBackgroundRequest = true;
904 let async = params.callback || params.onload || params.onerror;
906 xmlhttp.onload = function handler(event) { util.trapErrors(params.onload || params.callback, params, xmlhttp, event) };
907 xmlhttp.onerror = function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event) };
910 xmlhttp.overrideMimeType(params.mimeType);
912 xmlhttp.open(params.method || "GET", url, async,
913 params.user, params.pass);
919 util.dactyl.log(_("error.cantOpen", String.quote(url), e), 1);
925 * The identity function.
930 identity: function identity(k) k,
933 * Returns the intersection of two rectangles.
939 intersection: function (r1, r2) ({
940 get width() this.right - this.left,
941 get height() this.bottom - this.top,
942 left: Math.max(r1.left, r2.left),
943 right: Math.min(r1.right, r2.right),
944 top: Math.max(r1.top, r2.top),
945 bottom: Math.min(r1.bottom, r2.bottom)
949 * Returns true if the given stack frame resides in Dactyl code.
951 * @param {nsIStackFrame} frame
954 isDactyl: Class.memoize(function () {
955 let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
956 let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
957 return function isDactyl(frame) re.test(frame.filename);
961 * Returns true if *url* is in the domain *domain*.
963 * @param {string} url
964 * @param {string} domain
967 isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
969 /** Dactyl's notion of the current operating system platform. */
971 _arch: services.runtime.OS,
973 * @property {string} The normalised name of the OS. This is one of
974 * "Windows", "Mac OS X" or "Unix".
976 get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
977 /** @property {boolean} True if the OS is Windows. */
978 get isWindows() this._arch == "WINNT",
979 /** @property {boolean} True if the OS is Mac OS X. */
980 get isMacOSX() this._arch == "Darwin",
981 /** @property {boolean} True if the OS is some other *nix variant. */
982 get isUnix() !this.isWindows && !this.isMacOSX,
983 /** @property {RegExp} A RegExp which matches illegal characters in path components. */
984 get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g
988 * Returns true if *host* is a subdomain of *domain*.
990 * @param {string} host The host to check.
991 * @param {string} domain The base domain to check the host against.
994 isSubdomain: function isSubdomain(host, domain) {
997 let idx = host.lastIndexOf(domain);
998 return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
1002 * Returns true if the given DOM node is currently visible.
1004 * @param {Node} node
1005 * @returns {boolean}
1007 isVisible: function (node) {
1008 let style = util.computedStyle(node);
1009 return style.visibility == "visible" && style.display != "none";
1013 * Iterates over all currently open documents, including all
1014 * top-level window and sub-frames thereof.
1016 iterDocuments: function iterDocuments() {
1017 let windows = services.windowMediator.getXULWindowEnumerator(null);
1018 while (windows.hasMoreElements()) {
1019 let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
1020 for each (let type in ["typeChrome", "typeContent"]) {
1021 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
1022 Ci.nsIDocShell.ENUMERATE_FORWARDS);
1023 while (docShells.hasMoreElements())
1024 let (viewer = docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer) {
1026 yield viewer.DOMDocument;
1032 // ripped from Firefox; modified
1033 unsafeURI: Class.memoize(function () util.regexp(String.replace(<![CDATA[
1036 // Invisible characters (bug 452979)
1037 U001C U001D U001E U001F // file/group/record/unit separator
1038 U00AD // Soft hyphen
1040 U2060 // Word joiner
1041 U2062 U2063 // Invisible times/separator
1042 U200B UFFFC // Zero-width space/no-break space
1044 // Bidi formatting characters. (RFC 3987 sections 3.2 and 4.1 paragraph 6)
1045 U200E U200F U202A U202B U202C U202D U202E
1049 losslessDecodeURI: function losslessDecodeURI(url) {
1050 return url.split("%25").map(function (url) {
1051 // Non-UTF-8 compliant URLs cause "malformed URI sequence" errors.
1053 return decodeURI(url).replace(this.unsafeURI, encodeURIComponent);
1058 }, this).join("%25");
1062 * Returns an XPath union expression constructed from the specified node
1063 * tests. An expression is built with node tests for both the null and
1064 * XHTML namespaces. See {@link Buffer#evaluateXPath}.
1066 * @param nodes {Array(string)}
1069 makeXPath: function makeXPath(nodes) {
1070 return array(nodes).map(util.debrace).flatten()
1071 .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
1072 .map(function (node) "//" + node).join(" | ");
1076 * Creates a DTD fragment from the given object. Each property of
1077 * the object is converted to an ENTITY declaration. SGML special
1078 * characters other than ' and % are left intact.
1080 * @param {object} obj The object to convert.
1081 * @returns {string} The DTD fragment containing entity declaration
1084 makeDTD: let (map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" })
1085 function makeDTD(obj) iter(obj)
1086 .map(function ([k, v]) ["<!ENTITY ", k, " '", String.replace(v == null ? "null" : typeof v == "xml" ? v.toXMLString() : v,
1087 typeof v == "xml" ? /['%]/g : /['"%&<>]/g,
1088 function (m) map[m]),
1092 map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
1093 writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
1094 readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
1097 * Converts a URI string into a URI object.
1099 * @param {string} uri
1102 // FIXME: createURI needed too?
1103 newURI: function newURI(uri, charset, base) this.withProperErrors("newURI", services.io, uri, charset, base),
1106 * Removes leading garbage prepended to URIs by the subscript
1109 fixURI: function fixURI(url) String.replace(url, /.* -> /, ""),
1112 * Pretty print a JavaScript object. Use HTML markup to color certain items
1113 * if *color* is true.
1115 * @param {Object} object The object to pretty print.
1116 * @param {boolean} color Whether the output should be colored.
1119 objectToString: function objectToString(object, color) {
1120 // Use E4X literals so html is automatically quoted
1121 // only when it's asked for. No one wants to see <
1122 // on their console or :map :foo in their buffer
1123 // when they expect :map <C-f> :foo.
1124 XML.prettyPrinting = false;
1125 XML.ignoreWhitespace = false;
1128 return object + "\n";
1130 if (!isObject(object))
1131 return String(object);
1133 function namespaced(node) {
1134 var ns = NAMESPACES[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
1136 return node.localName;
1138 return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
1139 return ns + ":" + node.localName;
1142 if (object instanceof Ci.nsIDOMElement) {
1143 const NAMESPACES = array.toObject([
1149 if (elem.nodeType == elem.TEXT_NODE)
1153 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
1155 return <span highlight="HelpXMLBlock"><span highlight="HelpXMLTagStart"><{
1157 template.map(array.iterValues(elem.attributes),
1159 <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
1160 <span highlight="HelpXMLString">{attr.value}</span>,
1162 }{ !hasChildren ? "/>" : ">"
1163 }</span>{ !hasChildren ? "" : <>...</> +
1164 <span highlight="HtmlTagEnd"><{namespaced(elem)}></span>
1167 let tag = "<" + [namespaced(elem)].concat(
1168 [namespaced(a) + "=" + template.highlight(a.value, true)
1169 for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
1170 return tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">");
1173 return {}.toString.call(elem);
1177 try { // for window.JSON
1178 var obj = String(object);
1181 obj = Object.prototype.toString.call(obj);
1183 obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () <span highlight="NonText">^J</span>);
1184 let string = <><span highlight="Title Object">{obj}</span>::
</>;
1188 // window.content often does not want to be queried with "var i in object"
1190 let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"]));
1191 if (object.dactyl && object.modules && object.modules.modules == object.modules) {
1192 object = Iterator(object);
1195 for (let i in object) {
1196 let value = <![CDATA[<no value>]]>;
1202 if (isArray(i) && i.length == 2)
1208 value = template.highlight(value, true, 150);
1209 let key = <span highlight="Key">{i}</span>;
1212 else if (/^[A-Z_]+$/.test(i))
1214 keys.push([i, <>{key}{noVal ? "" : <>: {value}</>}
</>]);
1219 function compare(a, b) {
1220 if (!isNaN(a[0]) && !isNaN(b[0]))
1222 return String.localeCompare(a[0], b[0]);
1224 string += template.map(keys.sort(compare), function (f) f[1]);
1225 return color ? <div style="white-space: pre-wrap;">{string}</div> : [s for each (s in string)].join("");
1229 "dactyl-cleanup-modules": function (subject, reason) {
1230 defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
1232 for (let module in values(defineModule.modules))
1233 if (module.cleanup) {
1234 util.dump("cleanup: " + module.constructor.className);
1235 util.trapErrors(module.cleanup, module, reason);
1238 JSMLoader.cleanup();
1240 if (!this.rehashing)
1241 services.observer.addObserver(this, "dactyl-rehash", true);
1243 "dactyl-rehash": function () {
1244 services.observer.removeObserver(this, "dactyl-rehash");
1246 defineModule.loadLog.push("dactyl: util: observe: dactyl-rehash");
1247 if (!this.rehashing)
1248 for (let module in values(defineModule.modules)) {
1249 defineModule.loadLog.push("dactyl: util: init(" + module + ")");
1256 "dactyl-purge": function () {
1260 "toplevel-window-ready": function (window, data) {
1261 window.addEventListener("DOMContentLoaded", wrapCallback(function listener(event) {
1262 if (event.originalTarget === window.document) {
1263 window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
1264 util._loadOverlays(window);
1268 "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
1269 "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); }
1272 _loadOverlays: function _loadOverlays(window) {
1273 if (!window.dactylOverlays)
1274 window.dactylOverlays = [];
1276 for each (let obj in util.overlays[window.document.documentURI] || []) {
1277 if (window.dactylOverlays.indexOf(obj) >= 0)
1279 window.dactylOverlays.push(obj);
1280 this._loadOverlay(window, obj(window));
1284 _loadOverlay: function _loadOverlay(window, obj) {
1285 let doc = window.document;
1286 if (!doc.dactylOverlayElements) {
1287 doc.dactylOverlayElements = [];
1288 doc.dactylOverlayAttributes = [];
1291 function overlay(key, fn) {
1293 let iterator = Iterator(obj[key]);
1294 if (!isObject(obj[key]))
1295 iterator = ([elem.@id, elem.elements(), elem.@*::*.(function::name() != "id")] for each (elem in obj[key]));
1297 for (let [elem, xml, attr] in iterator) {
1298 if (elem = doc.getElementById(elem)) {
1299 let node = util.xmlToDom(xml, doc, obj.objects);
1300 if (!(node instanceof Ci.nsIDOMDocumentFragment))
1301 doc.dactylOverlayElements.push(node);
1303 for (let n in array.iterValues(node.childNodes))
1304 doc.dactylOverlayElements.push(n);
1307 for each (let attr in attr || []) {
1308 let ns = attr.namespace(), name = attr.localName();
1309 doc.dactylOverlayAttributes.push([elem, ns, name, getAttr(elem, ns, name), String(attr)]);
1310 if (attr.name() != "highlight")
1311 elem.setAttributeNS(ns, name, String(attr));
1313 highlight.highlightNode(elem, String(attr));
1320 overlay("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
1321 overlay("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
1322 overlay("append", function (elem, dom) elem.appendChild(dom));
1323 overlay("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
1328 if (doc.readyState === "complete")
1331 doc.addEventListener("load", wrapCallback(function load(event) {
1332 if (event.originalTarget === event.target) {
1333 doc.removeEventListener("load", load.wrapper, true);
1334 obj.load(window, event);
1340 * Overlays an object with the given property overrides. Each
1341 * property in *overrides* is added to *object*, replacing any
1342 * original value. Functions in *overrides* are augmented with the
1343 * new properties *super*, *supercall*, and *superapply*, in the
1344 * same manner as class methods, so that they man call their
1345 * overridden counterparts.
1347 * @param {object} object The object to overlay.
1348 * @param {object} overrides An object containing properties to
1350 * @returns {function} A function which, when called, will remove
1353 overlayObject: function (object, overrides) {
1354 let original = Object.create(object);
1355 overrides = update(Object.create(original), overrides);
1357 Object.getOwnPropertyNames(overrides).forEach(function (k) {
1358 let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
1359 if (desc.value instanceof Class.Property)
1360 desc = desc.value.init(k) || desc.value;
1363 for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
1364 if (orig = Object.getOwnPropertyDescriptor(obj, k))
1365 Object.defineProperty(original, k, orig);
1368 if (orig = Object.getPropertyDescriptor(object, k))
1369 Object.defineProperty(original, k, orig);
1372 // Guard against horrible add-ons that use eval-based monkey
1374 let value = desc.value;
1375 if (callable(desc.value)) {
1378 delete desc.writable;
1379 desc.get = function get() value;
1380 desc.set = function set(val) {
1381 if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0)
1382 Class.replaceProperty(this, k, val);
1384 let package_ = util.newURI(util.fixURI(Components.stack.caller.filename)).host;
1385 util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
1386 util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
1392 Object.defineProperty(object, k, desc);
1394 if (callable(value)) {
1395 let sentinel = "(function DactylOverlay() {}())"
1396 value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
1397 value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
1408 util.reportError(e);
1412 return function unwrap() {
1413 for each (let k in Object.getOwnPropertyNames(original))
1414 if (Object.getOwnPropertyDescriptor(object, k).configurable)
1415 Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
1418 object[k] = original[k];
1425 overlayWindow: function (url, fn) {
1426 if (url instanceof Ci.nsIDOMWindow)
1427 util._loadOverlay(url, fn);
1429 Array.concat(url).forEach(function (url) {
1430 if (!this.overlays[url])
1431 this.overlays[url] = [];
1432 this.overlays[url].push(fn);
1435 for (let doc in util.iterDocuments())
1436 if (["interactive", "complete"].indexOf(doc.readyState) >= 0)
1437 this._loadOverlays(doc.defaultView);
1439 this.observe(doc.defaultView, "toplevel-window-ready");
1444 * Parses the fields of a form and returns a URL/POST-data pair
1445 * that is the equivalent of submitting the form.
1447 * @param {nsINode} field One of the fields of the given form.
1450 // Nuances gleaned from browser.jar/content/browser/browser.js
1451 parseForm: function parseForm(field) {
1452 function encode(name, value, param) {
1453 param = param ? "%s" : "";
1455 return name + "=" + encodeComponent(value + param);
1456 return encodeComponent(name) + "=" + encodeComponent(value) + param;
1459 let form = field.form;
1460 let doc = form.ownerDocument;
1462 let charset = doc.characterSet;
1463 let converter = services.CharsetConv(charset);
1464 for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
1465 let c = services.CharsetConv(cs);
1467 converter = services.CharsetConv(cs);
1472 let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
1473 let url = util.newURI(form.action, charset, uri).spec;
1475 let post = form.method.toUpperCase() == "POST";
1477 let encodeComponent = encodeURIComponent;
1478 if (charset !== "UTF-8")
1479 encodeComponent = function encodeComponent(str)
1480 escape(converter.ConvertFromUnicode(str) + converter.Finish());
1483 if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
1484 elems.push(encode(field.name, field.value));
1486 for (let [, elem] in iter(form.elements))
1487 if (elem.name && !elem.disabled) {
1488 if (Set.has(util.editableInputs, elem.type)
1489 || /^(?:hidden|textarea)$/.test(elem.type)
1490 || elem.type == "submit" && elem == field
1491 || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
1492 elems.push(encode(elem.name, elem.value, elem === field));
1493 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
1494 for (let [, opt] in Iterator(elem.options))
1496 elems.push(encode(elem.name, opt.value));
1501 return [url, elems.join('&'), charset, elems];
1502 return [url + "?" + elems.join('&'), null, charset, elems];
1506 * A generator that returns the values between *start* and *end*, in *step*
1509 * @param {number} start The interval's start value.
1510 * @param {number} end The interval's end value.
1511 * @param {boolean} step The value to step the range by. May be
1512 * negative. @default 1
1513 * @returns {Iterator(Object)}
1515 range: function range(start, end, step) {
1519 for (; start < end; start += step)
1524 yield start += step;
1529 * An interruptible generator that returns all values between *start* and
1530 * *end*. The thread yields every *time* milliseconds.
1532 * @param {number} start The interval's start value.
1533 * @param {number} end The interval's end value.
1534 * @param {number} time The time in milliseconds between thread yields.
1535 * @returns {Iterator(Object)}
1537 interruptibleRange: function interruptibleRange(start, end, time) {
1538 let endTime = Date.now() + time;
1539 while (start < end) {
1540 if (Date.now() > endTime) {
1541 util.threadYield(true, true);
1542 endTime = Date.now() + time;
1549 * Creates a new RegExp object based on the value of expr stripped
1550 * of all white space and interpolated with the values from tokens.
1551 * If tokens, any string in the form of <key> in expr is replaced
1552 * with the value of the property, 'key', from tokens, if that
1553 * property exists. If the property value is itself a RegExp, its
1554 * source is substituted rather than its string value.
1556 * Additionally, expr is stripped of all JavaScript comments.
1558 * This is similar to Perl's extended regular expression format.
1560 * @param {string|XML} expr The expression to compile into a RegExp.
1561 * @param {string} flags Flags to apply to the new RegExp.
1562 * @param {object} tokens The tokens to substitute. @optional
1563 * @returns {RegExp} A custom regexp object.
1565 regexp: update(function (expr, flags, tokens) {
1566 flags = flags || [k for ([k, v] in Iterator({ g: "global", i: "ignorecase", m: "multiline", y: "sticky" }))
1567 if (expr[v])].join("");
1569 if (isinstance(expr, ["RegExp"]))
1572 expr = String.replace(expr, /\\(.)/, function (m, m1) {
1574 flags = flags.replace(/i/g, "") + "i";
1576 flags = flags.replace(/i/g, "");
1582 // Replace replacement <tokens>.
1584 expr = String.replace(expr, /(\(?P)?<(\w+)>/g, function (m, n1, n2) !n1 && Set.has(tokens, n2) ? tokens[n2].dactylSource || tokens[n2].source || tokens[n2] : m);
1586 // Strip comments and white space.
1587 if (/x/.test(flags))
1588 expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm, function (m, m1) m1 || "");
1590 // Replace (?P<named> parameters)
1591 if (/\(\?P</.test(expr)) {
1593 let groups = ["wholeMatch"];
1594 expr = expr.replace(/((?:[^[(\\]|\\.|\[(?:[^\]]|\\.)*\])*)\((?:\?P<([^>]+)>|(\?))?/gy,
1595 function (m0, m1, m2, m3) {
1597 groups.push(m2 || "-group-" + groups.length);
1598 return m1 + "(" + (m3 || "");
1600 var struct = Struct.apply(null, groups);
1603 let res = update(RegExp(expr, flags.replace("x", "")), {
1604 closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),
1605 dactylPropertyNames: ["exec", "match", "test", "toSource", "toString", "global", "ignoreCase", "lastIndex", "multiLine", "source", "sticky"],
1606 iterate: function (str, idx) util.regexp.iterate(this, str, idx)
1609 // Return a struct with properties for named parameters if we
1613 exec: function exec() let (match = exec.superapply(this, arguments)) match && struct.fromArray(match),
1614 dactylSource: source, struct: struct
1619 * Escapes Regular Expression special characters in *str*.
1621 * @param {string} str
1624 escape: function regexp_escape(str) str.replace(/([\\{}()[\]^$.?*+|])/g, "\\$1"),
1627 * Given a RegExp, returns its source in the form showable to the user.
1629 * @param {RegExp} re The regexp showable source of which is to be returned.
1632 getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g, function (m0, m1) m1 === "/" ? "/" : m0),
1635 * Iterates over all matches of the given regexp in the given
1638 * @param {RegExp} regexp The regular expression to execute.
1639 * @param {string} string The string to search.
1640 * @param {number} lastIndex The index at which to begin searching. @optional
1642 iterate: function iterate(regexp, string, lastIndex) iter(function () {
1643 regexp.lastIndex = lastIndex = lastIndex || 0;
1645 while (match = regexp.exec(string)) {
1646 lastIndex = regexp.lastIndex;
1648 regexp.lastIndex = lastIndex;
1649 if (match[0].length == 0 || !regexp.global)
1656 * Reloads dactyl in entirety by disabling the add-on and
1659 rehash: function (args) {
1660 storage.session.commandlineArgs = args;
1661 this.timeout(function () {
1662 services.observer.notifyObservers(null, "startupcache-invalidate", "");
1663 this.rehashing = true;
1664 let addon = config.addon;
1665 addon.userDisabled = true;
1666 addon.userDisabled = false;
1671 errors: Class.memoize(function () []),
1674 * Reports an error to the Error Console and the standard output,
1675 * along with a stack trace and other relevant information. The
1676 * error is appended to {@see #errors}.
1678 reportError: function (error) {
1682 if (isString(error))
1683 error = Error(error);
1686 Cu.reportError(error);
1691 let obj = update({}, error, {
1692 toString: function () String(error),
1693 stack: <>{util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t")}</>
1696 this.errors.push([new Date, obj + "\n" + obj.stack]);
1697 this.errors = this.errors.slice(-this.maxErrors);
1698 this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
1700 this.dump(String(error));
1706 this.dump(String(error));
1707 this.dump(util.stackLines(error.stack).join("\n"));
1709 catch (e) { dump(e + "\n"); }
1712 // ctypes.open("libc.so.6").declare("kill", ctypes.default_abi, ctypes.void_t, ctypes.int, ctypes.int)(
1713 // ctypes.open("libc.so.6").declare("getpid", ctypes.default_abi, ctypes.int)(), 2)
1717 * Given a domain, returns an array of all non-toplevel subdomains
1720 * @param {string} host The host for which to find subdomains.
1721 * @returns {[string]}
1723 subdomains: function subdomains(host) {
1724 if (/(^|\.)\d+$|:.*:/.test(host))
1725 // IP address or similar
1728 let base = host.replace(/.*\.(.+?\..+?)$/, "$1");
1730 base = services.tld.getBaseDomainFromHost(host);
1734 let ary = host.split(".");
1735 ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))];
1736 return ary.filter(function (h) h.length >= base.length);
1740 * Scrolls an element into view if and only if it's not already
1743 * @param {Node} elem The element to make visible.
1745 scrollIntoView: function scrollIntoView(elem, alignWithTop) {
1746 let win = elem.ownerDocument.defaultView;
1747 let rect = elem.getBoundingClientRect();
1748 if (!(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0))
1749 elem.scrollIntoView(arguments.length > 1 ? alignWithTop : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom));
1753 * Returns the selection controller for the given window.
1755 * @param {Window} window
1756 * @returns {nsISelectionController}
1758 selectionController: function (win)
1759 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1760 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
1761 .QueryInterface(Ci.nsISelectionController),
1764 * Suspend execution for at least *delay* milliseconds. Functions by
1765 * yielding execution to the next item in the main event queue, and
1766 * so may lead to unexpected call graphs, and long delays if another
1767 * handler yields execution while waiting.
1769 * @param {number} delay The time period for which to sleep in milliseconds.
1771 sleep: function (delay) {
1772 let mainThread = services.threading.mainThread;
1774 let end = Date.now() + delay;
1775 while (Date.now() < end)
1776 mainThread.processNextEvent(true);
1781 * Behaves like String.split, except that when *limit* is reached,
1782 * the trailing element contains the entire trailing portion of the
1785 * util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"]
1787 * @param {string} str The string to split.
1788 * @param {RegExp|string} re The regular expression on which to split the string.
1789 * @param {number} limit The maximum number of elements to return.
1790 * @returns {[string]}
1792 split: function (str, re, limit) {
1795 re = RegExp(re.source || re, "g");
1796 let match, start = 0, res = [];
1797 while (--limit && (match = re.exec(str)) && match[0].length) {
1798 res.push(str.substring(start, match.index));
1799 start = match.index + match[0].length;
1801 res.push(str.substring(start));
1806 * Split a string on literal occurrences of a marker.
1808 * Specifically this ignores occurrences preceded by a backslash, or
1809 * contained within 'single' or "double" quotes.
1811 * It assumes backslash escaping on strings, and will thus not count quotes
1812 * that are preceded by a backslash or within other quotes as starting or
1813 * ending quoted sections of the string.
1815 * @param {string} str
1816 * @param {RegExp} marker
1817 * @returns {[string]}
1819 splitLiteral: function splitLiteral(str, marker) {
1821 let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source);
1826 str = str.replace(resep, function (match, before) {
1827 results.push(before);
1828 cont = match !== "";
1839 * Yields execution to the next event in the current thread's event
1840 * queue. This is a potentially dangerous operation, since any
1841 * yielders higher in the event stack will prevent execution from
1842 * returning to the caller until they have finished their wait. The
1843 * potential for deadlock is high.
1845 * @param {boolean} flush If true, flush all events in the event
1846 * queue before returning. Otherwise, wait for an event to
1847 * process before proceeding.
1848 * @param {boolean} interruptable If true, this yield may be
1849 * interrupted by pressing <C-c>, in which case,
1850 * Error("Interrupted") will be thrown.
1852 threadYield: function (flush, interruptable) {
1855 let mainThread = services.threading.mainThread;
1857 util.interrupted = false;
1859 mainThread.processNextEvent(!flush);
1860 if (util.interrupted)
1861 throw Error("Interrupted");
1863 while (flush === true && mainThread.hasPendingEvents());
1871 * Waits for the function *test* to return true, or *timeout*
1872 * milliseconds to expire.
1874 * @param {function} test The predicate on which to wait.
1875 * @param {object} self The 'this' object for *test*.
1876 * @param {Number} timeout The maximum number of milliseconds to
1879 * @param {boolean} interruptable If true, may be interrupted by
1880 * pressing <C-c>, in which case, Error("Interrupted") will be
1883 waitFor: function waitFor(test, self, timeout, interruptable) {
1884 let end = timeout && Date.now() + timeout, result;
1886 let timer = services.Timer(function () {}, 10, services.Timer.TYPE_REPEATING_SLACK);
1888 while (!(result = test.call(self)) && (!end || Date.now() < end))
1889 this.threadYield(false, interruptable);
1898 * Makes the passed function yieldable. Each time the function calls
1899 * yield, execution is suspended for the yielded number of
1903 * let func = yieldable(function () {
1904 * util.dump(Date.now()); // 0
1906 * util.dump(Date.now()); // 1500
1910 * @param {function} func The function to mangle.
1911 * @returns {function} A new function which may not execute
1914 yieldable: function yieldable(func)
1916 let gen = func.apply(this, arguments);
1919 util.timeout(next, gen.next());
1921 catch (e if e instanceof StopIteration) {};
1926 * Wraps a callback function such that its errors are not lost. This
1927 * is useful for DOM event listeners, which ordinarily eat errors.
1928 * The passed function has the property *wrapper* set to the new
1929 * wrapper function, while the wrapper has the property *wrapped*
1930 * set to the original callback.
1932 * @param {function} callback The callback to wrap.
1933 * @returns {function}
1935 wrapCallback: wrapCallback,
1938 * Returns the top-level chrome window for the given window.
1940 * @param {Window} win The child window.
1941 * @returns {Window} The top-level parent window.
1943 topWindow: function topWindow(win)
1944 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1945 .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
1946 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow),
1949 * Traps errors in the called function, possibly reporting them.
1951 * @param {function} func The function to call
1952 * @param {object} self The 'this' object for the function.
1954 trapErrors: function trapErrors(func, self) {
1956 if (!callable(func))
1958 return func.apply(self || this, Array.slice(arguments, 2));
1961 util.reportError(e);
1967 * Returns the file path of a given *url*, for debugging purposes.
1968 * If *url* points to a file (even if indirectly), the native
1969 * filesystem path is returned. Otherwise, the URL itself is
1972 * @param {string} url The URL to mangle.
1973 * @returns {string} The path to the file.
1975 urlPath: function urlPath(url) {
1977 return util.getFile(url).path;
1985 * Returns a list of all domains and subdomains of documents in the
1986 * given window and all of its descendant frames.
1988 * @param {nsIDOMWindow} win The window for which to find domains.
1989 * @returns {[string]} The visible domains.
1991 visibleHosts: function (win) {
1992 let res = [], seen = {};
1993 (function rec(frame) {
1995 if (frame.location.hostname)
1996 res = res.concat(util.subdomains(frame.location.hostname));
1999 Array.forEach(frame.frames, rec);
2001 return res.filter(function (h) !Set.add(seen, h));
2005 * Returns a list of URIs of documents in the given window and all
2006 * of its descendant frames.
2008 * @param {nsIDOMWindow} win The window for which to find URIs.
2009 * @returns {[nsIURI]} The visible URIs.
2011 visibleURIs: function (win) {
2012 let res = [], seen = {};
2013 (function rec(frame) {
2015 res = res.concat(util.newURI(frame.location.href));
2018 Array.forEach(frame.frames, rec);
2020 return res.filter(function (h) !Set.add(seen, h.spec));
2024 * Wraps native exceptions thrown by the called function so that a
2025 * proper stack trace may be retrieved from them.
2027 * @param {function|string} meth The method to call.
2028 * @param {object} self The 'this' object of the method.
2029 * @param ... Arguments to pass to *meth*.
2031 withProperErrors: function withProperErrors(meth, self) {
2033 return (callable(meth) ? meth : self[meth]).apply(self, Array.slice(arguments, withProperErrors.length));
2036 throw e.stack ? e : Error(e);
2041 * Converts an E4X XML literal to a DOM node. Any attribute named
2042 * highlight is present, it is transformed into dactyl:highlight,
2043 * and the named highlight groups are guaranteed to be loaded.
2045 * @param {Node} node
2046 * @param {Document} doc
2047 * @param {Object} nodes If present, nodes with the "key" attribute are
2048 * stored here, keyed to the value thereof.
2051 xmlToDom: function xmlToDom(node, doc, nodes) {
2052 XML.prettyPrinting = false;
2053 if (typeof node === "string") // Sandboxes can't currently pass us XML objects.
2056 if (node.length() != 1) {
2057 let domnode = doc.createDocumentFragment();
2058 for each (let child in node)
2059 domnode.appendChild(xmlToDom(child, doc, nodes));
2063 switch (node.nodeKind()) {
2065 return doc.createTextNode(String(node));
2067 let domnode = doc.createElementNS(node.namespace(), node.localName());
2069 for each (let attr in node.@*::*)
2070 if (attr.name() != "highlight")
2071 domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
2073 for each (let child in node.*::*)
2074 domnode.appendChild(xmlToDom(child, doc, nodes));
2075 if (nodes && node.@key)
2076 nodes[node.@key] = domnode;
2078 if ("@highlight" in node)
2079 highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
2090 * Math utility methods.
2093 var GlobalMath = Math;
2094 var Math = update(Object.create(GlobalMath), {
2096 * Returns the specified *value* constrained to the range *min* - *max*.
2098 * @param {number} value The value to constrain.
2099 * @param {number} min The minimum constraint.
2100 * @param {number} max The maximum constraint.
2103 constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max)
2108 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
2110 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: