move some things out of utils.js into other files
[conkeror.git] / modules / utils.js
blob6e3843210256a8267ece4fc211e13c9fe6134332
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 make_file (path) {
119     if (path instanceof Ci.nsILocalFile)
120         return path;
121     var f = Cc["@mozilla.org/file/local;1"]
122         .createInstance(Ci.nsILocalFile);
123     f.initWithPath(path);
124     return f;
128 function make_uri (uri, charset, base_uri) {
129     const io_service = Cc["@mozilla.org/network/io-service;1"]
130         .getService(Ci.nsIIOService2);
131     if (uri instanceof Ci.nsIURI)
132         return uri;
133     if (uri instanceof Ci.nsIFile)
134         return io_service.newFileURI(uri);
135     return io_service.newURI(uri, charset, base_uri);
138 function make_file_from_chrome (url) {
139     var crs = Cc['@mozilla.org/chrome/chrome-registry;1']
140         .getService(Ci.nsIChromeRegistry);
141     var file = crs.convertChromeURL(make_uri(url));
142     return make_file(file.path);
145 function get_document_content_disposition (document_o) {
146     var content_disposition = null;
147     try {
148         content_disposition = document_o.defaultView
149             .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
150             .getInterface(Components.interfaces.nsIDOMWindowUtils)
151             .getDocumentMetadata("content-disposition");
152     } catch (e) { }
153     return content_disposition;
157 function set_focus_no_scroll (window, element) {
158     window.document.commandDispatcher.suppressFocusScroll = true;
159     element.focus();
160     window.document.commandDispatcher.suppressFocusScroll = false;
163 function do_repeatedly_positive (func, n) {
164     var args = Array.prototype.slice.call(arguments, 2);
165     while (n-- > 0)
166         func.apply(null, args);
169 function do_repeatedly (func, n, positive_args, negative_args) {
170     if (n < 0)
171         do func.apply(null, negative_args); while (++n < 0);
172     else
173         while (n-- > 0) func.apply(null, positive_args);
178  * Given a node, returns its position relative to the document.
180  * @param node The node to get the position of.
181  * @return An object with properties "x" and "y" representing its offset from
182  *         the left and top of the document, respectively.
183  */
184 function abs_point (node) {
185     var orig = node;
186     var pt = {};
187     try {
188         pt.x = node.offsetLeft;
189         pt.y = node.offsetTop;
190         // find imagemap's coordinates
191         if (node.tagName == "AREA") {
192             var coords = node.getAttribute("coords").split(",");
193             pt.x += Number(coords[0]);
194             pt.y += Number(coords[1]);
195         }
197         node = node.offsetParent;
198         // Sometimes this fails, so just return what we got.
200         while (node.tagName != "BODY") {
201             pt.x += node.offsetLeft;
202             pt.y += node.offsetTop;
203             node = node.offsetParent;
204         }
205     } catch(e) {
206 //      node = orig;
207 //      while (node.tagName != "BODY") {
208 //          alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
209 //          node = node.offsetParent;
210 //      }
211     }
212     return pt;
216 const XHTML_NS = "http://www.w3.org/1999/xhtml";
217 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
218 const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
219 const XLINK_NS = "http://www.w3.org/1999/xlink";
221 function create_XUL (window, tag_name) {
222     return window.document.createElementNS(XUL_NS, tag_name);
226 /* Used in calls to XPath evaluate */
227 function xpath_lookup_namespace (prefix) {
228     return {
229         xhtml: XHTML_NS,
230         m: MATHML_NS,
231         xul: XUL_NS
232     }[prefix] || null;
235 function method_caller (obj, func) {
236     return function () {
237         func.apply(obj, arguments);
238     };
242 function get_window_from_frame (frame) {
243     try {
244         var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
245             .getInterface(Ci.nsIWebNavigation)
246             .QueryInterface(Ci.nsIDocShellTreeItem)
247             .rootTreeItem
248             .QueryInterface(Ci.nsIInterfaceRequestor)
249             .getInterface(Ci.nsIDOMWindow).wrappedJSObject;
250         /* window is now an XPCSafeJSObjectWrapper */
251         window.escape_wrapper(function (w) { window = w; });
252         /* window is now completely unwrapped */
253         return window;
254     } catch (e) {
255         return null;
256     }
259 function get_buffer_from_frame (window, frame) {
260     var count = window.buffers.count;
261     for (var i = 0; i < count; ++i) {
262         var b = window.buffers.get_buffer(i);
263         if (b.top_frame == frame.top)
264             return b;
265     }
266     return null;
270 var conkeror_source_code_path = null;
272 function source_code_reference (uri, line_number) {
273     this.uri = uri;
274     this.line_number = line_number;
276 source_code_reference.prototype = {
277     get module_name () {
278         if (this.uri.indexOf(module_uri_prefix) == 0)
279             return this.uri.substring(module_uri_prefix.length);
280         return null;
281     },
283     get file_name () {
284         var file_uri_prefix = "file://";
285         if (this.uri.indexOf(file_uri_prefix) == 0)
286             return this.uri.substring(file_uri_prefix.length);
287         return null;
288     },
290     get best_uri () {
291         if (conkeror_source_code_path != null) {
292             var module_name = this.module_name;
293             if (module_name != null)
294                 return "file://" + conkeror_source_code_path + "/modules/" + module_name;
295         }
296         return this.uri;
297     },
299     open_in_editor : function() {
300         yield open_with_external_editor(load_spec(this.best_uri),
301                                         $line = this.line_number);
302     }
305 var get_caller_source_code_reference_ignored_functions = {};
307 function get_caller_source_code_reference (extra_frames_back) {
308     /* Skip at least this function itself and whoever called it (and
309      * more if the caller wants to be skipped). */
310     var frames_to_skip = 2;
311     if (extra_frames_back != null)
312         frames_to_skip += extra_frames_back;
314     for (let f = Components.stack; f != null; f = f.caller) {
315         if (frames_to_skip > 0) {
316             --frames_to_skip;
317             continue;
318         }
319         if (get_caller_source_code_reference_ignored_functions[f.name])
320             continue;
321         return new source_code_reference(f.filename, f.lineNumber);
322     }
324     return null;
327 function ignore_function_for_get_caller_source_code_reference (func_name) {
328     get_caller_source_code_reference_ignored_functions[func_name] = 1;
331 require_later("external-editor.js");
333 function dom_generator (document, ns) {
334     this.document = document;
335     this.ns = ns;
337 dom_generator.prototype = {
338     element : function (tag, parent) {
339         var node = this.document.createElementNS(this.ns, tag);
340         var i = 1;
341         if (parent != null && (parent instanceof Ci.nsIDOMNode)) {
342             parent.appendChild(node);
343             i = 2;
344         }
345         for (var nargs = arguments.length; i < nargs; i += 2)
346             node.setAttribute(arguments[i], arguments[i+1]);
347         return node;
348     },
350     text : function (str, parent) {
351         var node = this.document.createTextNode(str);
352         if (parent)
353             parent.appendChild(node);
354         return node;
355     },
358     stylesheet_link : function (href, parent) {
359         var node = this.element("link");
360         node.setAttribute("rel", "stylesheet");
361         node.setAttribute("type", "text/css");
362         node.setAttribute("href", href);
363         if (parent)
364             parent.appendChild(node);
365         return node;
366     },
369     add_stylesheet : function (url) {
370         var head = this.document.documentElement.firstChild;
371         this.stylesheet_link(url, head);
372     }
376  * Generates a QueryInterface function suitable for an implemenation
377  * of an XPCOM interface.  Unlike XPCOMUtils, this uses the Function
378  * constructor to generate a slightly more efficient version.  The
379  * arguments can be either Strings or elements of
380  * Components.interfaces.
381  */
382 function generate_QI () {
383     var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
384     var fstr = "if(" +
385         Array.prototype.map.call(args, function (x) {
386             return "iid.equals(Components.interfaces." + x + ")";
387         })
388         .join("||") +
389         ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
390     return new Function("iid", fstr);
393 const LOCALE_PREF = "general.useragent.locale";
395 function get_locale () {
396     return get_localized_pref(LOCALE_PREF) || get_pref(LOCALE_PREF);
399 const USER_AGENT_OVERRIDE_PREF = "general.useragent.override";
401 function set_user_agent (str) {
402     session_pref(USER_AGENT_OVERRIDE_PREF, str);
405 function define_builtin_commands (prefix, do_command_function, toggle_mark, mark_active_predicate, mode) {
407     // Specify a docstring
408     function D (cmd, docstring) {
409         var o = new String(cmd);
410         o.doc = docstring;
411         return o;
412     }
414     // Specify a forward/reverse pair
415     function R (a, b) {
416         var o = [a,b];
417         o.is_reverse_pair = true;
418         return o;
419     }
421     // Specify a movement/select/scroll/move-caret command group.
422     function S (command, movement, select, scroll, caret) {
423         var o = [movement, select, scroll, caret];
424         o.command = command;
425         o.is_move_select_pair = true;
426         return o;
427     }
429     var builtin_commands = [
431         /*
432          * cmd_scrollBeginLine and cmd_scrollEndLine don't do what I
433          * want, either in or out of caret mode...
434          */
435         S(D("beginning-of-line", "Move or extend the selection to the beginning of the current line."),
436           D("cmd_beginLine", "Move point to the beginning of the current line."),
437           D("cmd_selectBeginLine", "Extend selection to the beginning of the current line."),
438           D("cmd_beginLine", "Scroll to the beginning of the line"),
439           D("cmd_beginLine", "Scroll to the beginning of the line")),
440         S(D("end-of-line", "Move or extend the selection to the end of the current line."),
441           D("cmd_endLine", "Move point to the end of the current line."),
442           D("cmd_selectEndLine", "Extend selection to the end of the current line."),
443           D("cmd_endLine", "Scroll to the end of the current line."),
444           D("cmd_endLine", "Scroll to the end of the current line.")),
445         S(D("beginning-of-first-line", "Move or extend the selection to the beginning of the first line."),
446           D("cmd_moveTop", "Move point to the beginning of the first line."),
447           D("cmd_selectTop", "Extend selection to the beginning of the first line."),
448           D("cmd_scrollTop", "Scroll to the top of the buffer"),
449           D("cmd_scrollTop", "Move point to the beginning of the first line.")),
450         S(D("end-of-last-line", "Move or extend the selection to the end of the last line."),
451           D("cmd_moveBottom", "Move point to the end of the last line."),
452           D("cmd_selectBottom", "Extend selection to the end of the last line."),
453           D("cmd_scrollBottom", "Scroll to the bottom of the buffer"),
454           D("cmd_scrollBottom", "Move point to the end of the last line.")),
455         "cmd_copyOrDelete",
456         "cmd_scrollBeginLine",
457         "cmd_scrollEndLine",
458         "cmd_cutOrDelete",
459         D("cmd_copy", "Copy the selection into the clipboard."),
460         D("cmd_cut", "Cut the selection into the clipboard."),
461         D("cmd_deleteToBeginningOfLine", "Delete to the beginning of the current line."),
462         D("cmd_deleteToEndOfLine", "Delete to the end of the current line."),
463         D("cmd_selectAll", "Select all."),
464         D("cmd_scrollTop", "Scroll to the top of the buffer."),
465         D("cmd_scrollBottom", "Scroll to the bottom of the buffer.")];
467     var builtin_commands_with_count = [
468         R(S(D("forward-char", "Move or extend the selection forward one character."),
469             D("cmd_charNext", "Move point forward one character."),
470             D("cmd_selectCharNext", "Extend selection forward one character."),
471             D("cmd_scrollRight", "Scroll to the right"),
472             D("cmd_scrollRight", "Scroll to the right")),
473           S(D("backward-char", "Move or extend the selection backward one character."),
474             D("cmd_charPrevious", "Move point backward one character."),
475             D("cmd_selectCharPrevious", "Extend selection backward one character."),
476             D("cmd_scrollLeft", "Scroll to the left."),
477             D("cmd_scrollLeft", "Scroll to the left."))),
478         R(D("cmd_deleteCharForward", "Delete the following character."),
479           D("cmd_deleteCharBackward", "Delete the previous character.")),
480         R(D("cmd_deleteWordForward", "Delete the following word."),
481           D("cmd_deleteWordBackward", "Delete the previous word.")),
482         R(S(D("forward-line", "Move or extend the selection forward one line."),
483             D("cmd_lineNext", "Move point forward one line."),
484             D("cmd_selectLineNext", "Extend selection forward one line."),
485             D("cmd_scrollLineDown", "Scroll down one line."),
486             D("cmd_scrollLineDown", "Scroll down one line.")),
487           S(D("backward-line", "Move or extend the selection backward one line."),
488             D("cmd_linePrevious", "Move point backward one line."),
489             D("cmd_selectLinePrevious", "Extend selection backward one line."),
490             D("cmd_scrollLineUp", "Scroll up one line."),
491             D("cmd_scrollLineUp", "Scroll up one line."))),
492         R(S(D("forward-page", "Move or extend the selection forward one page."),
493             D("cmd_movePageDown", "Move point forward one page."),
494             D("cmd_selectPageDown", "Extend selection forward one page."),
495             D("cmd_scrollPageDown", "Scroll forward one page."),
496             D("cmd_movePageDown", "Move point forward one page.")),
497           S(D("backward-page", "Move or extend the selection backward one page."),
498             D("cmd_movePageUp", "Move point backward one page."),
499             D("cmd_selectPageUp", "Extend selection backward one page."),
500             D("cmd_scrollPageUp", "Scroll backward one page."),
501             D("cmd_movePageUp", "Move point backward one page."))),
502         R(D("cmd_undo", "Undo last editing action."),
503           D("cmd_redo", "Redo last editing action.")),
504         R(S(D("forward-word", "Move or extend the selection forward one word."),
505             D("cmd_wordNext", "Move point forward one word."),
506             D("cmd_selectWordNext", "Extend selection forward one word."),
507             D("cmd_scrollRight", "Scroll to the right."),
508             D("cmd_wordNext", "Move point forward one word.")),
509           S(D("backward-word", "Move or extend the selection backward one word."),
510             D("cmd_wordPrevious", "Move point backward one word."),
511             D("cmd_selectWordPrevious", "Extend selection backward one word."),
512             D("cmd_scrollLeft", "Scroll to the left."),
513             D("cmd_wordPrevious", "Move point backward one word."))),
514         R(D("cmd_scrollPageUp", "Scroll up one page."),
515           D("cmd_scrollPageDown", "Scroll down one page.")),
516         R(D("cmd_scrollLineUp", "Scroll up one line."),
517           D("cmd_scrollLineDown", "Scroll down one line.")),
518         R(D("cmd_scrollLeft", "Scroll left."),
519           D("cmd_scrollRight", "Scroll right.")),
520         D("cmd_paste", "Insert the contents of the clipboard.")];
522     interactive(prefix + "set-mark",
523                 "Toggle whether the mark is active.\n" +
524                 "When the mark is active, movement commands affect the selection.",
525                 toggle_mark);
527     function get_mode_idx () {
528         if (mode == 'scroll') return 2;
529         else if (mode == 'caret') return 3;
530         else return 0;
531     }
533     function get_move_select_idx (I) {
534         return mark_active_predicate(I) ? 1 : get_mode_idx();
535     }
537     function doc_for_builtin (c) {
538         var s = "";
539         if (c.doc != null)
540             s += c.doc + "\n";
541         return s + "Run the built-in command " + c + ".";
542     }
544     function define_simple_command (c) {
545         interactive(prefix + c, doc_for_builtin(c), function (I) { do_command_function(I, c); });
546     }
548     function get_move_select_doc_string (c) {
549         return c.command.doc +
550             "\nSpecifically, if the mark is active, runs `" + prefix + c[1] + "'.  " +
551             "Otherwise, runs `" + prefix + c[get_mode_idx()] + "'\n" +
552             "To toggle whether the mark is active, use `" + prefix + "set-mark'.";
553     }
555     for each (let c_temp in builtin_commands) {
556         let c = c_temp;
557         if (c.is_move_select_pair) {
558             interactive(prefix + c.command, get_move_select_doc_string(c), function (I) {
559                 var idx = get_move_select_idx(I);
560                 do_command_function(I, c[idx]);
561             });
562             define_simple_command(c[0]);
563             define_simple_command(c[1]);
564         }
565         else
566             define_simple_command(c);
567     }
569     function get_reverse_pair_doc_string (main_doc, alt_command) {
570         return main_doc + "\n" +
571             "The prefix argument specifies a repeat count for this command.  " +
572             "If the count is negative, `" + prefix + alt_command + "' is performed instead with " +
573             "a corresponding positive repeat count.";
574     }
576     function define_simple_reverse_pair (a, b) {
577         interactive(prefix + a, get_reverse_pair_doc_string(doc_for_builtin(a), b),
578                     function (I) {
579                         do_repeatedly(do_command_function, I.p, [I, a], [I, b]);
580                     });
581         interactive(prefix + b, get_reverse_pair_doc_string(doc_for_builtin(b), a),
582                     function (I) {
583                         do_repeatedly(do_command_function, I.p, [I, b], [I, a]);
584                     });
585     }
587     for each (let c_temp in builtin_commands_with_count) {
588         let c = c_temp;
589         if (c.is_reverse_pair) {
590             if (c[0].is_move_select_pair) {
591                 interactive(prefix + c[0].command, get_reverse_pair_doc_string(get_move_select_doc_string(c[0]),
592                                                                                c[1].command),
593                             function (I) {
594                                 var idx = get_move_select_idx(I);
595                                 do_repeatedly(do_command_function, I.p, [I, c[0][idx]], [I, c[1][idx]]);
596                             });
597                 interactive(prefix + c[1].command, get_reverse_pair_doc_string(get_move_select_doc_string(c[1]),
598                                                                                c[0].command),
599                             function (I) {
600                                 var idx = get_move_select_idx(I);
601                                 do_repeatedly(do_command_function, I.p, [I, c[1][idx]], [I, c[0][idx]]);
602                             });
603                 define_simple_reverse_pair(c[0][0], c[1][0]);
604                 define_simple_reverse_pair(c[0][1], c[1][1]);
605             } else
606                 define_simple_reverse_pair(c[0], c[1]);
607         } else {
608             let doc = doc_for_builtin(c) +
609                 "\nThe prefix argument specifies a positive repeat count for this command.";
610             interactive(prefix + c, doc, function (I) {
611                 do_repeatedly_positive(do_command_function, I.p, I, c);
612             });
613         }
614     }
618 function abort (str) {
619     var e = new Error(str);
620     e.__proto__ = abort.prototype;
621     return e;
623 abort.prototype.__proto__ = Error.prototype;
626 function get_temporary_file (name) {
627     if (name == null)
628         name = "temp.txt";
629     var file = file_locator_service.get("TmpD", Ci.nsIFile);
630     file.append(name);
631     // Create the file now to ensure that no exploits are possible
632     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
633     return file;
637 /* FIXME: This should be moved somewhere else, perhaps. */
638 function create_info_panel (window, panel_class, row_arr) {
639     /* Show information panel above minibuffer */
641     var g = new dom_generator(window.document, XUL_NS);
643     var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
644     var grid = g.element("grid", p);
645     var cols = g.element("columns", grid);
646     g.element("column", cols, "flex", "0");
647     g.element("column", cols, "flex", "1");
649     var rows = g.element("rows", grid);
650     var row;
652     for each (let [row_class, row_label, row_value] in row_arr) {
653         row = g.element("row", rows, "class", row_class);
654         g.element("label", row,
655                   "value", row_label,
656                   "class", "panel-row-label");
657         g.element("label", row,
658                   "value", row_value,
659                   "class", "panel-row-value",
660                   "crop", "end");
661     }
662     window.minibuffer.insert_before(p);
664     p.destroy = function () {
665         this.parentNode.removeChild(this);
666     };
668     return p;
673  * Paste from the X primary selection, unless the system doesn't support a
674  * primary selection, in which case fall back to the clipboard.
675  */
676 function read_from_x_primary_selection () {
677     // Get clipboard.
678     let clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
679         .getService(Components.interfaces.nsIClipboard);
681     // Fall back to global clipboard if the system doesn't support a selection
682     let which_clipboard = clipboard.supportsSelectionClipboard() ?
683         clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
685     let flavors = ["text/unicode"];
687     // Don't barf if there's nothing on the clipboard
688     if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
689         return "";
691     // Create transferable that will transfer the text.
692     let trans = Components.classes["@mozilla.org/widget/transferable;1"]
693         .createInstance(Components.interfaces.nsITransferable);
695     for each (let flavor in flavors) {
696         trans.addDataFlavor(flavor);
697     }
698     clipboard.getData(trans, which_clipboard);
700     var data_flavor = {};
701     var data = {};
702     var dataLen = {};
703     trans.getAnyTransferData(data_flavor, data, dataLen);
705     if (data) {
706         data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
707         let data_length = dataLen.value;
708         if (data_flavor.value == "text/unicode")
709             data_length = dataLen.value / 2;
710         return data.data.substring(0, data_length);
711     } else {
712         return "";
713     }
716 var user_variables = {};
718 function define_variable (name, default_value, doc) {
719     conkeror[name] = default_value;
720     user_variables[name] = {
721         default_value: default_value,
722         doc: doc,
723         shortdoc: get_shortdoc_string(doc),
724         source_code_reference: get_caller_source_code_reference()
725     };
728 function define_special_variable (name, getter, setter, doc) {
729     conkeror.__defineGetter__(name, getter);
730     conkeror.__defineSetter__(name, setter);
731     user_variables[name] = {
732         default_value: undefined,
733         doc: doc,
734         shortdoc: get_shortdoc_string(doc),
735         source_code_reference: get_caller_source_code_reference()
736     };
739 /* Re-define load_paths as a user variable. */
740 define_variable("load_paths", load_paths,
741                 "Array of URL prefixes searched in order when loading a module.\n" +
742                 "Each entry must end in a slash, and should begin with file:// or chrome://.");
745  * Stylesheets
746  */
747 function register_user_stylesheet (url) {
748     var uri = make_uri(url);
749     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
750         .getService(Ci.nsIStyleSheetService);
751     sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
754 function unregister_user_stylesheet (url) {
755     var uri = make_uri(url);
756     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
757         .getService(Ci.nsIStyleSheetService);
758     if (sss.sheetRegistered(uri, sss.USER_SHEET))
759         sss.unregisterSheet(uri, sss.USER_SHEET);
762 function register_agent_stylesheet (url) {
763     var uri = make_uri(url);
764     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
765         .getService(Ci.nsIStyleSheetService);
766     sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET);
769 function unregister_agent_stylesheet (url) {
770     var uri = make_uri(url);
771     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
772         .getService(Ci.nsIStyleSheetService);
773     if (sss.sheetRegistered(uri, sss.AGENT_SHEET))
774         sss.unregisterSheet(uri, sss.AGENT_SHEET);
777 function agent_stylesheet_registered_p (url) {
778     var uri = make_uri(url);
779     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
780         .getService(Ci.nsIStyleSheetService);
781     return sss.sheetRegistered(uri, sss.AGENT_SHEET);
784 function user_stylesheet_registered_p (url) {
785     var uri = make_uri(url);
786     var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
787         .getService(Ci.nsIStyleSheetService);
788     return sss.sheetRegistered(uri, sss.USER_SHEET);
793 function predicate_alist_match (alist, key) {
794     for each (let i in alist) {
795         if (i[0](key))
796             return i[1];
797     }
798     return undefined;
802 function get_meta_title (doc) {
803     var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
804                              Ci.nsIDOMXPathResult.STRING_TYPE , null);
805     if (title && title.stringValue)
806         return title.stringValue;
807     return null;
811 const PREFIX_ITEM_URI = "urn:mozilla:item:";
812 const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
814 var extension_manager = Cc["@mozilla.org/extensions/manager;1"]
815     .getService(Ci.nsIExtensionManager);
817 function get_extension_rdf_property (id, name, type) {
818     const rdf_service = Cc["@mozilla.org/rdf/rdf-service;1"]
819         .getService(Ci.nsIRDFService);
820     var value = extension_manager.datasource.GetTarget(
821         rdf_service.GetResource(PREFIX_ITEM_URI + id),
822         rdf_service.GetResource(PREFIX_NS_EM + name),
823         true);
824     if (value == null)
825         return null;
826     return value.QueryInterface(type || Ci.nsIRDFLiteral).Value;
829 function get_extension_update_item (id) {
830     return extension_manager.getItemForID(id);
833 function extension_info (id) {
834     this.id = id;
836 extension_info.prototype = {
837     // Returns the nsIUpdateItem object associated with this extension
838     get update_item () { return get_extension_update_item(this.id); },
840     get_rdf_property : function (name, type) {
841         return get_extension_rdf_property(this.id, name, type);
842     },
844     // RDF properties
845     get isDisabled () { return this.get_rdf_property("isDisabled"); },
846     get aboutURL () { return this.get_rdf_property("aboutURL"); },
847     get addonID () { return this.get_rdf_property("addonID"); },
848     get availableUpdateURL () { return this.get_rdf_property("availableUpdateURL"); },
849     get availableUpdateVersion () { return this.get_rdf_property("availableUpdateVersion"); },
850     get blocklisted () { return this.get_rdf_property("blocklisted"); },
851     get compatible () { return this.get_rdf_property("compatible"); },
852     get description () { return this.get_rdf_property("description"); },
853     get downloadURL () { return this.get_rdf_property("downloadURL"); },
854     get isDisabled () { return this.get_rdf_property("isDisabled"); },
855     get hidden () { return this.get_rdf_property("hidden"); },
856     get homepageURL () { return this.get_rdf_property("homepageURL"); },
857     get iconURL () { return this.get_rdf_property("iconURL"); },
858     get internalName () { return this.get_rdf_property("internalName"); },
859     get locked () { return this.get_rdf_property("locked"); },
860     get name () { return this.get_rdf_property("name"); },
861     get optionsURL () { return this.get_rdf_property("optionsURL"); },
862     get opType () { return this.get_rdf_property("opType"); },
863     get plugin () { return this.get_rdf_property("plugin"); },
864     get previewImage () { return this.get_rdf_property("previewImage"); },
865     get satisfiesDependencies () { return this.get_rdf_property("satisfiesDependencies"); },
866     get providesUpdatesSecurely () { return this.get_rdf_property("providesUpdatesSecurely"); },
867     get type () { return this.get_rdf_property("type", Ci.nsIRDFInt); },
868     get updateable () { return this.get_rdf_property("updateable"); },
869     get updateURL () { return this.get_rdf_property("updateURL"); },
870     get version () { return this.get_rdf_property("version"); }
873 function extension_is_enabled (id) {
874     var info = new extension_info(id);
875     return info.update_item && (info.isDisabled == "false");
878 function queue () {
879     this.input = [];
880     this.output = [];
882 queue.prototype = {
883     get length () {
884         return this.input.length + this.output.length;
885     },
886     push: function (x) {
887         this.input[this.input.length] = x;
888     },
889     pop: function (x) {
890         let l = this.output.length;
891         if (!l) {
892             l = this.input.length;
893             if (!l)
894                 return undefined;
895             this.output = this.input.reverse();
896             this.input = [];
897             let x = this.output[l];
898             this.output.length--;
899             return x;
900         }
901     }
904 function frame_iterator (root_frame, start_with) {
905     var q = new queue, x;
906     if (start_with) {
907         x = start_with;
908         do {
909             yield x;
910             for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
911                 q.push(x.frames[i]);
912         } while ((x = q.pop()));
913     }
914     x = root_frame;
915     do {
916         if (x == start_with)
917             continue;
918         yield x;
919         for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
920             q.push(x.frames[i]);
921     } while ((x = q.pop()));
924 function xml_http_request () {
925     return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
926         .createInstance(Ci.nsIXMLHttpRequest)
927         .QueryInterface(Ci.nsIJSXMLHttpRequest)
928         .QueryInterface(Ci.nsIDOMEventTarget);
931 var xml_http_request_load_listener = {
932   // nsIBadCertListener2
933   notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
934     return true;
935   },
937   // nsISSLErrorListener
938   notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
939     return true;
940   },
942   // nsIInterfaceRequestor
943   getInterface: function SSLL_getInterface (iid) {
944     return this.QueryInterface(iid);
945   },
947   // nsISupports
948   //
949   // FIXME: array comprehension used here to hack around the lack of
950   // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
951   // make it a simple generateQI when xulrunner is more stable.
952   QueryInterface: XPCOMUtils.generateQI(
953       [i for each (i in [Ci.nsIBadCertListener2,
954                          Ci.nsISSLErrorListener,
955                          Ci.nsIInterfaceRequestor])
956        if (i)])
961  * Coroutine interface for sending an HTTP request and waiting for the
962  * response. (This includes so-called "AJAX" requests.)
964  * @param lspec (required) a load_spec object or URI string (see load-spec.js)
966  * The request URI is obtained from this argument. In addition, if the
967  * load spec specifies post data, a POST request is made instead of a
968  * GET request, and the post data included in the load spec is
969  * sent. Specifically, the request_mime_type and raw_post_data
970  * properties of the load spec are used.
972  * @param $user (optional) HTTP user name to include in the request headers
973  * @param $password (optional) HTTP password to include in the request headers
975  * @param $override_mime_type (optional) Force the response to be interpreted
976  *                            as having the specified MIME type.  This is only
977  *                            really useful for forcing the MIME type to be
978  *                            text/xml or something similar, such that it is
979  *                            automatically parsed into a DOM document.
980  * @param $headers (optional) an array of [name,value] pairs (each specified as
981  *                 a two-element array) specifying additional headers to add to
982  *                 the request.
984  * @returns After the request completes (either successfully or with an error),
985  *          the nsIXMLHttpRequest object is returned.  Its responseText (for any
986  *          arbitrary document) or responseXML (if the response type is an XML
987  *          content type) properties can be accessed to examine the response
988  *          document.
990  * If an exception is thrown to the continutation (which can be obtained by the
991  * caller by calling yield CONTINUATION prior to calling this function) while the
992  * request is in progress (i.e. before this coroutine returns), the request will
993  * be aborted, and the exception will be propagated to the caller.
995  **/
996 define_keywords("$user", "$password", "$override_mime_type", "$headers");
997 function send_http_request (lspec) {
998     // why do we get warnings in jsconsole unless we initialize the
999     // following keywords?
1000     keywords(arguments, $user = undefined, $password = undefined,
1001              $override_mime_type = undefined, $headers = undefined);
1002     if (! (lspec instanceof load_spec))
1003         lspec = load_spec(lspec);
1004     var req = xml_http_request();
1005     var cc = yield CONTINUATION;
1006     var aborting = false;
1007     req.onreadystatechange = function send_http_request__onreadysatechange () {
1008         if (req.readyState != 4)
1009             return;
1010         if (aborting)
1011             return;
1012         cc();
1013     };
1015     if (arguments.$override_mime_type)
1016         req.overrideMimeType(arguments.$override_mime_type);
1018     var post_data = load_spec_raw_post_data(lspec);
1020     var method = post_data ? "POST" : "GET";
1022     req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
1023     req.channel.notificationCallbacks = xml_http_request_load_listener;
1025     for each (let [name,value] in arguments.$headers) {
1026         req.setRequestHeader(name, value);
1027     }
1029     if (post_data) {
1030         req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
1031         req.send(post_data);
1032     } else
1033         req.send(null);
1035     try {
1036         yield SUSPEND;
1037     } catch (e) {
1038         aborting = true;
1039         req.abort();
1040         throw e;
1041     }
1043     // Let the caller access the status and reponse data
1044     yield co_return(req);
1049  * scroll_selection_into_view takes an editable element, and scrolls it so
1050  * that the selection (or insertion point) are visible.
1051  */
1052 function scroll_selection_into_view (field) {
1053     if (field.namespaceURI == XUL_NS)
1054         field = field.inputField;
1055     try {
1056         field.QueryInterface(Ci.nsIDOMNSEditableElement)
1057             .editor
1058             .selectionController
1059             .scrollSelectionIntoView(
1060                 Ci.nsISelectionController.SELECTION_NORMAL,
1061                 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
1062                 true);
1063     } catch (e) {
1064         // we'll get here for richedit fields
1065     }
1070  * build_url_regex builds a regular expression to match URLs for a given
1071  * web site.
1073  * Both the $domain and $path arguments can be either regexes, in
1074  * which case they will be matched as is, or strings, in which case
1075  * they will be matched literally.
1077  * $tlds specifies a list of valid top-level-domains to match, and
1078  * defaults to .com. Useful for when e.g. foo.org and foo.com are the
1079  * same.
1081  * If $allow_www is true, www.domain.tld will also be allowed.
1082  */
1083 define_keywords("$domain", "$path", "$tlds", "$allow_www");
1084 function build_url_regex () {
1085     function regex_to_string (obj) {
1086         if (obj instanceof RegExp)
1087             return obj.source;
1088         return quotemeta(obj);
1089     }
1091     keywords(arguments, $path = "", $tlds = ["com"], $allow_www = false);
1092     var domain = regex_to_string(arguments.$domain);
1093     if(arguments.$allow_www) {
1094         domain = "(?:www\.)?" + domain;
1095     }
1096     var path   = regex_to_string(arguments.$path);
1097     var tlds   = arguments.$tlds;
1098     var regex = "^https?://" + domain + "\\." + choice_regex(tlds) + "/" + path;
1099     return new RegExp(regex);
1103  * splice_ranges: Given an ordered array of non-overlapping ranges,
1104  * represented as elements of [start, end], insert a new range into the
1105  * array, extending, replacing, or merging existing ranges as needed.
1106  * Mutates `arr' in place, but returns the reference to it.
1108  * Examples:
1110  * splice_range([[1,3],[4,6], 5, 8)
1111  *  => [[1,3],[4,8]]
1113  * splice_range([[1,3],[4,6],[7,10]], 2, 8)
1114  *  => [[1,10]]
1115  */
1116 function splice_range (arr, start, end) {
1117     for (var i = 0; i < arr.length; ++i) {
1118         let [n,m] = arr[i];
1119         if (start > m)
1120             continue;
1121         if (end < n) {
1122             arr.splice(i, 0, [start, end]);
1123             break;
1124         }
1125         if (start < n)
1126             arr[i][0] = start;
1128         if (end >= n) {
1129             /*
1130              * The range we are inserting overlaps the current
1131              * range. We need to scan right to see if it also contains any other
1132              * ranges entirely, and remove them if necessary.
1133              */
1134             var j = i;
1135             while (j < arr.length && end >= arr[j][0])
1136                 j++;
1137             j--;
1138             arr[i][1] = Math.max(end, arr[j][1]);
1139             arr.splice(i + 1, j - i);
1140             break;
1141         }
1142     }
1143     if (start > arr[arr.length - 1][1])
1144         arr.push([start, end]);
1145     return arr;
1149 function compute_url_up_path (url) {
1150     var new_url = Cc["@mozilla.org/network/standard-url;1"]
1151         .createInstance (Ci.nsIURL);
1152     new_url.spec = url;
1153     var up;
1154     if (new_url.param != "" || new_url.query != "")
1155         up = new_url.filePath;
1156     else if (new_url.fileName != "")
1157         up = ".";
1158     else
1159         up = "..";
1160     return up;
1164 function url_path_trim (url) {
1165     var uri = make_uri(url);
1166     uri.spec = url;
1167     uri.path = "";
1168     return uri.spec;
1171 /* possibly_valid_url returns true if the string might be a valid
1172  * thing to pass to nsIWebNavigation.loadURI.  Currently just checks
1173  * that there's no whitespace in the middle and that it's not entirely
1174  * whitespace.
1175  */
1176 function possibly_valid_url (url) {
1177     return !(/\S\s+\S/.test(url)) && !(/^\s*$/.test(url));
1181 /* remove_duplicates_filter returns a function that can be
1182  * used in Array.filter.  It removes duplicates.
1183  */
1184 function remove_duplicates_filter () {
1185     var acc = {};
1186     return function (x) {
1187         if (acc[x]) return false;
1188         acc[x] = 1;
1189         return true;
1190     };
1195  * Given an array, switches places on the subarrays at index i1 to i2 and j1 to
1196  * j2. Leaves the rest of the array unchanged.
1197  */
1198 function switch_subarrays (arr, i1, i2, j1, j2) {
1199     return arr.slice(0, i1) +
1200         arr.slice(j1, j2) +
1201         arr.slice(i2, j1) +
1202         arr.slice(i1, i2) +
1203         arr.slice(j2, arr.length);
1208  * Convenience function for making simple XPath lookups in a document.
1210  * @param doc The document to look in.
1211  * @param exp The XPath expression to search for.
1212  * @return The XPathResult object representing the set of found nodes.
1213  */
1214 function xpath_lookup (doc, exp) {
1215     return doc.evaluate(exp, doc, null, Ci.nsIDOMXPathResult.ANY_TYPE, null);
1219 /* get_contents_synchronously returns the contents of the given
1220  * url (string or nsIURI) as a string on success, or null on failure.
1221  */
1222 function get_contents_synchronously (url) {
1223     var ioService=Cc["@mozilla.org/network/io-service;1"]
1224         .getService(Ci.nsIIOService);
1225     var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
1226         .getService(Ci.nsIScriptableInputStream);
1227     var channel;
1228     var input;
1229     try {
1230         if (url instanceof Ci.nsIURI)
1231             channel = ioService.newChannelFromURI(url);
1232         else
1233             channel = ioService.newChannel(url, null, null);
1234         input=channel.open();
1235     } catch (e) {
1236         return null;
1237     }
1238     scriptableStream.init(input);
1239     var str=scriptableStream.read(input.available());
1240     scriptableStream.close();
1241     input.close();
1242     return str;
1247  * dom_add_class adds a css class to the given dom node.
1248  */
1249 function dom_add_class (node, cssclass) {
1250     if (node.className)
1251         node.className += " "+cssclass;
1252     else
1253         node.className = cssclass;
1257  * dom_remove_class removes the given css class from the given dom node.
1258  */
1259 function dom_remove_class (node, cssclass) {
1260     if (! node.className)
1261         return;
1262     var classes = node.className.split(" ");
1263     classes = classes.filter(function (x) { return x != cssclass; });
1264     node.className = classes.join(" ");
1269  * dom_node_flash adds the given cssclass to the node for a brief interval.
1270  * this class can be styled, to create a flashing effect.
1271  */
1272 function dom_node_flash (node, cssclass) {
1273     dom_add_class(node, cssclass);
1274     call_after_timeout(
1275         function () {
1276             dom_remove_class(node, cssclass);
1277         },
1278         400);
1283  * data is an an alist (array of 2 element arrays) where each pair is a key
1284  * and a value.
1286  * The return type is a mime input stream that can be passed as postData to
1287  * nsIWebNavigation.loadURI.  In terms of Conkeror's API, the return value
1288  * of this function is of the correct type for the `post_data' field of a
1289  * load_spec.
1290  */
1291 function make_post_data (data) {
1292     data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
1293             for each (pair in data)].join('&');
1294     data = string_input_stream(data);
1295     return mime_input_stream(
1296         data, [["Content-Type", "application/x-www-form-urlencoded"]]);
1301  * Centers the viewport around a given element.
1303  * @param win  The window to scroll.
1304  * @param elem The element arund which we put the viewport.
1305  */
1306 function center_in_viewport (win, elem) {
1307     let point = abs_point(elem);
1309     point.x -= win.innerWidth / 2;
1310     point.y -= win.innerHeight / 2;
1312     win.scrollTo(point.x, point.y);
1317  * Takes an interactive context and a function to call with the word
1318  * at point as its sole argument, and which returns a modified word.
1319  */
1320 //XXX: this should be implemented in terms of modify_region,
1321 //     in order to work in richedit fields.
1322 function modify_word_at_point (I, func) {
1323     var focused = I.buffer.focused_element;
1325     // Skip any whitespaces at point and move point to the right place.
1326     var point = focused.selectionStart;
1327     var rest = focused.value.substring(point);
1329     // Skip any whitespaces.
1330     for (var i = 0, rlen = rest.length; i < rlen; i++) {
1331         if (" \n".indexOf(rest.charAt(i)) == -1) {
1332             point += i;
1333             break;
1334         }
1335     }
1337     // Find the next whitespace, as it is the end of the word.  If no next
1338     // whitespace is found, we're at the end of input.  TODO: Add "\n" support.
1339     goal = focused.value.indexOf(" ", point);
1340     if (goal == -1)
1341         goal = focused.value.length;
1343     // Change the value of the text field.
1344     var input = focused.value;
1345     focused.value =
1346         input.substring(0, point) +
1347         func(input.substring(point, goal)) +
1348         input.substring(goal);
1350     // Move point.
1351     focused.selectionStart = goal;
1352     focused.selectionEnd = goal;
1357  * Simple predicate returns true if elem is an nsIDOMNode or
1358  * nsIDOMWindow.
1359  */
1360 function element_dom_node_or_window_p (elem) {
1361     if (elem instanceof Ci.nsIDOMNode)
1362         return true;
1363     if (elem instanceof Ci.nsIDOMWindow)
1364         return true;
1365     return false;
1369  * Given a hook name, a buffer and a function, waits until the buffer document
1370  * has fully loaded, then calls the function with the buffer as its only
1371  * argument.
1373  * @param {String} The hook name.
1374  * @param {buffer} The buffer.
1375  * @param {function} The function to call with the buffer as its argument once
1376  *                   the buffer has loaded.
1377  */
1378 function do_when (hook, buffer, fun) {
1379     if (buffer.browser.webProgress.isLoadingDocument)
1380         add_hook.call(buffer, hook, fun);
1381     else
1382         fun(buffer);