move const json to only place where it is used
[conkeror.git] / modules / utils.js
blob62d47b70ec38be51ff54263382c943daae781495
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009 John J. Foerch
4  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 function string_hashset () {}
12 string_hashset.prototype = {
13     constructor : string_hashset,
15     add : function (s) {
16         this["-" + s] = true;
17     },
19     contains : function (s) {
20         return (("-" + s) in this);
21     },
23     remove : function (s) {
24         delete this["-" + s];
25     },
27     for_each : function (f) {
28         for (var i in this) {
29             if (i[0] == "-")
30                 f(i.slice(1));
31         }
32     },
34     iterator : function () {
35         for (let k in this) {
36             if (i[0] == "-")
37                 yield i.slice(1);
38         }
39     }
42 function string_hashmap () {}
44 string_hashmap.prototype = {
45     constructor : string_hashmap,
47     put : function (s,value) {
48         this["-" + s] = value;
49     },
51     contains : function (s) {
52         return (("-" + s) in this);
53     },
55     get : function (s, default_value) {
56         if (this.contains(s))
57             return this["-" + s];
58         return default_value;
59     },
61     get_put_default : function (s, default_value) {
62         if (this.contains(s))
63             return this["-" + s];
64         return (this["-" + s] = default_value);
65     },
67     remove : function (s) {
68         delete this["-" + s];
69     },
71     for_each : function (f) {
72         for (var i in this) {
73             if (i[0] == "-")
74                 f(i.slice(1), this[i]);
75         }
76     },
78     for_each_value : function (f) {
79         for (var i in this) {
80             if (i[0] == "-")
81                 f(this[i]);
82         }
83     },
85     iterator: function (only_keys) {
86         if (only_keys) {
87             for (let k in Iterator(this, true)) {
88                 if (k[0] == "-")
89                     yield k.slice(1);
90             }
91         } else {
92             for (let [k,v] in Iterator(this, false)) {
93                 if (k[0] == "-")
94                     yield [k.slice(1),v];
95             }
96         }
97     }
101 // Put the string on the clipboard
102 function writeToClipboard (str) {
103     var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
104         .getService(Ci.nsIClipboardHelper);
105     gClipboardHelper.copyString(str);
109 function makeURLAbsolute (base, url) {
110     // Construct nsIURL.
111     var ioService = Cc["@mozilla.org/network/io-service;1"]
112         .getService(Ci.nsIIOService);
113     var baseURI  = ioService.newURI(base, null, null);
114     return ioService.newURI(baseURI.resolve(url), null, null).spec;
118 function get_link_location (element) {
119     if (element && element.getAttribute("href")) {
120         var loc = element.getAttribute("href");
121         return makeURLAbsolute(element.baseURI, loc);
122     }
123     return null;
127 function make_file (path) {
128     if (path instanceof Ci.nsILocalFile)
129         return path;
130     var f = Cc["@mozilla.org/file/local;1"]
131         .createInstance(Ci.nsILocalFile);
132     f.initWithPath(path);
133     return f;
136 var io_service = Cc["@mozilla.org/network/io-service;1"]
137     .getService(Ci.nsIIOService2);
139 function make_uri (uri, charset, base_uri) {
140     if (uri instanceof Ci.nsIURI)
141         return uri;
142     if (uri instanceof Ci.nsIFile)
143         return io_service.newFileURI(uri);
144     return io_service.newURI(uri, charset, base_uri);
147 function make_file_from_chrome (url) {
148     var crs = Cc['@mozilla.org/chrome/chrome-registry;1']
149         .getService(Ci.nsIChromeRegistry);
150     var file = crs.convertChromeURL(make_uri(url));
151     return make_file(file.path);
154 function get_document_content_disposition (document_o) {
155     var content_disposition = null;
156     try {
157         content_disposition = document_o.defaultView
158             .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
159             .getInterface(Components.interfaces.nsIDOMWindowUtils)
160             .getDocumentMetadata("content-disposition");
161     } catch (e) { }
162     return content_disposition;
166 function set_focus_no_scroll (window, element) {
167     window.document.commandDispatcher.suppressFocusScroll = true;
168     element.focus();
169     window.document.commandDispatcher.suppressFocusScroll = false;
172 function do_repeatedly_positive (func, n) {
173     var args = Array.prototype.slice.call(arguments, 2);
174     while (n-- > 0)
175         func.apply(null, args);
178 function do_repeatedly (func, n, positive_args, negative_args) {
179     if (n < 0)
180         do func.apply(null, negative_args); while (++n < 0);
181     else
182         while (n-- > 0) func.apply(null, positive_args);
185 // remove whitespace from the beginning and end
186 function trim_whitespace (str) {
187     var tmp = new String(str);
188     return tmp.replace(/^\s+/, "").replace(/\s+$/, "");
192  * Given a node, returns its position relative to the document.
194  * @param node The node to get the position of.
195  * @return An object with properties "x" and "y" representing its offset from
196  *         the left and top of the document, respectively.
197  */
198 function abs_point (node) {
199     var orig = node;
200     var pt = {};
201     try {
202         pt.x = node.offsetLeft;
203         pt.y = node.offsetTop;
204         // find imagemap's coordinates
205         if (node.tagName == "AREA") {
206             var coords = node.getAttribute("coords").split(",");
207             pt.x += Number(coords[0]);
208             pt.y += Number(coords[1]);
209         }
211         node = node.offsetParent;
212         // Sometimes this fails, so just return what we got.
214         while (node.tagName != "BODY") {
215             pt.x += node.offsetLeft;
216             pt.y += node.offsetTop;
217             node = node.offsetParent;
218         }
219     } catch(e) {
220 //      node = orig;
221 //      while (node.tagName != "BODY") {
222 //          alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
223 //          node = node.offsetParent;
224 //      }
225     }
226     return pt;
231  * get_os returns a string identifying the current OS.
232  * possible values include 'Darwin', 'Linux' and 'WINNT'.
233  */
234 let (xul_runtime = Cc['@mozilla.org/xre/app-info;1']
235          .getService(Ci.nsIXULRuntime)) {
236     function get_os () {
237         return xul_runtime.OS;
238     }
243  * getenv returns the value of a named environment variable or null if
244  * the environment variable does not exist.
245  */
246 let (env = Cc['@mozilla.org/process/environment;1']
247          .getService(Ci.nsIEnvironment)) {
248     function getenv (variable) {
249         if (env.exists(variable))
250             return env.get(variable);
251         return null;
252     }
257  * get_home_directory returns an nsILocalFile object of the user's
258  * home directory.
259  */
260 function get_home_directory () {
261     var dir = Cc["@mozilla.org/file/local;1"]
262         .createInstance(Ci.nsILocalFile);
263     if (get_os() == "WINNT")
264         dir.initWithPath(getenv('USERPROFILE') ||
265                          getenv('HOMEDRIVE') + getenv('HOMEPATH'));
266     else
267         dir.initWithPath(getenv('HOME'));
268     return dir;
272 const XHTML_NS = "http://www.w3.org/1999/xhtml";
273 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
274 const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
275 const XLINK_NS = "http://www.w3.org/1999/xlink";
277 function create_XUL (window, tag_name) {
278     return window.document.createElementNS(XUL_NS, tag_name);
282 /* Used in calls to XPath evaluate */
283 function xpath_lookup_namespace (prefix) {
284     return {
285         xhtml: XHTML_NS,
286         m: MATHML_NS,
287         xul: XUL_NS
288     }[prefix] || null;
291 function method_caller (obj, func) {
292     return function () {
293         func.apply(obj, arguments);
294     };
297 function shell_quote (str) {
298     var s = str.replace("\"", "\\\"", "g");
299     s = s.replace("$", "\$", "g");
300     return s;
303 /* Like perl's quotemeta. Backslash all non-alphanumerics. */
304 function quotemeta (str) {
305     return str.replace(/([^a-zA-Z0-9])/g, "\\$1");
308 /* Given a list of choices (strings), return a regex which matches any
309    of them*/
310 function choice_regex (choices) {
311     var regex = "(?:" + choices.map(quotemeta).join("|") + ")";
312     return regex;
315 function get_window_from_frame (frame) {
316     try {
317         var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
318             .getInterface(Ci.nsIWebNavigation)
319             .QueryInterface(Ci.nsIDocShellTreeItem)
320             .rootTreeItem
321             .QueryInterface(Ci.nsIInterfaceRequestor)
322             .getInterface(Ci.nsIDOMWindow).wrappedJSObject;
323         /* window is now an XPCSafeJSObjectWrapper */
324         window.escape_wrapper(function (w) { window = w; });
325         /* window is now completely unwrapped */
326         return window;
327     } catch (e) {
328         return null;
329     }
332 function get_buffer_from_frame (window, frame) {
333     var count = window.buffers.count;
334     for (var i = 0; i < count; ++i) {
335         var b = window.buffers.get_buffer(i);
336         if (b.top_frame == frame.top)
337             return b;
338     }
339     return null;
342 var file_locator = Cc["@mozilla.org/file/directory_service;1"]
343     .getService(Ci.nsIProperties);
345 function get_shortdoc_string (doc) {
346     var shortdoc = null;
347     if (doc != null) {
348         var idx = doc.indexOf("\n");
349         if (idx >= 0)
350             shortdoc = doc.substring(0,idx);
351         else
352             shortdoc = doc;
353     }
354     return shortdoc;
357 var conkeror_source_code_path = null;
359 function source_code_reference (uri, line_number) {
360     this.uri = uri;
361     this.line_number = line_number;
363 source_code_reference.prototype = {
364     get module_name () {
365         if (this.uri.indexOf(module_uri_prefix) == 0)
366             return this.uri.substring(module_uri_prefix.length);
367         return null;
368     },
370     get file_name () {
371         var file_uri_prefix = "file://";
372         if (this.uri.indexOf(file_uri_prefix) == 0)
373             return this.uri.substring(file_uri_prefix.length);
374         return null;
375     },
377     get best_uri () {
378         if (conkeror_source_code_path != null) {
379             var module_name = this.module_name;
380             if (module_name != null)
381                 return "file://" + conkeror_source_code_path + "/modules/" + module_name;
382         }
383         return this.uri;
384     },
386     open_in_editor : function() {
387         yield open_with_external_editor(load_spec(this.best_uri),
388                                         $line = this.line_number);
389     }
392 var get_caller_source_code_reference_ignored_functions = {};
394 function get_caller_source_code_reference (extra_frames_back) {
395     /* Skip at least this function itself and whoever called it (and
396      * more if the caller wants to be skipped). */
397     var frames_to_skip = 2;
398     if (extra_frames_back != null)
399         frames_to_skip += extra_frames_back;
401     for (let f = Components.stack; f != null; f = f.caller) {
402         if (frames_to_skip > 0) {
403             --frames_to_skip;
404             continue;
405         }
406         if (get_caller_source_code_reference_ignored_functions[f.name])
407             continue;
408         return new source_code_reference(f.filename, f.lineNumber);
409     }
411     return null;
414 function ignore_function_for_get_caller_source_code_reference (func_name) {
415     get_caller_source_code_reference_ignored_functions[func_name] = 1;
418 require_later("external-editor.js");
420 function dom_generator (document, ns) {
421     this.document = document;
422     this.ns = ns;
424 dom_generator.prototype = {
425     element : function (tag, parent) {
426         var node = this.document.createElementNS(this.ns, tag);
427         var i = 1;
428         if (parent != null && (parent instanceof Ci.nsIDOMNode)) {
429             parent.appendChild(node);
430             i = 2;
431         }
432         for (var nargs = arguments.length; i < nargs; i += 2)
433             node.setAttribute(arguments[i], arguments[i+1]);
434         return node;
435     },
437     text : function (str, parent) {
438         var node = this.document.createTextNode(str);
439         if (parent)
440             parent.appendChild(node);
441         return node;
442     },
445     stylesheet_link : function (href, parent) {
446         var node = this.element("link");
447         node.setAttribute("rel", "stylesheet");
448         node.setAttribute("type", "text/css");
449         node.setAttribute("href", href);
450         if (parent)
451             parent.appendChild(node);
452         return node;
453     },
456     add_stylesheet : function (url) {
457         var head = this.document.documentElement.firstChild;
458         this.stylesheet_link(url, head);
459     }
463  * Generates a QueryInterface function suitable for an implemenation
464  * of an XPCOM interface.  Unlike XPCOMUtils, this uses the Function
465  * constructor to generate a slightly more efficient version.  The
466  * arguments can be either Strings or elements of
467  * Components.interfaces.
468  */
469 function generate_QI () {
470     var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
471     var fstr = "if(" +
472         Array.prototype.map.call(args, function (x) {
473             return "iid.equals(Components.interfaces." + x + ")";
474         })
475         .join("||") +
476         ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
477     return new Function("iid", fstr);
480 function set_branch_pref (branch, name, value) {
481     if (typeof(value) == "string") {
482         branch.setCharPref(name, value);
483     } else if (typeof(value) == "number") {
484         branch.setIntPref(name, value);
485     } else if (typeof(value) == "boolean") {
486         branch.setBoolPref(name, value);
487     }
490 function default_pref (name, value) {
491     var branch = preferences.getDefaultBranch(null);
492     set_branch_pref(branch, name, value);
495 function user_pref (name, value) {
496     var branch = preferences.getBranch(null);
497     set_branch_pref(branch, name, value);
500 function get_branch_pref (branch, name) {
501     switch (branch.getPrefType(name)) {
502     case branch.PREF_STRING:
503         return branch.getCharPref(name);
504     case branch.PREF_INT:
505         return branch.getIntPref(name);
506     case branch.PREF_BOOL:
507         return branch.getBoolPref(name);
508     default:
509         return null;
510     }
513 function get_localized_pref (name) {
514     try {
515         return preferences.getBranch(null).getComplexValue(name, Ci.nsIPrefLocalizedString).data;
516     } catch (e) {
517         return null;
518     }
521 function get_pref (name) {
522     var branch = preferences.getBranch(null);
523     return get_branch_pref(branch, name);
526 function get_default_pref (name) {
527     var branch = preferences.getDefaultBranch(null);
528     return get_branch_pref(branch, name);
531 function clear_pref (name) {
532     var branch = preferences.getBranch(null);
533     return branch.clearUserPref(name);
536 function pref_has_user_value (name) {
537     var branch = preferences.getBranch(null);
538     return branch.prefHasUserValue(name);
541 function pref_has_default_value (name) {
542     var branch = preferences.getDefaultBranch(null);
543     return branch.prefHasUserValue(name);
546 function session_pref (name, value) {
547     try {
548         clear_pref (name);
549     } catch (e) {}
550     return default_pref(name, value);
553 function watch_pref (pref, hook) {
554     /* Extract pref into branch.pref */
555     let match = pref.match(/^(.*[.])?([^.]*)$/);
556     let br = match[1];
557     let key = match[2];
558     let branch = preferences.getBranch(br).QueryInterface(Ci.nsIPrefBranch2);
559     let observer = {
560         observe: function (subject, topic, data) {
561             if (topic == "nsPref:changed" && data == key) {
562                 hook();
563             }
564         }
565     };
566     branch.addObserver("", observer, false);
569 const LOCALE_PREF = "general.useragent.locale";
571 function get_locale () {
572     return get_localized_pref(LOCALE_PREF) || get_pref(LOCALE_PREF);
575 const USER_AGENT_OVERRIDE_PREF = "general.useragent.override";
577 function set_user_agent (str) {
578     session_pref(USER_AGENT_OVERRIDE_PREF, str);
581 function define_builtin_commands (prefix, do_command_function, toggle_mark, mark_active_predicate, mode) {
583     // Specify a docstring
584     function D (cmd, docstring) {
585         var o = new String(cmd);
586         o.doc = docstring;
587         return o;
588     }
590     // Specify a forward/reverse pair
591     function R (a, b) {
592         var o = [a,b];
593         o.is_reverse_pair = true;
594         return o;
595     }
597     // Specify a movement/select/scroll/move-caret command group.
598     function S (command, movement, select, scroll, caret) {
599         var o = [movement, select, scroll, caret];
600         o.command = command;
601         o.is_move_select_pair = true;
602         return o;
603     }
605     var builtin_commands = [
607         /*
608          * cmd_scrollBeginLine and cmd_scrollEndLine don't do what I
609          * want, either in or out of caret mode...
610          */
611         S(D("beginning-of-line", "Move or extend the selection to the beginning of the current line."),
612           D("cmd_beginLine", "Move point to the beginning of the current line."),
613           D("cmd_selectBeginLine", "Extend selection to the beginning of the current line."),
614           D("cmd_beginLine", "Scroll to the beginning of the line"),
615           D("cmd_beginLine", "Scroll to the beginning of the line")),
616         S(D("end-of-line", "Move or extend the selection to the end of the current line."),
617           D("cmd_endLine", "Move point to the end of the current line."),
618           D("cmd_selectEndLine", "Extend selection to the end of the current line."),
619           D("cmd_endLine", "Scroll to the end of the current line."),
620           D("cmd_endLine", "Scroll to the end of the current line.")),
621         S(D("beginning-of-first-line", "Move or extend the selection to the beginning of the first line."),
622           D("cmd_moveTop", "Move point to the beginning of the first line."),
623           D("cmd_selectTop", "Extend selection to the beginning of the first line."),
624           D("cmd_scrollTop", "Scroll to the top of the buffer"),
625           D("cmd_scrollTop", "Move point to the beginning of the first line.")),
626         S(D("end-of-last-line", "Move or extend the selection to the end of the last line."),
627           D("cmd_moveBottom", "Move point to the end of the last line."),
628           D("cmd_selectBottom", "Extend selection to the end of the last line."),
629           D("cmd_scrollBottom", "Scroll to the bottom of the buffer"),
630           D("cmd_scrollBottom", "Move point to the end of the last line.")),
631         "cmd_copyOrDelete",
632         "cmd_scrollBeginLine",
633         "cmd_scrollEndLine",
634         "cmd_cutOrDelete",
635         D("cmd_copy", "Copy the selection into the clipboard."),
636         D("cmd_cut", "Cut the selection into the clipboard."),
637         D("cmd_deleteToBeginningOfLine", "Delete to the beginning of the current line."),
638         D("cmd_deleteToEndOfLine", "Delete to the end of the current line."),
639         D("cmd_selectAll", "Select all."),
640         D("cmd_scrollTop", "Scroll to the top of the buffer."),
641         D("cmd_scrollBottom", "Scroll to the bottom of the buffer.")];
643     var builtin_commands_with_count = [
644         R(S(D("forward-char", "Move or extend the selection forward one character."),
645             D("cmd_charNext", "Move point forward one character."),
646             D("cmd_selectCharNext", "Extend selection forward one character."),
647             D("cmd_scrollRight", "Scroll to the right"),
648             D("cmd_scrollRight", "Scroll to the right")),
649           S(D("backward-char", "Move or extend the selection backward one character."),
650             D("cmd_charPrevious", "Move point backward one character."),
651             D("cmd_selectCharPrevious", "Extend selection backward one character."),
652             D("cmd_scrollLeft", "Scroll to the left."),
653             D("cmd_scrollLeft", "Scroll to the left."))),
654         R(D("cmd_deleteCharForward", "Delete the following character."),
655           D("cmd_deleteCharBackward", "Delete the previous character.")),
656         R(D("cmd_deleteWordForward", "Delete the following word."),
657           D("cmd_deleteWordBackward", "Delete the previous word.")),
658         R(S(D("forward-line", "Move or extend the selection forward one line."),
659             D("cmd_lineNext", "Move point forward one line."),
660             D("cmd_selectLineNext", "Extend selection forward one line."),
661             D("cmd_scrollLineDown", "Scroll down one line."),
662             D("cmd_scrollLineDown", "Scroll down one line.")),
663           S(D("backward-line", "Move or extend the selection backward one line."),
664             D("cmd_linePrevious", "Move point backward one line."),
665             D("cmd_selectLinePrevious", "Extend selection backward one line."),
666             D("cmd_scrollLineUp", "Scroll up one line."),
667             D("cmd_scrollLineUp", "Scroll up one line."))),
668         R(S(D("forward-page", "Move or extend the selection forward one page."),
669             D("cmd_movePageDown", "Move point forward one page."),
670             D("cmd_selectPageDown", "Extend selection forward one page."),
671             D("cmd_scrollPageDown", "Scroll forward one page."),
672             D("cmd_movePageDown", "Move point forward one page.")),
673           S(D("backward-page", "Move or extend the selection backward one page."),
674             D("cmd_movePageUp", "Move point backward one page."),
675             D("cmd_selectPageUp", "Extend selection backward one page."),
676             D("cmd_scrollPageUp", "Scroll backward one page."),
677             D("cmd_movePageUp", "Move point backward one page."))),
678         R(D("cmd_undo", "Undo last editing action."),
679           D("cmd_redo", "Redo last editing action.")),
680         R(S(D("forward-word", "Move or extend the selection forward one word."),
681             D("cmd_wordNext", "Move point forward one word."),
682             D("cmd_selectWordNext", "Extend selection forward one word."),
683             D("cmd_scrollRight", "Scroll to the right."),
684             D("cmd_wordNext", "Move point forward one word.")),
685           S(D("backward-word", "Move or extend the selection backward one word."),
686             D("cmd_wordPrevious", "Move point backward one word."),
687             D("cmd_selectWordPrevious", "Extend selection backward one word."),
688             D("cmd_scrollLeft", "Scroll to the left."),
689             D("cmd_wordPrevious", "Move point backward one word."))),
690         R(D("cmd_scrollPageUp", "Scroll up one page."),
691           D("cmd_scrollPageDown", "Scroll down one page.")),
692         R(D("cmd_scrollLineUp", "Scroll up one line."),
693           D("cmd_scrollLineDown", "Scroll down one line.")),
694         R(D("cmd_scrollLeft", "Scroll left."),
695           D("cmd_scrollRight", "Scroll right.")),
696         D("cmd_paste", "Insert the contents of the clipboard.")];
698     interactive(prefix + "set-mark",
699                 "Toggle whether the mark is active.\n" +
700                 "When the mark is active, movement commands affect the selection.",
701                 toggle_mark);
703     function get_mode_idx () {
704         if (mode == 'scroll') return 2;
705         else if (mode == 'caret') return 3;
706         else return 0;
707     }
709     function get_move_select_idx (I) {
710         return mark_active_predicate(I) ? 1 : get_mode_idx();
711     }
713     function doc_for_builtin (c) {
714         var s = "";
715         if (c.doc != null)
716             s += c.doc + "\n";
717         return s + "Run the built-in command " + c + ".";
718     }
720     function define_simple_command (c) {
721         interactive(prefix + c, doc_for_builtin(c), function (I) { do_command_function(I, c); });
722     }
724     function get_move_select_doc_string (c) {
725         return c.command.doc +
726             "\nSpecifically, if the mark is active, runs `" + prefix + c[1] + "'.  " +
727             "Otherwise, runs `" + prefix + c[get_mode_idx()] + "'\n" +
728             "To toggle whether the mark is active, use `" + prefix + "set-mark'.";
729     }
731     for each (let c_temp in builtin_commands) {
732         let c = c_temp;
733         if (c.is_move_select_pair) {
734             interactive(prefix + c.command, get_move_select_doc_string(c), function (I) {
735                 var idx = get_move_select_idx(I);
736                 do_command_function(I, c[idx]);
737             });
738             define_simple_command(c[0]);
739             define_simple_command(c[1]);
740         }
741         else
742             define_simple_command(c);
743     }
745     function get_reverse_pair_doc_string (main_doc, alt_command) {
746         return main_doc + "\n" +
747             "The prefix argument specifies a repeat count for this command.  " +
748             "If the count is negative, `" + prefix + alt_command + "' is performed instead with " +
749             "a corresponding positive repeat count.";
750     }
752     function define_simple_reverse_pair (a, b) {
753         interactive(prefix + a, get_reverse_pair_doc_string(doc_for_builtin(a), b),
754                     function (I) {
755                         do_repeatedly(do_command_function, I.p, [I, a], [I, b]);
756                     });
757         interactive(prefix + b, get_reverse_pair_doc_string(doc_for_builtin(b), a),
758                     function (I) {
759                         do_repeatedly(do_command_function, I.p, [I, b], [I, a]);
760                     });
761     }
763     for each (let c_temp in builtin_commands_with_count) {
764         let c = c_temp;
765         if (c.is_reverse_pair) {
766             if (c[0].is_move_select_pair) {
767                 interactive(prefix + c[0].command, get_reverse_pair_doc_string(get_move_select_doc_string(c[0]),
768                                                                                c[1].command),
769                             function (I) {
770                                 var idx = get_move_select_idx(I);
771                                 do_repeatedly(do_command_function, I.p, [I, c[0][idx]], [I, c[1][idx]]);
772                             });
773                 interactive(prefix + c[1].command, get_reverse_pair_doc_string(get_move_select_doc_string(c[1]),
774                                                                                c[0].command),
775                             function (I) {
776                                 var idx = get_move_select_idx(I);
777                                 do_repeatedly(do_command_function, I.p, [I, c[1][idx]], [I, c[0][idx]]);
778                             });
779                 define_simple_reverse_pair(c[0][0], c[1][0]);
780                 define_simple_reverse_pair(c[0][1], c[1][1]);
781             } else
782                 define_simple_reverse_pair(c[0], c[1]);
783         } else {
784             let doc = doc_for_builtin(c) +
785                 "\nThe prefix argument specifies a positive repeat count for this command.";
786             interactive(prefix + c, doc, function (I) {
787                 do_repeatedly_positive(do_command_function, I.p, I, c);
788             });
789         }
790     }
793 var observer_service = Cc["@mozilla.org/observer-service;1"]
794     .getService(Ci.nsIObserverService);
796 function abort (str) {
797     var e = new Error(str);
798     e.__proto__ = abort.prototype;
799     return e;
801 abort.prototype.__proto__ = Error.prototype;
804 function get_temporary_file (name) {
805     if (name == null)
806         name = "temp.txt";
807     var file = file_locator.get("TmpD", Ci.nsIFile);
808     file.append(name);
809     // Create the file now to ensure that no exploits are possible
810     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
811     return file;
815 /* FIXME: This should be moved somewhere else, perhaps. */
816 function create_info_panel (window, panel_class, row_arr) {
817     /* Show information panel above minibuffer */
819     var g = new dom_generator(window.document, XUL_NS);
821     var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
822     var grid = g.element("grid", p);
823     var cols = g.element("columns", grid);
824     g.element("column", cols, "flex", "0");
825     g.element("column", cols, "flex", "1");
827     var rows = g.element("rows", grid);
828     var row;
830     for each (let [row_class, row_label, row_value] in row_arr) {
831         row = g.element("row", rows, "class", row_class);
832         g.element("label", row,
833                   "value", row_label,
834                   "class", "panel-row-label");
835         g.element("label", row,
836                   "value", row_value,
837                   "class", "panel-row-value",
838                   "crop", "end");
839     }
840     window.minibuffer.insert_before(p);
842     p.destroy = function () {
843         this.parentNode.removeChild(this);
844     };
846     return p;
851  * Paste from the X primary selection, unless the system doesn't support a
852  * primary selection, in which case fall back to the clipboard.
853  */
854 function read_from_x_primary_selection () {
855     // Get clipboard.
856     let clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
857         .getService(Components.interfaces.nsIClipboard);
859     // Fall back to global clipboard if the system doesn't support a selection
860     let which_clipboard = clipboard.supportsSelectionClipboard() ?
861         clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
863     let flavors = ["text/unicode"];
865     // Don't barf if there's nothing on the clipboard
866     if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
867         return "";
869     // Create transferable that will transfer the text.
870     let trans = Components.classes["@mozilla.org/widget/transferable;1"]
871         .createInstance(Components.interfaces.nsITransferable);
873     for each (let flavor in flavors) {
874         trans.addDataFlavor(flavor);
875     }
876     clipboard.getData(trans, which_clipboard);
878     var data_flavor = {};
879     var data = {};
880     var dataLen = {};
881     trans.getAnyTransferData(data_flavor, data, dataLen);
883     if (data) {
884         data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
885         let data_length = dataLen.value;
886         if (data_flavor.value == "text/unicode")
887             data_length = dataLen.value / 2;
888         return data.data.substring(0, data_length);
889     } else {
890         return "";
891     }
894 var user_variables = {};
896 function define_variable (name, default_value, doc) {
897     conkeror[name] = default_value;
898     user_variables[name] = {
899         default_value: default_value,
900         doc: doc,
901         shortdoc: get_shortdoc_string(doc),
902         source_code_reference: get_caller_source_code_reference()
903     };
906 function define_special_variable (name, getter, setter, doc) {
907     conkeror.__defineGetter__(name, getter);
908     conkeror.__defineSetter__(name, setter);
909     user_variables[name] = {
910         default_value: undefined,
911         doc: doc,
912         shortdoc: get_shortdoc_string(doc),
913         source_code_reference: get_caller_source_code_reference()
914     };
917 /* Re-define load_paths as a user variable. */
918 define_variable("load_paths", load_paths,
919                 "Array of URL prefixes searched in order when loading a module.\n" +
920                 "Each entry must end in a slash, and should begin with file:// or chrome://.");
923  * Stylesheets
924  */
925 function register_user_stylesheet (url) {
926     var uri = make_uri(url);
927     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
928         .getService(Ci.nsIStyleSheetService);
929     sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
932 function unregister_user_stylesheet (url) {
933     var uri = make_uri(url);
934     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
935         .getService(Ci.nsIStyleSheetService);
936     if (sss.sheetRegistered(uri, sss.USER_SHEET))
937         sss.unregisterSheet(uri, sss.USER_SHEET);
940 function register_agent_stylesheet (url) {
941     var uri = make_uri(url);
942     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
943         .getService(Ci.nsIStyleSheetService);
944     sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET);
947 function unregister_agent_stylesheet (url) {
948     var uri = make_uri(url);
949     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
950         .getService(Ci.nsIStyleSheetService);
951     if (sss.sheetRegistered(uri, sss.AGENT_SHEET))
952         sss.unregisterSheet(uri, sss.AGENT_SHEET);
955 function agent_stylesheet_registered_p (url) {
956     var uri = make_uri(url);
957     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
958         .getService(Ci.nsIStyleSheetService);
959     return sss.sheetRegistered(uri, sss.AGENT_SHEET);
962 function user_stylesheet_registered_p (url) {
963     var uri = make_uri(url);
964     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
965         .getService(Ci.nsIStyleSheetService);
966     return sss.sheetRegistered(uri, sss.USER_SHEET);
971 function predicate_alist_match (alist, key) {
972     for each (let i in alist) {
973         if (i[0](key))
974             return i[1];
975     }
976     return undefined;
980 function get_meta_title (doc) {
981     var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
982                              Ci.nsIDOMXPathResult.STRING_TYPE , null);
983     if (title && title.stringValue)
984         return title.stringValue;
985     return null;
988 var rdf_service = Cc["@mozilla.org/rdf/rdf-service;1"]
989     .getService(Ci.nsIRDFService);
991 const PREFIX_ITEM_URI = "urn:mozilla:item:";
992 const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
994 var extension_manager = Cc["@mozilla.org/extensions/manager;1"]
995     .getService(Ci.nsIExtensionManager);
997 function get_extension_rdf_property (id, name, type) {
998     var value = extension_manager.datasource.GetTarget(
999         rdf_service.GetResource(PREFIX_ITEM_URI + id),
1000         rdf_service.GetResource(PREFIX_NS_EM + name),
1001         true);
1002     if (value == null)
1003         return null;
1004     return value.QueryInterface(type || Ci.nsIRDFLiteral).Value;
1007 function get_extension_update_item (id) {
1008     return extension_manager.getItemForID(id);
1011 function extension_info (id) {
1012     this.id = id;
1014 extension_info.prototype = {
1015     // Returns the nsIUpdateItem object associated with this extension
1016     get update_item () { return get_extension_update_item(this.id); },
1018     get_rdf_property : function (name, type) {
1019         return get_extension_rdf_property(this.id, name, type);
1020     },
1022     // RDF properties
1023     get isDisabled () { return this.get_rdf_property("isDisabled"); },
1024     get aboutURL () { return this.get_rdf_property("aboutURL"); },
1025     get addonID () { return this.get_rdf_property("addonID"); },
1026     get availableUpdateURL () { return this.get_rdf_property("availableUpdateURL"); },
1027     get availableUpdateVersion () { return this.get_rdf_property("availableUpdateVersion"); },
1028     get blocklisted () { return this.get_rdf_property("blocklisted"); },
1029     get compatible () { return this.get_rdf_property("compatible"); },
1030     get description () { return this.get_rdf_property("description"); },
1031     get downloadURL () { return this.get_rdf_property("downloadURL"); },
1032     get isDisabled () { return this.get_rdf_property("isDisabled"); },
1033     get hidden () { return this.get_rdf_property("hidden"); },
1034     get homepageURL () { return this.get_rdf_property("homepageURL"); },
1035     get iconURL () { return this.get_rdf_property("iconURL"); },
1036     get internalName () { return this.get_rdf_property("internalName"); },
1037     get locked () { return this.get_rdf_property("locked"); },
1038     get name () { return this.get_rdf_property("name"); },
1039     get optionsURL () { return this.get_rdf_property("optionsURL"); },
1040     get opType () { return this.get_rdf_property("opType"); },
1041     get plugin () { return this.get_rdf_property("plugin"); },
1042     get previewImage () { return this.get_rdf_property("previewImage"); },
1043     get satisfiesDependencies () { return this.get_rdf_property("satisfiesDependencies"); },
1044     get providesUpdatesSecurely () { return this.get_rdf_property("providesUpdatesSecurely"); },
1045     get type () { return this.get_rdf_property("type", Ci.nsIRDFInt); },
1046     get updateable () { return this.get_rdf_property("updateable"); },
1047     get updateURL () { return this.get_rdf_property("updateURL"); },
1048     get version () { return this.get_rdf_property("version"); }
1051 function extension_is_enabled (id) {
1052     var info = new extension_info(id);
1053     return info.update_item && (info.isDisabled == "false");
1056 function queue () {
1057     this.input = [];
1058     this.output = [];
1060 queue.prototype = {
1061     get length () {
1062         return this.input.length + this.output.length;
1063     },
1064     push: function (x) {
1065         this.input[this.input.length] = x;
1066     },
1067     pop: function (x) {
1068         let l = this.output.length;
1069         if (!l) {
1070             l = this.input.length;
1071             if (!l)
1072                 return undefined;
1073             this.output = this.input.reverse();
1074             this.input = [];
1075             let x = this.output[l];
1076             this.output.length--;
1077             return x;
1078         }
1079     }
1082 function frame_iterator (root_frame, start_with) {
1083     var q = new queue, x;
1084     if (start_with) {
1085         x = start_with;
1086         do {
1087             yield x;
1088             for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
1089                 q.push(x.frames[i]);
1090         } while ((x = q.pop()));
1091     }
1092     x = root_frame;
1093     do {
1094         if (x == start_with)
1095             continue;
1096         yield x;
1097         for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
1098             q.push(x.frames[i]);
1099     } while ((x = q.pop()));
1102 function xml_http_request () {
1103     return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
1104         .createInstance(Ci.nsIXMLHttpRequest)
1105         .QueryInterface(Ci.nsIJSXMLHttpRequest)
1106         .QueryInterface(Ci.nsIDOMEventTarget);
1109 var xml_http_request_load_listener = {
1110   // nsIBadCertListener2
1111   notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
1112     return true;
1113   },
1115   // nsISSLErrorListener
1116   notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
1117     return true;
1118   },
1120   // nsIInterfaceRequestor
1121   getInterface: function SSLL_getInterface (iid) {
1122     return this.QueryInterface(iid);
1123   },
1125   // nsISupports
1126   //
1127   // FIXME: array comprehension used here to hack around the lack of
1128   // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
1129   // make it a simple generateQI when xulrunner is more stable.
1130   QueryInterface: XPCOMUtils.generateQI(
1131       [i for each (i in [Ci.nsIBadCertListener2,
1132                          Ci.nsISSLErrorListener,
1133                          Ci.nsIInterfaceRequestor])
1134        if (i)])
1139  * Coroutine interface for sending an HTTP request and waiting for the
1140  * response. (This includes so-called "AJAX" requests.)
1142  * @param lspec (required) a load_spec object or URI string (see load-spec.js)
1144  * The request URI is obtained from this argument. In addition, if the
1145  * load spec specifies post data, a POST request is made instead of a
1146  * GET request, and the post data included in the load spec is
1147  * sent. Specifically, the request_mime_type and raw_post_data
1148  * properties of the load spec are used.
1150  * @param $user (optional) HTTP user name to include in the request headers
1151  * @param $password (optional) HTTP password to include in the request headers
1153  * @param $override_mime_type (optional) Force the response to be interpreted
1154  *                            as having the specified MIME type.  This is only
1155  *                            really useful for forcing the MIME type to be
1156  *                            text/xml or something similar, such that it is
1157  *                            automatically parsed into a DOM document.
1158  * @param $headers (optional) an array of [name,value] pairs (each specified as
1159  *                 a two-element array) specifying additional headers to add to
1160  *                 the request.
1162  * @returns After the request completes (either successfully or with an error),
1163  *          the nsIXMLHttpRequest object is returned.  Its responseText (for any
1164  *          arbitrary document) or responseXML (if the response type is an XML
1165  *          content type) properties can be accessed to examine the response
1166  *          document.
1168  * If an exception is thrown to the continutation (which can be obtained by the
1169  * caller by calling yield CONTINUATION prior to calling this function) while the
1170  * request is in progress (i.e. before this coroutine returns), the request will
1171  * be aborted, and the exception will be propagated to the caller.
1173  **/
1174 define_keywords("$user", "$password", "$override_mime_type", "$headers");
1175 function send_http_request (lspec) {
1176     // why do we get warnings in jsconsole unless we initialize the
1177     // following keywords?
1178     keywords(arguments, $user = undefined, $password = undefined,
1179              $override_mime_type = undefined, $headers = undefined);
1180     if (! (lspec instanceof load_spec))
1181         lspec = load_spec(lspec);
1182     var req = xml_http_request();
1183     var cc = yield CONTINUATION;
1184     var aborting = false;
1185     req.onreadystatechange = function send_http_request__onreadysatechange () {
1186         if (req.readyState != 4)
1187             return;
1188         if (aborting)
1189             return;
1190         cc();
1191     };
1193     if (arguments.$override_mime_type)
1194         req.overrideMimeType(arguments.$override_mime_type);
1196     var post_data = load_spec_raw_post_data(lspec);
1198     var method = post_data ? "POST" : "GET";
1200     req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
1201     req.channel.notificationCallbacks = xml_http_request_load_listener;
1203     for each (let [name,value] in arguments.$headers) {
1204         req.setRequestHeader(name, value);
1205     }
1207     if (post_data) {
1208         req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
1209         req.send(post_data);
1210     } else
1211         req.send(null);
1213     try {
1214         yield SUSPEND;
1215     } catch (e) {
1216         aborting = true;
1217         req.abort();
1218         throw e;
1219     }
1221     // Let the caller access the status and reponse data
1222     yield co_return(req);
1227  * scroll_selection_into_view takes an editable element, and scrolls it so
1228  * that the selection (or insertion point) are visible.
1229  */
1230 function scroll_selection_into_view (field) {
1231     if (field.namespaceURI == XUL_NS)
1232         field = field.inputField;
1233     try {
1234         field.QueryInterface(Ci.nsIDOMNSEditableElement)
1235             .editor
1236             .selectionController
1237             .scrollSelectionIntoView(
1238                 Ci.nsISelectionController.SELECTION_NORMAL,
1239                 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
1240                 true);
1241     } catch (e) {
1242         // we'll get here for richedit fields
1243     }
1248  * build_url_regex builds a regular expression to match URLs for a given
1249  * web site.
1251  * Both the $domain and $path arguments can be either regexes, in
1252  * which case they will be matched as is, or strings, in which case
1253  * they will be matched literally.
1255  * $tlds specifies a list of valid top-level-domains to match, and
1256  * defaults to .com. Useful for when e.g. foo.org and foo.com are the
1257  * same.
1259  * If $allow_www is true, www.domain.tld will also be allowed.
1260  */
1261 define_keywords("$domain", "$path", "$tlds", "$allow_www");
1262 function build_url_regex () {
1263     function regex_to_string (obj) {
1264         if (obj instanceof RegExp)
1265             return obj.source;
1266         return quotemeta(obj);
1267     }
1269     keywords(arguments, $path = "", $tlds = ["com"], $allow_www = false);
1270     var domain = regex_to_string(arguments.$domain);
1271     if(arguments.$allow_www) {
1272         domain = "(?:www\.)?" + domain;
1273     }
1274     var path   = regex_to_string(arguments.$path);
1275     var tlds   = arguments.$tlds;
1276     var regex = "^https?://" + domain + "\\." + choice_regex(tlds) + "/" + path;
1277     return new RegExp(regex);
1281  * splice_ranges: Given an ordered array of non-overlapping ranges,
1282  * represented as elements of [start, end], insert a new range into the
1283  * array, extending, replacing, or merging existing ranges as needed.
1284  * Mutates `arr' in place, but returns the reference to it.
1286  * Examples:
1288  * splice_range([[1,3],[4,6], 5, 8)
1289  *  => [[1,3],[4,8]]
1291  * splice_range([[1,3],[4,6],[7,10]], 2, 8)
1292  *  => [[1,10]]
1293  */
1294 function splice_range (arr, start, end) {
1295     for (var i = 0; i < arr.length; ++i) {
1296         let [n,m] = arr[i];
1297         if (start > m)
1298             continue;
1299         if (end < n) {
1300             arr.splice(i, 0, [start, end]);
1301             break;
1302         }
1303         if (start < n)
1304             arr[i][0] = start;
1306         if (end >= n) {
1307             /*
1308              * The range we are inserting overlaps the current
1309              * range. We need to scan right to see if it also contains any other
1310              * ranges entirely, and remove them if necessary.
1311              */
1312             var j = i;
1313             while (j < arr.length && end >= arr[j][0])
1314                 j++;
1315             j--;
1316             arr[i][1] = Math.max(end, arr[j][1]);
1317             arr.splice(i + 1, j - i);
1318             break;
1319         }
1320     }
1321     if (start > arr[arr.length - 1][1])
1322         arr.push([start, end]);
1323     return arr;
1327 function compute_url_up_path (url) {
1328     var new_url = Cc["@mozilla.org/network/standard-url;1"]
1329         .createInstance (Ci.nsIURL);
1330     new_url.spec = url;
1331     var up;
1332     if (new_url.param != "" || new_url.query != "")
1333         up = new_url.filePath;
1334     else if (new_url.fileName != "")
1335         up = ".";
1336     else
1337         up = "..";
1338     return up;
1342 function url_path_trim (url) {
1343     var uri = make_uri(url);
1344     uri.spec = url;
1345     uri.path = "";
1346     return uri.spec;
1349 /* possibly_valid_url returns true if the string might be a valid
1350  * thing to pass to nsIWebNavigation.loadURI.  Currently just checks
1351  * that there's no whitespace in the middle and that it's not entirely
1352  * whitespace.
1353  */
1354 function possibly_valid_url (url) {
1355     return !(/\S\s+\S/.test(url)) && !(/^\s*$/.test(url));
1359 /* remove_duplicates_filter returns a function that can be
1360  * used in Array.filter.  It removes duplicates.
1361  */
1362 function remove_duplicates_filter () {
1363     var acc = {};
1364     return function (x) {
1365         if (acc[x]) return false;
1366         acc[x] = 1;
1367         return true;
1368     };
1372 /* get_current_profile returns the name of the current profile, or
1373  * null if that information cannot be found.  The result is cached for
1374  * quick repeat lookup.  This is safe because xulrunner does not
1375  * support switching profiles on the fly.
1377  * Profiles don't necessarily have a name--as such this information should
1378  * not be depended on for anything important.  It is mainly intended for
1379  * decoration of the window title and mode-line.
1380  */
1381 let (profile_name = null) {
1382     function get_current_profile () {
1383         if (profile_name)
1384             return profile_name;
1385         if ("@mozilla.org/profile/manager;1" in Cc) {
1386             profile_name = Cc["@mozilla.org/profile/manager;1"]
1387                 .getService(Ci.nsIProfile)
1388                 .currentProfile;
1389             return profile_name;
1390         }
1391         var current_profile_path = Cc["@mozilla.org/file/directory_service;1"]
1392             .getService(Ci.nsIProperties)
1393             .get("ProfD", Ci.nsIFile).path;
1394         var profile_service = Cc["@mozilla.org/toolkit/profile-service;1"]
1395             .getService(Components.interfaces.nsIToolkitProfileService);
1396         var profiles = profile_service.profiles;
1397         while (profiles.hasMoreElements()) {
1398             var p = profiles.getNext().QueryInterface(Ci.nsIToolkitProfile);
1399             if (current_profile_path == p.localDir.path ||
1400                 current_profile_path == p.rootDir.path)
1401             {
1402                 profile_name = p.name;
1403                 return p.name;
1404             }
1405         }
1406         return null;
1407     }
1412  * Given an array, switches places on the subarrays at index i1 to i2 and j1 to
1413  * j2. Leaves the rest of the array unchanged.
1414  */
1415 function switch_subarrays (arr, i1, i2, j1, j2) {
1416     return arr.slice(0, i1) +
1417         arr.slice(j1, j2) +
1418         arr.slice(i2, j1) +
1419         arr.slice(i1, i2) +
1420         arr.slice(j2, arr.length);
1425  * Convenience function for making simple XPath lookups in a document.
1427  * @param doc The document to look in.
1428  * @param exp The XPath expression to search for.
1429  * @return The XPathResult object representing the set of found nodes.
1430  */
1431 function xpath_lookup (doc, exp) {
1432     return doc.evaluate(exp, doc, null, Ci.nsIDOMXPathResult.ANY_TYPE, null);
1436 /* get_contents_synchronously returns the contents of the given
1437  * url (string or nsIURI) as a string on success, or null on failure.
1438  */
1439 function get_contents_synchronously (url) {
1440     var ioService=Cc["@mozilla.org/network/io-service;1"]
1441         .getService(Ci.nsIIOService);
1442     var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
1443         .getService(Ci.nsIScriptableInputStream);
1444     var channel;
1445     var input;
1446     try {
1447         if (url instanceof Ci.nsIURI)
1448             channel = ioService.newChannelFromURI(url);
1449         else
1450             channel = ioService.newChannel(url, null, null);
1451         input=channel.open();
1452     } catch (e) {
1453         return null;
1454     }
1455     scriptableStream.init(input);
1456     var str=scriptableStream.read(input.available());
1457     scriptableStream.close();
1458     input.close();
1459     return str;
1464  * string_format takes a format-string containing %X style format codes,
1465  * and an object mapping the code-letters to replacement text.  It
1466  * returns a string with the formatting codes replaced by the replacement
1467  * text.
1468  */
1469 function string_format (spec, substitutions) {
1470     return spec.replace(/%(.)/g, function (a,b) { return substitutions[b]; });
1475  * dom_add_class adds a css class to the given dom node.
1476  */
1477 function dom_add_class (node, cssclass) {
1478     if (node.className)
1479         node.className += " "+cssclass;
1480     else
1481         node.className = cssclass;
1485  * dom_remove_class removes the given css class from the given dom node.
1486  */
1487 function dom_remove_class (node, cssclass) {
1488     if (! node.className)
1489         return;
1490     var classes = node.className.split(" ");
1491     classes = classes.filter(function (x) { return x != cssclass; });
1492     node.className = classes.join(" ");
1497  * dom_node_flash adds the given cssclass to the node for a brief interval.
1498  * this class can be styled, to create a flashing effect.
1499  */
1500 function dom_node_flash (node, cssclass) {
1501     dom_add_class(node, cssclass);
1502     call_after_timeout(
1503         function () {
1504             dom_remove_class(node, cssclass);
1505         },
1506         400);
1511  * data is an an alist (array of 2 element arrays) where each pair is a key
1512  * and a value.
1514  * The return type is a mime input stream that can be passed as postData to
1515  * nsIWebNavigation.loadURI.  In terms of Conkeror's API, the return value
1516  * of this function is of the correct type for the `post_data' field of a
1517  * load_spec.
1518  */
1519 function make_post_data (data) {
1520     data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
1521             for each (pair in data)].join('&');
1522     data = string_input_stream(data);
1523     return mime_input_stream(
1524         data, [["Content-Type", "application/x-www-form-urlencoded"]]);
1529  * Centers the viewport around a given element.
1531  * @param win  The window to scroll.
1532  * @param elem The element arund which we put the viewport.
1533  */
1534 function center_in_viewport (win, elem) {
1535     let point = abs_point(elem);
1537     point.x -= win.innerWidth / 2;
1538     point.y -= win.innerHeight / 2;
1540     win.scrollTo(point.x, point.y);
1545  * Takes an interactive context and a function to call with the word
1546  * at point as its sole argument, and which returns a modified word.
1547  */
1548 //XXX: this should be implemented in terms of modify_region,
1549 //     in order to work in richedit fields.
1550 function modify_word_at_point (I, func) {
1551     var focused = I.buffer.focused_element;
1553     // Skip any whitespaces at point and move point to the right place.
1554     var point = focused.selectionStart;
1555     var rest = focused.value.substring(point);
1557     // Skip any whitespaces.
1558     for (var i = 0, rlen = rest.length; i < rlen; i++) {
1559         if (" \n".indexOf(rest.charAt(i)) == -1) {
1560             point += i;
1561             break;
1562         }
1563     }
1565     // Find the next whitespace, as it is the end of the word.  If no next
1566     // whitespace is found, we're at the end of input.  TODO: Add "\n" support.
1567     goal = focused.value.indexOf(" ", point);
1568     if (goal == -1)
1569         goal = focused.value.length;
1571     // Change the value of the text field.
1572     var input = focused.value;
1573     focused.value =
1574         input.substring(0, point) +
1575         func(input.substring(point, goal)) +
1576         input.substring(goal);
1578     // Move point.
1579     focused.selectionStart = goal;
1580     focused.selectionEnd = goal;
1585  * Simple predicate returns true if elem is an nsIDOMNode or
1586  * nsIDOMWindow.
1587  */
1588 function element_dom_node_or_window_p (elem) {
1589     if (elem instanceof Ci.nsIDOMNode)
1590         return true;
1591     if (elem instanceof Ci.nsIDOMWindow)
1592         return true;
1593     return false;
1597  * Given a hook name, a buffer and a function, waits until the buffer document
1598  * has fully loaded, then calls the function with the buffer as its only
1599  * argument.
1601  * @param {String} The hook name.
1602  * @param {buffer} The buffer.
1603  * @param {function} The function to call with the buffer as its argument once
1604  *                   the buffer has loaded.
1605  */
1606 function do_when (hook, buffer, fun) {
1607     if (buffer.browser.webProgress.isLoadingDocument)
1608         add_hook.call(buffer, hook, fun);
1609     else
1610         fun(buffer);
1615  * html_escape replaces characters which are special in html with character
1616  * entities, safe for inserting as text into an html document.
1617  */
1618 function html_escape (str) {
1619     return str.replace(/&/g, '&amp;')
1620         .replace(/</g, '&lt;')
1621         .replace(/>/g, '&gt;')
1622         .replace(/"/g, '&quot;');