]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/util.jsm
673366a099188f42f964583bc1a7f44a78d6f0b5
[dactyl.git] / common / modules / util.jsm
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>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 try {
10
11 Components.utils.import("resource://dactyl/bootstrap.jsm");
12     let frag=1;
13 defineModule("util", {
14     exports: ["frag", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
15     require: ["services"],
16     use: ["commands", "config", "highlight", "storage", "template"]
17 }, this);
18
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;
24
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);
30     },
31
32     level: 3,
33
34     noTrace: true
35 });
36
37 var Point = Struct("x", "y");
38
39 var wrapCallback = function wrapCallback(fn) {
40     fn.wrapper = function wrappedCallback () {
41         try {
42             return fn.apply(this, arguments);
43         }
44         catch (e) {
45             util.reportError(e);
46             return undefined;
47         }
48     };
49     fn.wrapper.wrapped = fn;
50     return fn.wrapper;
51 }
52
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) {
56     if (val == null)
57         elem.removeAttributeNS(ns, name);
58     else
59         elem.setAttributeNS(ns, name, val);
60 }
61
62 var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
63     init: function () {
64         this.Array = array;
65
66         this.addObserver(this);
67         this.overlays = {};
68     },
69
70     cleanup: function cleanup() {
71         for (let { document: doc } in iter(services.windowMediator.getEnumerator(null))) {
72             for (let elem in values(doc.dactylOverlayElements || []))
73                 if (elem.parentNode)
74                     elem.parentNode.removeChild(elem);
75
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);
79
80             delete doc.dactylOverlayElements;
81             delete doc.dactylOverlayAttributes;
82             delete doc.dactylOverlays;
83         }
84     },
85
86     // FIXME: Only works for Pentadactyl
87     get activeWindow() services.windowMediator.getMostRecentWindow("navigator:browser"),
88     dactyl: update(function dactyl(obj) {
89         if (obj)
90             var global = Class.objectGlobal(obj);
91         return {
92             __noSuchMethod__: function (meth, args) {
93                 let win = util.activeWindow;
94                 var dactyl = global && global.dactyl || win && win.dactyl;
95                 if (!dactyl)
96                     return null;
97
98                 let prop = dactyl[meth];
99                 if (callable(prop))
100                     return prop.apply(dactyl, args);
101                 return prop;
102             }
103         };
104     }, {
105         __noSuchMethod__: function () this().__noSuchMethod__.apply(null, arguments)
106     }),
107
108     /**
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
114      * shutdown.
115      *
116      * @param {object} obj
117      */
118     addObserver: function (obj) {
119         if (!obj.observers)
120             obj.observers = obj.observe;
121
122         function register(meth) {
123             for (let target in set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers))))
124                 try {
125                     services.observer[meth](obj, target, true);
126                 }
127                 catch (e) {}
128         }
129
130         Class.replaceProperty(obj, "observe",
131             function (subject, target, data) {
132                 try {
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);
137                 }
138                 catch (e) {
139                     if (typeof util === "undefined")
140                         dump("dactyl: error: " + e + "\n" + (e.stack || Error().stack).replace(/^/gm, "dactyl:    "));
141                     else
142                         util.reportError(e);
143                 }
144             });
145
146         obj.observe.unregister = function () register("removeObserver");
147         register("addObserver");
148     },
149
150     /*
151      * Tests a condition and throws a FailedAssertion error on
152      * failure.
153      *
154      * @param {boolean} condition The condition to test.
155      * @param {string} message The message to present to the
156      *     user on failure.
157      */
158     assert: function (condition, message, quiet) {
159         if (!condition)
160             throw FailedAssertion(message, 1, quiet === undefined ? true : quiet);
161     },
162
163     /**
164      * Capitalizes the first character of the given string.
165      * @param {string} str The string to capitalize
166      * @returns {string}
167      */
168     capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1),
169
170     /**
171      * Returns a RegExp object that matches characters specified in the range
172      * expression *list*, or signals an appropriate error if *list* is invalid.
173      *
174      * @param {string} list Character list, e.g., "a b d-xA-Z" produces /[abd-xA-Z]/.
175      * @param {string} accepted Character range(s) to accept, e.g. "a-zA-Z" for
176      *     ASCII letters. Used to validate *list*.
177      * @returns {RegExp}
178      */
179     charListToRegexp: function charListToRegexp(list, accepted) {
180         list = list.replace(/\s+/g, "");
181
182         // check for chars not in the accepted range
183         this.assert(RegExp("^[" + accepted + "-]+$").test(list),
184                     "Character list outside the range " + accepted.quote());
185
186         // check for illegal ranges
187         for (let [match] in this.regexp.iterate(/.-./g, list))
188             this.assert(match.charCodeAt(0) <= match.charCodeAt(2),
189                         "Invalid character range: " + list.slice(list.indexOf(match)))
190
191         return RegExp("[" + util.regexp.escape(list) + "]");
192     },
193
194     get chromePackages() {
195         // Horrible hack.
196         let res = {};
197         function process(manifest) {
198             for each (let line in manifest.split(/\n+/)) {
199                 let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
200                 if (match)
201                     res[match[2]] = true;
202             }
203         }
204         function processJar(file) {
205             let jar = services.ZipReader(file);
206             if (jar) {
207                 if (jar.hasEntry("chrome.manifest"))
208                     process(File.readStream(jar.getInputStream("chrome.manifest")));
209                 jar.close();
210             }
211         }
212
213         for each (let dir in ["UChrm", "AChrom"]) {
214             dir = File(services.directory.get(dir, Ci.nsIFile));
215             if (dir.exists() && dir.isDirectory())
216                 for (let file in dir.iterDirectory())
217                     if (/\.manifest$/.test(file.leafName))
218                         process(file.read());
219
220             dir = File(dir.parent);
221             if (dir.exists() && dir.isDirectory())
222                 for (let file in dir.iterDirectory())
223                     if (/\.jar$/.test(file.leafName))
224                         processJar(file);
225
226             dir = dir.child("extensions");
227             if (dir.exists() && dir.isDirectory())
228                 for (let ext in dir.iterDirectory()) {
229                     if (/\.xpi$/.test(ext.leafName))
230                         processJar(ext);
231                     else {
232                         if (ext.isFile())
233                             ext = File(ext.read().replace(/\n*$/, ""));
234                         let mf = ext.child("chrome.manifest");
235                         if (mf.exists())
236                             process(mf.read());
237                     }
238                 }
239         }
240         return Object.keys(res).sort();
241     },
242
243     /**
244      * Returns a shallow copy of *obj*.
245      *
246      * @param {Object} obj
247      * @returns {Object}
248      */
249     cloneObject: function cloneObject(obj) {
250         if (isArray(obj))
251             return obj.slice();
252         let newObj = {};
253         for (let [k, v] in Iterator(obj))
254             newObj[k] = v;
255         return newObj;
256     },
257
258     /**
259      * Clips a string to a given length. If the input string is longer
260      * than *length*, an ellipsis is appended.
261      *
262      * @param {string} str The string to truncate.
263      * @param {number} length The length of the returned string.
264      * @returns {string}
265      */
266     clip: function clip(str, length) {
267         return str.length <= length ? str : str.substr(0, length - 3) + "...";
268     },
269
270     /**
271      * Compares two strings, case insensitively. Return values are as
272      * in String#localeCompare.
273      *
274      * @param {string} a
275      * @param {string} b
276      * @returns {number}
277      */
278     compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()),
279
280     compileFormat: function compileFormat(format) {
281         let stack = [frame()];
282         stack.__defineGetter__("top", function () this[this.length - 1]);
283
284         function frame() update(
285             function _frame(obj)
286                 _frame === stack.top || _frame.valid(obj) ?
287                     _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
288             {
289                 elements: [],
290                 seen: {},
291                 valid: function (obj) this.elements.every(function (e) !e.test || e.test(obj))
292             });
293
294         let end = 0;
295         for (let match in util.regexp.iterate(/(.*?)%(.)/gy, format)) {
296
297             let [, prefix, char] = match;
298             end += match[0].length;
299
300             if (prefix)
301                 stack.top.elements.push(prefix);
302             if (char === "%")
303                 stack.top.elements.push("%");
304             else if (char === "[") {
305                 let f = frame();
306                 stack.top.elements.push(f);
307                 stack.push(f);
308             }
309             else if (char === "]") {
310                 stack.pop();
311                 util.assert(stack.length, "Unmatched %] in format");
312             }
313             else {
314                 let quote = function quote(obj, char) obj[char];
315                 if (char !== char.toLowerCase())
316                     quote = function quote(obj, char) Commands.quote(obj[char]);
317                 char = char.toLowerCase();
318
319                 stack.top.elements.push(update(
320                     function (obj) obj[char] != null ? quote(obj, char) : "",
321                     { test: function (obj) obj[char] != null }));
322
323                 for (let elem in array.iterValues(stack))
324                     elem.seen[char] = true;
325             }
326         }
327         if (end < format.length)
328             stack.top.elements.push(format.substr(end));
329
330         util.assert(stack.length === 1, "Unmatched %[ in format");
331         return stack.top;
332     },
333
334     compileMacro: function compileMacro(macro, keepUnknown) {
335         let stack = [frame()];
336         stack.__defineGetter__("top", function () this[this.length - 1]);
337
338         let unknown = util.identity;
339         if (!keepUnknown)
340             unknown = function () "";
341
342         function frame() update(
343             function _frame(obj)
344                 _frame === stack.top || _frame.valid(obj) ?
345                     _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
346             {
347                 elements: [],
348                 seen: {},
349                 valid: function (obj) this.elements.every(function (e) !e.test || e.test(obj))
350             });
351
352         let defaults = { lt: "<", gt: ">" };
353
354         let re = util.regexp(<![CDATA[
355             ([^]*?) // 1
356             (?:
357                 (<\{) | // 2
358                 (< ((?:[a-z]-)?[a-z-]+?) >) | // 3 4
359                 (\}>) // 5
360             )
361         ]]>, "gixy");
362         macro = String(macro);
363         let end = 0;
364         for (let match in re.iterate(macro)) {
365             let [, prefix, open, full, macro, close] = match;
366             end += match[0].length;
367
368             if (prefix)
369                 stack.top.elements.push(prefix);
370             if (open) {
371                 let f = frame();
372                 stack.top.elements.push(f);
373                 stack.push(f);
374             }
375             else if (close) {
376                 stack.pop();
377                 util.assert(stack.length, "Unmatched %] in macro");
378             }
379             else {
380                 let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro);
381                 flags = set(flags);
382
383                 let quote = util.identity;
384                 if (flags.q)
385                     quote = function quote(obj) typeof obj === "number" ? obj : String.quote(obj);
386                 if (flags.e)
387                     quote = function quote(obj) "";
388
389                 if (set.has(defaults, name))
390                     stack.top.elements.push(quote(defaults[name]));
391                 else {
392                     stack.top.elements.push(update(
393                         function (obj) obj[name] != null ? quote(obj[name]) : set.has(obj, name) ? "" : unknown(full),
394                         { test: function (obj) obj[name] != null && obj[name] !== false && (!flags.e || obj[name] != "") }));
395
396                     for (let elem in array.iterValues(stack))
397                         elem.seen[name] = true;
398                 }
399             }
400         }
401         if (end < macro.length)
402             stack.top.elements.push(macro.substr(end));
403
404         util.assert(stack.length === 1, "Unmatched <{ in macro");
405         return stack.top;
406     },
407
408     compileMatcher: function compileMatcher(list) {
409         let xpath = [], css = [];
410         for (let elem in values(list))
411             if (/^xpath:/.test(elem))
412                 xpath.push(elem.substr(6));
413             else
414                 css.push(elem);
415
416         return update(
417             function matcher(node) {
418                 if (matcher.xpath)
419                     for (let elem in util.evaluateXPath(matcher.xpath, node))
420                         yield elem;
421
422                 if (matcher.css)
423                     for (let [, elem] in iter(node.querySelectorAll(matcher.css)))
424                         yield elem;
425             }, {
426                 css: css.join(", "),
427                 xpath: xpath.join(" | ")
428             });
429     },
430
431     validateMatcher: function validateMatcher(values) {
432         let evaluator = services.XPathEvaluator();
433         let node = util.xmlToDom(<div/>, document);
434         return this.testValues(values, function (value) {
435             if (/^xpath:/.test(value))
436                 evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver);
437             else
438                 node.querySelector(value);
439             return true;
440         });
441     },
442
443     /**
444      * Returns an object representing a Node's computed CSS style.
445      *
446      * @param {Node} node
447      * @returns {Object}
448      */
449     computedStyle: function computedStyle(node) {
450         while (!(node instanceof Ci.nsIDOMElement) && node.parentNode)
451             node = node.parentNode;
452         try {
453             var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
454         }
455         catch (e) {}
456         if (res == null) {
457             util.dumpStack(_("error.nullComputedStyle", node));
458             Cu.reportError(Error(_("error.nullComputedStyle", node)));
459             return {};
460         }
461         return res;
462     },
463
464     /**
465      * Converts any arbitrary string into an URI object. Returns null on
466      * failure.
467      *
468      * @param {string} str
469      * @returns {nsIURI|null}
470      */
471     createURI: function createURI(str) {
472         try {
473             return services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
474         }
475         catch (e) {
476             return null;
477         }
478     },
479
480     /**
481      * Expands brace globbing patterns in a string.
482      *
483      * Example:
484      *     "a{b,c}d" => ["abd", "acd"]
485      *
486      * @param {string} pattern The pattern to deglob.
487      * @returns [string] The resulting strings.
488      */
489     debrace: function debrace(pattern) {
490         if (pattern.indexOf("{") == -1)
491             return [pattern];
492
493         function split(pattern, re, fn, dequote) {
494             let end = 0, match, res = [];
495             while (match = re.exec(pattern)) {
496                 end = match.index + match[0].length;
497                 res.push(match[1]);
498                 if (fn)
499                     fn(match);
500             }
501             res.push(pattern.substr(end));
502             return res.map(function (s) util.dequote(s, dequote));
503         }
504         let patterns = [], res = [];
505         let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
506             function (match) {
507                 patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
508                     null, ",{}"));
509             }, "{}");
510         function rec(acc) {
511             if (acc.length == patterns.length)
512                 res.push(array(substrings).zip(acc).flatten().join(""));
513             else
514                 for (let [, pattern] in Iterator(patterns[acc.length]))
515                     rec(acc.concat(pattern));
516         }
517         rec([]);
518         return res;
519     },
520
521     /**
522      * Removes certain backslash-quoted characters while leaving other
523      * backslash-quoting sequences untouched.
524      *
525      * @param {string} pattern The string to unquote.
526      * @param {string} chars The characters to unquote.
527      * @returns {string}
528      */
529     dequote: function dequote(pattern, chars)
530         pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0),
531
532     domToString: function (node, html) {
533         if (node instanceof Ci.nsISelection && node.isCollapsed)
534             return "";
535
536         if (node instanceof Ci.nsIDOMNode) {
537             let range = node.ownerDocument.createRange();
538             range.selectNode(node);
539             node = range;
540         }
541         let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer.ownerDocument;
542
543         let encoder = services.HtmlEncoder();
544         encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
545         if (node instanceof Ci.nsISelection)
546             encoder.setSelection(node);
547         else if (node instanceof Ci.nsIDOMRange)
548             encoder.setRange(node);
549
550         let str = services.String(encoder.encodeToString());
551         if (html)
552             return str.data;
553
554         let [result, length] = [{}, {}];
555         services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
556         return result.value.QueryInterface(Ci.nsISupportsString).data;
557     },
558
559     /**
560      * Prints a message to the console. If *msg* is an object it is pretty
561      * printed.
562      *
563      * @param {string|Object} msg The message to print.
564      */
565     dump: defineModule.dump,
566
567     stackLines: function (stack) {
568         let lines = [];
569         let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
570         while (match = re.exec(stack))
571             lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + "@" +
572                        util.fixURI(match[2]));
573         return lines;
574     },
575
576     /**
577      * Dumps a stack trace to the console.
578      *
579      * @param {string} msg The trace message.
580      * @param {number} frames The number of frames to print.
581      */
582     dumpStack: function dumpStack(msg, frames) {
583         let stack = util.stackLines(Error().stack);
584         stack = stack.slice(1, 1 + (frames || stack.length)).join("\n").replace(/^/gm, "    ");
585         util.dump((arguments.length == 0 ? "Stack" : msg) + "\n" + stack + "\n");
586     },
587
588     /**
589      * The set of input element type attribute values that mark the element as
590      * an editable field.
591      */
592     editableInputs: set(["date", "datetime", "datetime-local", "email", "file",
593                          "month", "number", "password", "range", "search",
594                          "tel", "text", "time", "url", "week"]),
595
596     /**
597      * Converts HTML special characters in *str* to the equivalent HTML
598      * entities.
599      *
600      * @param {string} str
601      * @returns {string}
602      */
603     escapeHTML: function escapeHTML(str) {
604         return str.replace(/&/g, "&amp;").replace(/</g, "&lt;");
605     },
606
607     /**
608      * Escapes quotes, newline and tab characters in *str*. The returned string
609      * is delimited by *delimiter* or " if *delimiter* is not specified.
610      * {@see String#quote}.
611      *
612      * @param {string} str
613      * @param {string} delimiter
614      * @returns {string}
615      */
616     escapeString: function escapeString(str, delimiter) {
617         if (delimiter == undefined)
618             delimiter = '"';
619         return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
620     },
621
622     /**
623      * Evaluates an XPath expression in the current or provided
624      * document. It provides the xhtml, xhtml2 and dactyl XML
625      * namespaces. The result may be used as an iterator.
626      *
627      * @param {string} expression The XPath expression to evaluate.
628      * @param {Node} elem The context element.
629      * @default The current document.
630      * @param {boolean} asIterator Whether to return the results as an
631      *     XPath iterator.
632      * @returns {Object} Iterable result of the evaluation.
633      */
634     evaluateXPath: update(
635         function evaluateXPath(expression, elem, asIterator) {
636             try {
637                 if (!elem)
638                     elem = util.activeWindow.content.document;
639                 let doc = elem.ownerDocument || elem;
640                 if (isArray(expression))
641                     expression = util.makeXPath(expression);
642
643                 let result = doc.evaluate(expression, elem,
644                     evaluateXPath.resolver,
645                     asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
646                     null
647                 );
648
649                 return Object.create(result, {
650                     __iterator__: {
651                         value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
652                                           : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
653                     }
654                 });
655             }
656             catch (e) {
657                 throw e.stack ? e : Error(e);
658             }
659         },
660         {
661             resolver: function lookupNamespaceURI(prefix) ({
662                     xul: XUL.uri,
663                     xhtml: XHTML.uri,
664                     xhtml2: "http://www.w3.org/2002/06/xhtml2",
665                     dactyl: NS.uri
666                 }[prefix] || null)
667         }),
668
669     extend: function extend(dest) {
670         Array.slice(arguments, 1).filter(util.identity).forEach(function (src) {
671             for (let [k, v] in Iterator(src)) {
672                 let get = src.__lookupGetter__(k),
673                     set = src.__lookupSetter__(k);
674                 if (!get && !set)
675                     dest[k] = v;
676                 if (get)
677                     dest.__defineGetter__(k, get);
678                 if (set)
679                     dest.__defineSetter__(k, set);
680             }
681         });
682         return dest;
683     },
684
685     /**
686      * Converts *bytes* to a pretty printed data size string.
687      *
688      * @param {number} bytes The number of bytes.
689      * @param {string} decimalPlaces The number of decimal places to use if
690      *     *humanReadable* is true.
691      * @param {boolean} humanReadable Use byte multiples.
692      * @returns {string}
693      */
694     formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) {
695         const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
696         let unitIndex = 0;
697         let tmpNum = parseInt(bytes, 10) || 0;
698         let strNum = [tmpNum + ""];
699
700         if (humanReadable) {
701             while (tmpNum >= 1024) {
702                 tmpNum /= 1024;
703                 if (++unitIndex > (unitVal.length - 1))
704                     break;
705             }
706
707             let decPower = Math.pow(10, decimalPlaces);
708             strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2);
709
710             if (!strNum[1])
711                 strNum[1] = "";
712
713             while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces)
714                 strNum[1] += "0";
715         }
716
717         for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000
718             strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u);
719
720         if (unitIndex) // decimalPlaces only when > Bytes
721             strNum[0] += "." + strNum[1];
722
723         return strNum[0] + " " + unitVal[unitIndex];
724     },
725
726     /**
727      * Converts *seconds* into a human readable time string.
728      *
729      * @param {number} seconds
730      * @returns {string}
731      */
732     formatSeconds: function formatSeconds(seconds) {
733         function pad(n, val) ("0000000" + val).substr(-Math.max(n, String(val).length));
734         function div(num, denom) [Math.round(num / denom), Math.round(num % denom)];
735         let days, hours, minutes;
736
737         [minutes, seconds] = div(seconds, 60);
738         [hours, minutes]   = div(minutes, 60);
739         [days, hours]      = div(hours,   24);
740         if (days)
741             return days + " days " + hours + " hours"
742         if (hours)
743             return hours + "h " + minutes + "m";
744         if (minutes)
745             return minutes + ":" + pad(2, seconds);
746         return seconds + "s";
747     },
748
749     /**
750      * Returns the file which backs a given URL, if available.
751      *
752      * @param {nsIURI} uri The URI for which to find a file.
753      * @returns {File|null}
754      */
755     getFile: function getFile(uri) {
756         try {
757             if (isString(uri))
758                 uri = util.newURI(util.fixURI(uri));
759
760             if (uri instanceof Ci.nsIFileURL)
761                 return File(uri.QueryInterface(Ci.nsIFileURL).file);
762
763             let channel = services.io.newChannelFromURI(uri);
764             channel.cancel(Cr.NS_BINDING_ABORTED);
765             if (channel instanceof Ci.nsIFileChannel)
766                 return File(channel.QueryInterface(Ci.nsIFileChannel).file);
767         }
768         catch (e) {}
769         return null;
770     },
771
772     /**
773      * Returns the host for the given URL, or null if invalid.
774      *
775      * @param {string} url
776      * @returns {string|null}
777      */
778     getHost: function (url) {
779         try {
780             return util.createURI(url).host;
781         }
782         catch (e) {}
783         return null;
784     },
785
786     /**
787      * Returns true if the current Gecko runtime is of the given version
788      * or greater.
789      *
790      * @param {string} ver The required version.
791      * @returns {boolean}
792      */
793     haveGecko: function (ver) services.versionCompare.compare(services.runtime.platformVersion, ver) >= 0,
794
795     /**
796      * Sends a synchronous or asynchronous HTTP request to *url* and returns
797      * the XMLHttpRequest object. If *callback* is specified the request is
798      * asynchronous and the *callback* is invoked with the object as its
799      * argument.
800      *
801      * @param {string} url
802      * @param {function(XMLHttpRequest)} callback
803      * @returns {XMLHttpRequest}
804      */
805     httpGet: function httpGet(url, callback, self) {
806         let params = callback;
807         if (!isObject(params))
808             params = { callback: params && function () callback.apply(self, arguments) };
809
810         try {
811             let xmlhttp = services.Xmlhttp();
812             xmlhttp.mozBackgroundRequest = true;
813
814             let async = params.callback || params.onload || params.onerror;
815             if (async) {
816                 xmlhttp.onload = function handler(event) { util.trapErrors(params.onload || params.callback, params, xmlhttp, event) };
817                 xmlhttp.onerror = function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event) };
818             }
819             if (params.mimeType)
820                 xmlhttp.overrideMimeType(params.mimeType);
821
822             xmlhttp.open(params.method || "GET", url, async,
823                          params.user, params.pass);
824
825             xmlhttp.send(null);
826             return xmlhttp;
827         }
828         catch (e) {
829             util.dactyl.log("Error opening " + String.quote(url) + ": " + e, 1);
830             return null;
831         }
832     },
833
834     /**
835      * The identity function.
836      *
837      * @param {Object} k
838      * @returns {Object}
839      */
840     identity: function identity(k) k,
841
842     /**
843      * Returns the intersection of two rectangles.
844      *
845      * @param {Object} r1
846      * @param {Object} r2
847      * @returns {Object}
848      */
849     intersection: function (r1, r2) ({
850         get width()  this.right - this.left,
851         get height() this.bottom - this.top,
852         left: Math.max(r1.left, r2.left),
853         right: Math.min(r1.right, r2.right),
854         top: Math.max(r1.top, r2.top),
855         bottom: Math.min(r1.bottom, r2.bottom)
856     }),
857
858     /**
859      * Returns true if the given stack frame resides in Dactyl code.
860      *
861      * @param {nsIStackFrame} frame
862      * @returns {boolean}
863      */
864     isDactyl: Class.memoize(function () {
865         let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
866         let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
867         return function isDactyl(frame) re.test(frame.filename);
868     }),
869
870     /**
871      * Returns true if *url* is in the domain *domain*.
872      *
873      * @param {string} url
874      * @param {string} domain
875      * @returns {boolean}
876      */
877     isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
878
879     /** Dactyl's notion of the current operating system platform. */
880     OS: memoize({
881         _arch: services.runtime.OS,
882         /**
883          * @property {string} The normalised name of the OS. This is one of
884          *     "Windows", "Mac OS X" or "Unix".
885          */
886         get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
887         /** @property {boolean} True if the OS is Windows. */
888         get isWindows() this._arch == "WINNT",
889         /** @property {boolean} True if the OS is Mac OS X. */
890         get isMacOSX() this._arch == "Darwin",
891         /** @property {boolean} True if the OS is some other *nix variant. */
892         get isUnix() !this.isWindows && !this.isMacOSX,
893         /** @property {RegExp} A RegExp which matches illegal characters in path components. */
894         get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g
895     }),
896
897     /**
898      * Returns true if *host* is a subdomain of *domain*.
899      *
900      * @param {string} host The host to check.
901      * @param {string} domain The base domain to check the host against.
902      * @returns {boolean}
903      */
904     isSubdomain: function isSubdomain(host, domain) {
905         if (host == null)
906             return false;
907         let idx = host.lastIndexOf(domain);
908         return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
909     },
910
911     /**
912      * Returns true if the given DOM node is currently visible.
913      *
914      * @param {Node} node
915      * @returns {boolean}
916      */
917     isVisible: function (node) {
918         let style = util.computedStyle(node);
919         return style.visibility == "visible" && style.display != "none";
920     },
921
922     /**
923      * Iterates over all currently open documents, including all
924      * top-level window and sub-frames thereof.
925      */
926     iterDocuments: function iterDocuments() {
927         let windows = services.windowMediator.getXULWindowEnumerator(null);
928         while (windows.hasMoreElements()) {
929             let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
930             for each (let type in ["typeChrome", "typeContent"]) {
931                 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
932                                                                       Ci.nsIDocShell.ENUMERATE_FORWARDS);
933                 while (docShells.hasMoreElements())
934                     let (viewer = docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer) {
935                         if (viewer)
936                             yield viewer.DOMDocument;
937                     }
938             }
939         }
940     },
941
942     /**
943      * Returns an XPath union expression constructed from the specified node
944      * tests. An expression is built with node tests for both the null and
945      * XHTML namespaces. See {@link Buffer#evaluateXPath}.
946      *
947      * @param nodes {Array(string)}
948      * @returns {string}
949      */
950     makeXPath: function makeXPath(nodes) {
951         return array(nodes).map(util.debrace).flatten()
952                            .map(function (node) [node, "xhtml:" + node]).flatten()
953                            .map(function (node) "//" + node).join(" | ");
954     },
955
956     map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
957     writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
958     readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
959
960     /**
961      * Converts a URI string into a URI object.
962      *
963      * @param {string} uri
964      * @returns {nsIURI}
965      */
966     // FIXME: createURI needed too?
967     newURI: function (uri, charset, base) services.io.newURI(uri, charset, base),
968
969     /**
970      * Removes leading garbage prepended to URIs by the subscript
971      * loader.
972      */
973     fixURI: function fixURI(url) String.replace(url, /.* -> /, ""),
974
975     /**
976      * Pretty print a JavaScript object. Use HTML markup to color certain items
977      * if *color* is true.
978      *
979      * @param {Object} object The object to pretty print.
980      * @param {boolean} color Whether the output should be colored.
981      * @returns {string}
982      */
983     objectToString: function objectToString(object, color) {
984         // Use E4X literals so html is automatically quoted
985         // only when it's asked for. No one wants to see &lt;
986         // on their console or :map :foo in their buffer
987         // when they expect :map <C-f> :foo.
988         XML.prettyPrinting = false;
989         XML.ignoreWhitespace = false;
990
991         if (object == null)
992             return object + "\n";
993
994         if (!isObject(object))
995             return String(object);
996
997         function namespaced(node) {
998             var ns = NAMESPACES[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
999             if (!ns)
1000                 return node.localName;
1001             if (color)
1002                 return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
1003             return ns + ":" + node.localName;
1004         }
1005
1006         if (object instanceof Ci.nsIDOMElement) {
1007             const NAMESPACES = array.toObject([
1008                 [NS, "dactyl"],
1009                 [XHTML, "html"],
1010                 [XUL, "xul"]
1011             ]);
1012             let elem = object;
1013             if (elem.nodeType == elem.TEXT_NODE)
1014                 return elem.data;
1015
1016             try {
1017                 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
1018                 if (color)
1019                     return <span highlight="HelpXMLBlock"><span highlight="HelpXMLTagStart">&lt;{
1020                             namespaced(elem)} {
1021                                 template.map(array.iterValues(elem.attributes),
1022                                     function (attr)
1023                                         <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
1024                                         <span highlight="HelpXMLString">{attr.value}</span>,
1025                                     <> </>)
1026                             }{ !hasChildren ? "/>" : ">"
1027                         }</span>{ !hasChildren ? "" : <>...</> +
1028                             <span highlight="HtmlTagEnd">&lt;{namespaced(elem)}></span>
1029                     }</span>;
1030
1031                 let tag = "<" + [namespaced(elem)].concat(
1032                     [namespaced(a) + "=" +  template.highlight(a.value, true)
1033                      for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
1034                 return tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">");
1035             }
1036             catch (e) {
1037                 return {}.toString.call(elem);
1038             }
1039         }
1040
1041         try { // for window.JSON
1042             var obj = String(object);
1043         }
1044         catch (e) {
1045             obj = Object.prototype.toString.call(obj);
1046         }
1047         obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () <span highlight="NonText">^J</span>);
1048         let string = <><span highlight="Title Object">{obj}</span>::&#x0a;</>;
1049
1050         let keys = [];
1051
1052         // window.content often does not want to be queried with "var i in object"
1053         try {
1054             let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"]));
1055             if (object.dactyl && object.modules && object.modules.modules == object.modules) {
1056                 object = Iterator(object);
1057                 hasValue = false;
1058             }
1059             for (let i in object) {
1060                 let value = <![CDATA[<no value>]]>;
1061                 try {
1062                     value = object[i];
1063                 }
1064                 catch (e) {}
1065                 if (!hasValue) {
1066                     if (isArray(i) && i.length == 2)
1067                         [i, value] = i;
1068                     else
1069                         var noVal = true;
1070                 }
1071
1072                 value = template.highlight(value, true, 150);
1073                 let key = <span highlight="Key">{i}</span>;
1074                 if (!isNaN(i))
1075                     i = parseInt(i);
1076                 else if (/^[A-Z_]+$/.test(i))
1077                     i = "";
1078                 keys.push([i, <>{key}{noVal ? "" : <>: {value}</>}&#x0a;</>]);
1079             }
1080         }
1081         catch (e) {}
1082
1083         function compare(a, b) {
1084             if (!isNaN(a[0]) && !isNaN(b[0]))
1085                 return a[0] - b[0];
1086             return String.localeCompare(a[0], b[0]);
1087         }
1088         string += template.map(keys.sort(compare), function (f) f[1]);
1089         return color ? <div style="white-space: pre-wrap;">{string}</div> : [s for each (s in string)].join("");
1090     },
1091
1092     observers: {
1093         "dactyl-cleanup-modules": function () {
1094             defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules");
1095
1096             for (let module in values(defineModule.modules))
1097                 if (module.cleanup) {
1098                     util.dump("cleanup: " + module.constructor.className);
1099                     util.trapErrors(module.cleanup, module);
1100                 }
1101
1102             JSMLoader.cleanup();
1103
1104             if (!this.rehashing)
1105                 services.observer.addObserver(this, "dactyl-rehash", true);
1106         },
1107         "dactyl-rehash": function () {
1108             services.observer.removeObserver(this, "dactyl-rehash");
1109
1110             defineModule.loadLog.push("dactyl: util: observe: dactyl-rehash");
1111             if (!this.rehashing)
1112                 for (let module in values(defineModule.modules)) {
1113                     defineModule.loadLog.push("dactyl: util: init(" + module + ")");
1114                     if (module.reinit)
1115                         module.reinit();
1116                     else
1117                         module.init();
1118                 }
1119         },
1120         "dactyl-purge": function () {
1121             this.rehashing = 1;
1122         },
1123         "toplevel-window-ready": function (window, data) {
1124             window.addEventListener("DOMContentLoaded", wrapCallback(function listener(event) {
1125                 if (event.originalTarget === window.document) {
1126                     window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
1127                     util._loadOverlays(window);
1128                 }
1129             }), true);
1130         },
1131         "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
1132         "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); }
1133     },
1134
1135     _loadOverlays: function _loadOverlays(window) {
1136         if (!window.dactylOverlays)
1137             window.dactylOverlays = [];
1138
1139         for each (let obj in util.overlays[window.document.documentURI] || []) {
1140             if (window.dactylOverlays.indexOf(obj) >= 0)
1141                 continue;
1142             window.dactylOverlays.push(obj);
1143             this._loadOverlay(window, obj(window));
1144         }
1145     },
1146
1147     _loadOverlay: function _loadOverlay(window, obj) {
1148         let doc = window.document;
1149         if (!doc.dactylOverlayElements) {
1150             doc.dactylOverlayElements = [];
1151             doc.dactylOverlayAttributes = [];
1152         }
1153
1154         function overlay(key, fn) {
1155             if (obj[key]) {
1156                 let iterator = Iterator(obj[key]);
1157                 if (!isObject(obj[key]))
1158                     iterator = ([elem.@id, elem.elements(), elem.@*::*.(function::name() != "id")] for each (elem in obj[key]));
1159
1160                 for (let [elem, xml, attr] in iterator) {
1161                     if (elem = doc.getElementById(elem)) {
1162                         let node = util.xmlToDom(xml, doc, obj.objects);
1163                         if (!(node instanceof Ci.nsIDOMDocumentFragment))
1164                             doc.dactylOverlayElements.push(node);
1165                         else
1166                             for (let n in array.iterValues(node.childNodes))
1167                                 doc.dactylOverlayElements.push(n);
1168
1169                         fn(elem, node);
1170                         for each (let attr in attr || []) {
1171                             let ns = attr.namespace(), name = attr.localName();
1172                             doc.dactylOverlayAttributes.push([elem, ns, name, getAttr(elem, ns, name), String(attr)]);
1173                             if (attr.name() != "highlight")
1174                                 elem.setAttributeNS(ns, name, String(attr));
1175                             else
1176                                 highlight.highlightNode(elem, String(attr));
1177                         }
1178                     }
1179                 }
1180             }
1181         }
1182
1183         overlay("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
1184         overlay("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
1185         overlay("append", function (elem, dom) elem.appendChild(dom));
1186         overlay("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
1187         if (obj.init)
1188             obj.init(window);
1189
1190         if (obj.load)
1191             if (doc.readyState === "complete")
1192                 obj.load(window);
1193             else
1194                 doc.addEventListener("load", wrapCallback(function load(event) {
1195                     if (event.originalTarget === event.target) {
1196                         doc.removeEventListener("load", load.wrapper, true);
1197                         obj.load(window, event);
1198                     }
1199                 }), true);
1200     },
1201
1202     overlayObject: function (object, overrides) {
1203         let original = Object.create(object);
1204         overrides = update(Object.create(original), overrides);
1205
1206         Object.getOwnPropertyNames(overrides).forEach(function (k) {
1207             let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
1208             if (desc.value instanceof Class.Property)
1209                 desc = desc.value.init(k) || desc.value;
1210
1211             for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
1212                 if (orig = Object.getOwnPropertyDescriptor(obj, k))
1213                     Object.defineProperty(original, k, orig);
1214
1215             // Guard against horrible add-ons that use eval-based monkey
1216             // patching.
1217             if (callable(desc.value)) {
1218                 let value = desc.value;
1219
1220                 let sentinel = "(function DactylOverlay() {}())"
1221                 value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
1222                 value.toSource = function toSource() toString.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
1223
1224                 delete desc.value;
1225                 delete desc.writable;
1226                 desc.get = function get() value;
1227                 desc.set = function set(val) {
1228                     if (String(val).indexOf(sentinel) < 0)
1229                         Class.replaceProperty(this, k, val);
1230                     else {
1231                         let package_ = util.newURI(util.fixURI(Components.stack.caller.filename)).host;
1232                         util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
1233                         util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
1234                     }
1235                 };
1236             }
1237
1238             Object.defineProperty(object, k, desc);
1239         }, this);
1240
1241         return function unwrap() {
1242             for each (let k in Object.getOwnPropertyNames(original))
1243                 Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
1244         };
1245     },
1246
1247     overlayWindow: function (url, fn) {
1248         if (url instanceof Ci.nsIDOMWindow)
1249             util._loadOverlay(url, fn);
1250         else {
1251             Array.concat(url).forEach(function (url) {
1252                 if (!this.overlays[url])
1253                     this.overlays[url] = [];
1254                 this.overlays[url].push(fn);
1255             }, this);
1256
1257             for (let doc in util.iterDocuments())
1258                 if (["interactive", "complete"].indexOf(doc.readyState) >= 0)
1259                     this._loadOverlays(doc.defaultView);
1260                 else
1261                     this.observe(doc.defaultView, "toplevel-window-ready");
1262         }
1263     },
1264
1265     /**
1266      * Parses the fields of a form and returns a URL/POST-data pair
1267      * that is the equivalent of submitting the form.
1268      *
1269      * @param {nsINode} field One of the fields of the given form.
1270      * @returns {array}
1271      */
1272     // Nuances gleaned from browser.jar/content/browser/browser.js
1273     parseForm: function parseForm(field) {
1274         function encode(name, value, param) {
1275             param = param ? "%s" : "";
1276             if (post)
1277                 return name + "=" + encodeComponent(value + param);
1278             return encodeComponent(name) + "=" + encodeComponent(value) + param;
1279         }
1280
1281         let form = field.form;
1282         let doc = form.ownerDocument;
1283
1284         let charset = doc.characterSet;
1285         let converter = services.CharsetConv(charset);
1286         for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
1287             let c = services.CharsetConv(cs);
1288             if (c) {
1289                 converter = services.CharsetConv(cs);
1290                 charset = cs;
1291             }
1292         }
1293
1294         let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
1295         let url = util.newURI(form.action, charset, uri).spec;
1296
1297         let post = form.method.toUpperCase() == "POST";
1298
1299         let encodeComponent = encodeURIComponent;
1300         if (charset !== "UTF-8")
1301             encodeComponent = function encodeComponent(str)
1302                 escape(converter.ConvertFromUnicode(str) + converter.Finish());
1303
1304         let elems = [];
1305         if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
1306             elems.push(encode(field.name, field.value));
1307
1308         for (let [, elem] in iter(form.elements)) {
1309             if (set.has(util.editableInputs, elem.type)
1310                     || /^(?:hidden|textarea)$/.test(elem.type)
1311                     || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
1312                 elems.push(encode(elem.name, elem.value, elem === field));
1313             else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
1314                 for (let [, opt] in Iterator(elem.options))
1315                     if (opt.selected)
1316                         elems.push(encode(elem.name, opt.value));
1317             }
1318         }
1319         if (post)
1320             return [url, elems.join('&'), charset, elems];
1321         return [url + "?" + elems.join('&'), null, charset, elems];
1322     },
1323
1324     /**
1325      * A generator that returns the values between *start* and *end*, in *step*
1326      * increments.
1327      *
1328      * @param {number} start The interval's start value.
1329      * @param {number} end The interval's end value.
1330      * @param {boolean} step The value to step the range by. May be
1331      *     negative. @default 1
1332      * @returns {Iterator(Object)}
1333      */
1334     range: function range(start, end, step) {
1335         if (!step)
1336             step = 1;
1337         if (step > 0) {
1338             for (; start < end; start += step)
1339                 yield start;
1340         }
1341         else {
1342             while (start > end)
1343                 yield start += step;
1344         }
1345     },
1346
1347     /**
1348      * An interruptible generator that returns all values between *start* and
1349      * *end*. The thread yields every *time* milliseconds.
1350      *
1351      * @param {number} start The interval's start value.
1352      * @param {number} end The interval's end value.
1353      * @param {number} time The time in milliseconds between thread yields.
1354      * @returns {Iterator(Object)}
1355      */
1356     interruptibleRange: function interruptibleRange(start, end, time) {
1357         let endTime = Date.now() + time;
1358         while (start < end) {
1359             if (Date.now() > endTime) {
1360                 util.threadYield(true, true);
1361                 endTime = Date.now() + time;
1362             }
1363             yield start++;
1364         }
1365     },
1366
1367     /**
1368      * Creates a new RegExp object based on the value of expr stripped
1369      * of all white space and interpolated with the values from tokens.
1370      * If tokens, any string in the form of <key> in expr is replaced
1371      * with the value of the property, 'key', from tokens, if that
1372      * property exists. If the property value is itself a RegExp, its
1373      * source is substituted rather than its string value.
1374      *
1375      * Additionally, expr is stripped of all JavaScript comments.
1376      *
1377      * This is similar to Perl's extended regular expression format.
1378      *
1379      * @param {string|XML} expr The expression to compile into a RegExp.
1380      * @param {string} flags Flags to apply to the new RegExp.
1381      * @param {object} tokens The tokens to substitute. @optional
1382      * @returns {RegExp} A custom regexp object.
1383      */
1384     regexp: update(function (expr, flags, tokens) {
1385         flags = flags || [k for ([k, v] in Iterator({ g: "global", i: "ignorecase", m: "multiline", y: "sticky" }))
1386                           if (expr[v])].join("");
1387
1388         if (isinstance(expr, ["RegExp"]))
1389             expr = expr.source;
1390
1391         expr = String.replace(expr, /\\(.)/, function (m, m1) {
1392             if (m1 === "c")
1393                 flags = flags.replace(/i/g, "") + "i";
1394             else if (m === "C")
1395                 flags = flags.replace(/i/g, "");
1396             else
1397                 return m;
1398             return "";
1399         });
1400
1401         // Replace replacement <tokens>.
1402         if (tokens)
1403             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);
1404
1405         // Strip comments and white space.
1406         if (/x/.test(flags))
1407             expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm, function (m, m1) m1 || "");
1408
1409         // Replace (?P<named> parameters)
1410         if (/\(\?P</.test(expr)) {
1411             var source = expr;
1412             let groups = ["wholeMatch"];
1413             expr = expr.replace(/((?:[^[(\\]|\\.|\[(?:[^\]]|\\.)*\])*)\((?:\?P<([^>]+)>|(\?))?/gy,
1414                 function (m0, m1, m2, m3) {
1415                     if (!m3)
1416                         groups.push(m2 || "-group-" + groups.length);
1417                     return m1 + "(" + (m3 || "");
1418                 });
1419             var struct = Struct.apply(null, groups);
1420         }
1421
1422         let res = update(RegExp(expr, flags.replace("x", "")), {
1423             closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),
1424             dactylPropertyNames: ["exec", "match", "test", "toSource", "toString", "global", "ignoreCase", "lastIndex", "multiLine", "source", "sticky"],
1425             iterate: function (str, idx) util.regexp.iterate(this, str, idx)
1426         });
1427
1428         // Return a struct with properties for named parameters if we
1429         // have them.
1430         if (struct)
1431             update(res, {
1432                 exec: function exec() let (match = exec.superapply(this, arguments)) match && struct.fromArray(match),
1433                 dactylSource: source, struct: struct
1434             });
1435         return res;
1436     }, {
1437         /**
1438          * Escapes Regular Expression special characters in *str*.
1439          *
1440          * @param {string} str
1441          * @returns {string}
1442          */
1443         escape: function regexp_escape(str) str.replace(/([\\{}()[\]^$.?*+|])/g, "\\$1"),
1444
1445         /**
1446          * Given a RegExp, returns its source in the form showable to the user.
1447          *
1448          * @param {RegExp} re The regexp showable source of which is to be returned.
1449          * @returns {string}
1450          */
1451         getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g, function (m0, m1) m1 === "/" ? "/" : m0),
1452
1453         /**
1454          * Iterates over all matches of the given regexp in the given
1455          * string.
1456          *
1457          * @param {RegExp} regexp The regular expression to execute.
1458          * @param {string} string The string to search.
1459          * @param {number} lastIndex The index at which to begin searching. @optional
1460          */
1461         iterate: function iterate(regexp, string, lastIndex) iter(function () {
1462             regexp.lastIndex = lastIndex = lastIndex || 0;
1463             let match;
1464             while (match = regexp.exec(string)) {
1465                 lastIndex = regexp.lastIndex;
1466                 yield match;
1467                 regexp.lastIndex = lastIndex;
1468                 if (match[0].length == 0 || !regexp.global)
1469                     break;
1470             }
1471         }())
1472     }),
1473
1474     rehash: function (args) {
1475         JSMLoader.commandlineArgs = args;
1476         this.timeout(function () {
1477             this.rehashing = true;
1478             let addon = config.addon;
1479             addon.userDisabled = true;
1480             addon.userDisabled = false;
1481         });
1482     },
1483
1484     errorCount: 0,
1485     errors: Class.memoize(function () []),
1486     maxErrors: 15,
1487     reportError: function (error) {
1488         if (error.noTrace)
1489             return;
1490
1491         if (isString(error))
1492             error = Error(error);
1493
1494         if (Cu.reportError)
1495             Cu.reportError(error);
1496
1497         try {
1498             this.errorCount++;
1499
1500             let obj = update({}, error, {
1501                 toString: function () String(error),
1502                 stack: <>{util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t")}</>
1503             });
1504
1505             this.errors.push([new Date, obj + "\n" + obj.stack]);
1506             this.errors = this.errors.slice(-this.maxErrors);
1507             this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
1508
1509             this.dump(String(error));
1510             this.dump(obj);
1511             this.dump("");
1512         }
1513         catch (e) {
1514             try {
1515                 this.dump(String(error));
1516                 this.dump(util.stackLines(error.stack).join("\n"));
1517             }
1518             catch (e) { dump(e + "\n"); }
1519         }
1520
1521         // ctypes.open("libc.so.6").declare("kill", ctypes.default_abi, ctypes.void_t, ctypes.int, ctypes.int)(
1522         //     ctypes.open("libc.so.6").declare("getpid", ctypes.default_abi, ctypes.int)(), 2)
1523     },
1524
1525     /**
1526      * Given a domain, returns an array of all non-toplevel subdomains
1527      * of that domain.
1528      *
1529      * @param {string} host The host for which to find subdomains.
1530      * @returns {[string]}
1531      */
1532     subdomains: function subdomains(host) {
1533         if (/(^|\.)\d+$|:.*:/.test(host))
1534             // IP address or similar
1535             return [host];
1536
1537         let base = host.replace(/.*\.(.+?\..+?)$/, "$1");
1538         try {
1539             base = services.tld.getBaseDomainFromHost(host);
1540         }
1541         catch (e) {}
1542
1543         let ary = host.split(".");
1544         ary = [ary.slice(i).join(".") for (i in util.range(ary.length - 1, 0, -1))];
1545         return ary.filter(function (h) h.length >= base.length);
1546     },
1547
1548     /**
1549      * Scrolls an element into view if and only if it's not already
1550      * fully visible.
1551      *
1552      * @param {Node} elem The element to make visible.
1553      */
1554     scrollIntoView: function scrollIntoView(elem, alignWithTop) {
1555         let win = elem.ownerDocument.defaultView;
1556         let rect = elem.getBoundingClientRect();
1557         if (!(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0))
1558             elem.scrollIntoView(arguments.length > 1 ? alignWithTop : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom));
1559     },
1560
1561     /**
1562      * Returns the selection controller for the given window.
1563      *
1564      * @param {Window} window
1565      * @returns {nsISelectionController}
1566      */
1567     selectionController: function (win)
1568         win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1569            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
1570            .QueryInterface(Ci.nsISelectionController),
1571
1572     /**
1573      * Suspend execution for at least *delay* milliseconds. Functions by
1574      * yielding execution to the next item in the main event queue, and
1575      * so may lead to unexpected call graphs, and long delays if another
1576      * handler yields execution while waiting.
1577      *
1578      * @param {number} delay The time period for which to sleep in milliseconds.
1579      */
1580     sleep: function (delay) {
1581         let mainThread = services.threading.mainThread;
1582
1583         let end = Date.now() + delay;
1584         while (Date.now() < end)
1585             mainThread.processNextEvent(true);
1586         return true;
1587     },
1588
1589     highlightFilter: function highlightFilter(str, filter, highlight) {
1590         return this.highlightSubstrings(str, (function () {
1591             if (filter.length == 0)
1592                 return;
1593             let lcstr = String.toLowerCase(str);
1594             let lcfilter = filter.toLowerCase();
1595             let start = 0;
1596             while ((start = lcstr.indexOf(lcfilter, start)) > -1) {
1597                 yield [start, filter.length];
1598                 start += filter.length;
1599             }
1600         })(), highlight || template.filter);
1601     },
1602
1603     /**
1604      * Behaves like String.split, except that when *limit* is reached,
1605      * the trailing element contains the entire trailing portion of the
1606      * string.
1607      *
1608      *     util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"]
1609      *
1610      * @param {string} str The string to split.
1611      * @param {RegExp|string} re The regular expression on which to split the string.
1612      * @param {number} limit The maximum number of elements to return.
1613      * @returns {[string]}
1614      */
1615     split: function (str, re, limit) {
1616         re.lastIndex = 0;
1617         if (!re.global)
1618             re = RegExp(re.source || re, "g");
1619         let match, start = 0, res = [];
1620         while (--limit && (match = re.exec(str)) && match[0].length) {
1621             res.push(str.substring(start, match.index));
1622             start = match.index + match[0].length;
1623         }
1624         res.push(str.substring(start));
1625         return res;
1626     },
1627
1628     /**
1629      * Split a string on literal occurrences of a marker.
1630      *
1631      * Specifically this ignores occurrences preceded by a backslash, or
1632      * contained within 'single' or "double" quotes.
1633      *
1634      * It assumes backslash escaping on strings, and will thus not count quotes
1635      * that are preceded by a backslash or within other quotes as starting or
1636      * ending quoted sections of the string.
1637      *
1638      * @param {string} str
1639      * @param {RegExp} marker
1640      * @returns {[string]}
1641      */
1642     splitLiteral: function splitLiteral(str, marker) {
1643         let results = [];
1644         let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source);
1645         let cont = true;
1646
1647         while (cont) {
1648             cont = false;
1649             str = str.replace(resep, function (match, before) {
1650                 results.push(before);
1651                 cont = match !== "";
1652                 return "";
1653             });
1654         }
1655
1656         results.push(str);
1657         return results;
1658     },
1659
1660     yielders: 0,
1661     threadYield: function (flush, interruptable) {
1662         this.yielders++;
1663         try {
1664             let mainThread = services.threading.mainThread;
1665             /* FIXME */
1666             util.interrupted = false;
1667             do {
1668                 mainThread.processNextEvent(!flush);
1669                 if (util.interrupted)
1670                     throw new Error("Interrupted");
1671             }
1672             while (flush === true && mainThread.hasPendingEvents());
1673         }
1674         finally {
1675             this.yielders--;
1676         }
1677     },
1678
1679     waitFor: function waitFor(test, self, timeout, interruptable) {
1680         let end = timeout && Date.now() + timeout, result;
1681
1682         let timer = services.Timer(function () {}, 10, services.Timer.TYPE_REPEATING_SLACK);
1683         try {
1684             while (!(result = test.call(self)) && (!end || Date.now() < end))
1685                 this.threadYield(false, interruptable);
1686         }
1687         finally {
1688             timer.cancel();
1689         }
1690         return result;
1691     },
1692
1693     yieldable: function yieldable(func)
1694         function magic() {
1695             let gen = func.apply(this, arguments);
1696             (function next() {
1697                 try {
1698                     util.timeout(next, gen.next());
1699                 }
1700                 catch (e if e instanceof StopIteration) {};
1701             })();
1702         },
1703
1704     wrapCallback: wrapCallback,
1705
1706     /**
1707      * Returns the top-level chrome window for the given window.
1708      *
1709      * @param {Window} win The child window.
1710      * @returns {Window} The top-level parent window.
1711      */
1712     topWindow: function topWindow(win)
1713             win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1714                .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
1715                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow),
1716
1717     /**
1718      * Traps errors in the called function, possibly reporting them.
1719      *
1720      * @param {function} func The function to call
1721      * @param {object} self The 'this' object for the function.
1722      */
1723     trapErrors: function trapErrors(func, self) {
1724         try {
1725             if (isString(func))
1726                 func = self[func];
1727             return func.apply(self || this, Array.slice(arguments, 2));
1728         }
1729         catch (e) {
1730             util.reportError(e);
1731             return undefined;
1732         }
1733     },
1734
1735     urlPath: function urlPath(url) {
1736         try {
1737             return util.getFile(url).path;
1738         }
1739         catch (e) {
1740             return url;
1741         }
1742     },
1743
1744     visibleHosts: function (win) {
1745         let res = [], seen = {};
1746         (function rec(frame) {
1747             try {
1748                 res = res.concat(util.subdomains(frame.location.host));
1749             }
1750             catch (e) {}
1751             Array.forEach(frame.frames, rec);
1752         })(win);
1753         return res.filter(function (h) !set.add(seen, h));
1754     },
1755
1756     visibleURIs: function (win) {
1757         let res = [], seen = {};
1758         (function rec(frame) {
1759             try {
1760                 res = res.concat(util.newURI(frame.location.href));
1761             }
1762             catch (e) {}
1763             Array.forEach(frame.frames, rec);
1764         })(win);
1765         return res.filter(function (h) !set.add(seen, h.spec));
1766     },
1767
1768     /**
1769      * Converts an E4X XML literal to a DOM node. Any attribute named
1770      * highlight is present, it is transformed into dactyl:highlight,
1771      * and the named highlight groups are guaranteed to be loaded.
1772      *
1773      * @param {Node} node
1774      * @param {Document} doc
1775      * @param {Object} nodes If present, nodes with the "key" attribute are
1776      *     stored here, keyed to the value thereof.
1777      * @returns {Node}
1778      */
1779     xmlToDom: function xmlToDom(node, doc, nodes) {
1780         XML.prettyPrinting = false;
1781         if (typeof node === "string") // Sandboxes can't currently pass us XML objects.
1782             node = XML(node);
1783
1784         if (node.length() != 1) {
1785             let domnode = doc.createDocumentFragment();
1786             for each (let child in node)
1787                 domnode.appendChild(xmlToDom(child, doc, nodes));
1788             return domnode;
1789         }
1790
1791         switch (node.nodeKind()) {
1792         case "text":
1793             return doc.createTextNode(String(node));
1794         case "element":
1795             let domnode = doc.createElementNS(node.namespace(), node.localName());
1796
1797             for each (let attr in node.@*::*)
1798                 if (attr.name() != "highlight")
1799                     domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
1800
1801             for each (let child in node.*::*)
1802                 domnode.appendChild(xmlToDom(child, doc, nodes));
1803             if (nodes && node.@key)
1804                 nodes[node.@key] = domnode;
1805
1806             if ("@highlight" in node)
1807                 highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
1808             return domnode;
1809         default:
1810             return null;
1811         }
1812     }
1813 }, {
1814     Array: array
1815 });
1816
1817 /**
1818  * Math utility methods.
1819  * @singleton
1820  */
1821 var GlobalMath = Math;
1822 var Math = update(Object.create(GlobalMath), {
1823     /**
1824      * Returns the specified *value* constrained to the range *min* - *max*.
1825      *
1826      * @param {number} value The value to constrain.
1827      * @param {number} min The minimum constraint.
1828      * @param {number} max The maximum constraint.
1829      * @returns {number}
1830      */
1831     constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max)
1832 });
1833
1834 endModule();
1835
1836 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1837
1838 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: