predicate_alist_match: typecheck for RegExp keys
[conkeror.git] / modules / utils.js
blobef3b028b44270dffa8ac45d45795ba9c4f6aa882
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2010 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 in_module(null);
12 require("io");
14 function string_hashset () {}
15 string_hashset.prototype = {
16     constructor: string_hashset,
18     add: function (s) {
19         this["-" + s] = true;
20     },
22     contains: function (s) {
23         return (("-" + s) in this);
24     },
26     remove: function (s) {
27         delete this["-" + s];
28     },
30     for_each: function (f) {
31         for (var i in this) {
32             if (i[0] == "-")
33                 f(i.slice(1));
34         }
35     },
37     iterator: function () {
38         for (let k in this) {
39             if (i[0] == "-")
40                 yield i.slice(1);
41         }
42     }
45 function string_hashmap () {}
46 string_hashmap.prototype = {
47     constructor: string_hashmap,
49     put: function (s,value) {
50         this["-" + s] = value;
51     },
53     contains: function (s) {
54         return (("-" + s) in this);
55     },
57     get: function (s, default_value) {
58         if (this.contains(s))
59             return this["-" + s];
60         return default_value;
61     },
63     get_put_default: function (s, default_value) {
64         if (this.contains(s))
65             return this["-" + s];
66         return (this["-" + s] = default_value);
67     },
69     remove: function (s) {
70         delete this["-" + s];
71     },
73     for_each: function (f) {
74         for (var i in this) {
75             if (i[0] == "-")
76                 f(i.slice(1), this[i]);
77         }
78     },
80     for_each_value: function (f) {
81         for (var i in this) {
82             if (i[0] == "-")
83                 f(this[i]);
84         }
85     },
87     iterator: function (only_keys) {
88         if (only_keys) {
89             for (let k in Iterator(this, true)) {
90                 if (k[0] == "-")
91                     yield k.slice(1);
92             }
93         } else {
94             for (let [k,v] in Iterator(this, false)) {
95                 if (k[0] == "-")
96                     yield [k.slice(1),v];
97             }
98         }
99     }
103 // Put the string on the clipboard
104 function writeToClipboard (str) {
105     var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
106         .getService(Ci.nsIClipboardHelper);
107     gClipboardHelper.copyString(str);
111 function makeURLAbsolute (base, url) {
112     // Construct nsIURL.
113     var ioService = Cc["@mozilla.org/network/io-service;1"]
114         .getService(Ci.nsIIOService);
115     var baseURI  = ioService.newURI(base, null, null);
116     return ioService.newURI(baseURI.resolve(url), null, null).spec;
120 function make_file (path) {
121     if (path instanceof Ci.nsILocalFile)
122         return path;
123     if (path == "~")
124         return get_home_directory();
125     if (WINDOWS)
126         path = path.replace("/", "\\", "g");
127     if ((POSIX && path.substring(0,2) == "~/") ||
128         (WINDOWS && path.substring(0,2) == "~\\"))
129     {
130         var f = get_home_directory();
131         f.appendRelativePath(path.substring(2));
132     } else {
133         f = Cc["@mozilla.org/file/local;1"]
134             .createInstance(Ci.nsILocalFile);
135         f.initWithPath(path);
136     }
137     return f;
141 function make_file_from_chrome (url) {
142     var crs = Cc['@mozilla.org/chrome/chrome-registry;1']
143         .getService(Ci.nsIChromeRegistry);
144     var file = crs.convertChromeURL(make_uri(url));
145     return make_file(file.path);
148 function get_document_content_disposition (document_o) {
149     var content_disposition = null;
150     try {
151         content_disposition = document_o.defaultView
152             .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
153             .getInterface(Components.interfaces.nsIDOMWindowUtils)
154             .getDocumentMetadata("content-disposition");
155     } catch (e) { }
156     return content_disposition;
160 function set_focus_no_scroll (window, element) {
161     window.document.commandDispatcher.suppressFocusScroll = true;
162     element.focus();
163     window.document.commandDispatcher.suppressFocusScroll = false;
166 function do_repeatedly_positive (func, n) {
167     var args = Array.prototype.slice.call(arguments, 2);
168     while (n-- > 0)
169         func.apply(null, args);
172 function do_repeatedly (func, n, positive_args, negative_args) {
173     if (n < 0)
174         do func.apply(null, negative_args); while (++n < 0);
175     else
176         while (n-- > 0) func.apply(null, positive_args);
181  * Given a node, returns its position relative to the document.
183  * @param node The node to get the position of.
184  * @return An object with properties "x" and "y" representing its offset from
185  *         the left and top of the document, respectively.
186  */
187 function abs_point (node) {
188     var orig = node;
189     var pt = {};
190     try {
191         pt.x = node.offsetLeft;
192         pt.y = node.offsetTop;
193         // find imagemap's coordinates
194         if (node.tagName == "AREA") {
195             var coords = node.getAttribute("coords").split(",");
196             pt.x += Number(coords[0]);
197             pt.y += Number(coords[1]);
198         }
200         node = node.offsetParent;
201         // Sometimes this fails, so just return what we got.
203         while (node.tagName != "BODY") {
204             pt.x += node.offsetLeft;
205             pt.y += node.offsetTop;
206             node = node.offsetParent;
207         }
208     } catch(e) {
209 //      node = orig;
210 //      while (node.tagName != "BODY") {
211 //          alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
212 //          node = node.offsetParent;
213 //      }
214     }
215     return pt;
219 const XHTML_NS = "http://www.w3.org/1999/xhtml";
220 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
221 const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
222 const XLINK_NS = "http://www.w3.org/1999/xlink";
223 const SVG_NS = "http://www.w3.org/2000/svg";
225 function create_XUL (window, tag_name) {
226     return window.document.createElementNS(XUL_NS, tag_name);
230 /* Used in calls to XPath evaluate */
231 function xpath_lookup_namespace (prefix) {
232     return {
233         xhtml: XHTML_NS,
234         m: MATHML_NS,
235         xul: XUL_NS,
236         svg: SVG_NS
237     }[prefix] || null;
240 function method_caller (obj, func) {
241     return function () {
242         func.apply(obj, arguments);
243     };
247 function get_window_from_frame (frame) {
248     try {
249         var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
250             .getInterface(Ci.nsIWebNavigation)
251             .QueryInterface(Ci.nsIDocShellTreeItem)
252             .rootTreeItem
253             .QueryInterface(Ci.nsIInterfaceRequestor)
254             .getInterface(Ci.nsIDOMWindow).wrappedJSObject;
255         /* window is now an XPCSafeJSObjectWrapper */
256         window.escape_wrapper(function (w) { window = w; });
257         /* window is now completely unwrapped */
258         return window;
259     } catch (e) {
260         return null;
261     }
264 function get_buffer_from_frame (window, frame) {
265     var count = window.buffers.count;
266     for (var i = 0; i < count; ++i) {
267         var b = window.buffers.get_buffer(i);
268         if (b.top_frame == frame.top)
269             return b;
270     }
271     return null;
275 function dom_generator (document, ns) {
276     this.document = document;
277     this.ns = ns;
279 dom_generator.prototype = {
280     constructor: dom_generator,
281     element: function (tag, parent) {
282         var node = this.document.createElementNS(this.ns, tag);
283         var i = 1;
284         if (parent != null && (parent instanceof Ci.nsIDOMNode)) {
285             parent.appendChild(node);
286             i = 2;
287         }
288         for (var nargs = arguments.length; i < nargs; i += 2)
289             node.setAttribute(arguments[i], arguments[i+1]);
290         return node;
291     },
293     text: function (str, parent) {
294         var node = this.document.createTextNode(str);
295         if (parent)
296             parent.appendChild(node);
297         return node;
298     },
301     stylesheet_link: function (href, parent) {
302         var node = this.element("link");
303         node.setAttribute("rel", "stylesheet");
304         node.setAttribute("type", "text/css");
305         node.setAttribute("href", href);
306         if (parent)
307             parent.appendChild(node);
308         return node;
309     },
312     add_stylesheet: function (url) {
313         var head = this.document.documentElement.firstChild;
314         this.stylesheet_link(url, head);
315     }
319  * Generates a QueryInterface function suitable for an implemenation
320  * of an XPCOM interface.  Unlike XPCOMUtils, this uses the Function
321  * constructor to generate a slightly more efficient version.  The
322  * arguments can be either Strings or elements of
323  * Components.interfaces.
324  */
325 function generate_QI () {
326     var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
327     var fstr = "if(" +
328         Array.prototype.map.call(args, function (x) {
329             return "iid.equals(Components.interfaces." + x + ")";
330         })
331         .join("||") +
332         ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
333     return new Function("iid", fstr);
337 function abort (str) {
338     var e = new Error(str);
339     e.__proto__ = abort.prototype;
340     return e;
342 abort.prototype.__proto__ = Error.prototype;
345 function get_temporary_file (name) {
346     if (name == null)
347         name = "temp.txt";
348     var file = file_locator_service.get("TmpD", Ci.nsIFile);
349     file.append(name);
350     // Create the file now to ensure that no exploits are possible
351     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
352     return file;
356 /* FIXME: This should be moved somewhere else, perhaps. */
357 function create_info_panel (window, panel_class, row_arr) {
358     /* Show information panel above minibuffer */
360     var g = new dom_generator(window.document, XUL_NS);
362     var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
363     var grid = g.element("grid", p);
364     var cols = g.element("columns", grid);
365     g.element("column", cols, "flex", "0");
366     g.element("column", cols, "flex", "1");
368     var rows = g.element("rows", grid);
369     var row;
371     for each (let [row_class, row_label, row_value] in row_arr) {
372         row = g.element("row", rows, "class", row_class);
373         g.element("label", row,
374                   "value", row_label,
375                   "class", "panel-row-label");
376         g.element("label", row,
377                   "value", row_value,
378                   "class", "panel-row-value",
379                   "crop", "end");
380     }
381     window.minibuffer.insert_before(p);
383     p.destroy = function () {
384         this.parentNode.removeChild(this);
385     };
387     return p;
392  * Paste from the X primary selection, unless the system doesn't support a
393  * primary selection, in which case fall back to the clipboard.
394  */
395 function read_from_x_primary_selection () {
396     // Get clipboard.
397     let clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
398         .getService(Components.interfaces.nsIClipboard);
400     // Fall back to global clipboard if the system doesn't support a selection
401     let which_clipboard = clipboard.supportsSelectionClipboard() ?
402         clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
404     let flavors = ["text/unicode"];
406     // Don't barf if there's nothing on the clipboard
407     if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
408         return "";
410     // Create transferable that will transfer the text.
411     let trans = Components.classes["@mozilla.org/widget/transferable;1"]
412         .createInstance(Components.interfaces.nsITransferable);
414     for each (let flavor in flavors) {
415         trans.addDataFlavor(flavor);
416     }
417     clipboard.getData(trans, which_clipboard);
419     var data_flavor = {};
420     var data = {};
421     var dataLen = {};
422     trans.getAnyTransferData(data_flavor, data, dataLen);
424     if (data) {
425         data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
426         let data_length = dataLen.value;
427         if (data_flavor.value == "text/unicode")
428             data_length = dataLen.value / 2;
429         return data.data.substring(0, data_length);
430     } else {
431         return "";
432     }
436 function predicate_alist_match (alist, key) {
437     for each (let i in alist) {
438         if (i[0] instanceof RegExp) {
439             if (i[0].exec(key))
440                 return i[1];
441         } else if (i[0](key))
442             return i[1];
443     }
444     return undefined;
448 function get_meta_title (doc) {
449     var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
450                              Ci.nsIDOMXPathResult.STRING_TYPE , null);
451     if (title && title.stringValue)
452         return title.stringValue;
453     return null;
457 function queue () {
458     this.input = [];
459     this.output = [];
461 queue.prototype = {
462     constructor: queue,
463     get length () {
464         return this.input.length + this.output.length;
465     },
466     push: function (x) {
467         this.input[this.input.length] = x;
468     },
469     pop: function (x) {
470         let l = this.output.length;
471         if (!l) {
472             l = this.input.length;
473             if (!l)
474                 return undefined;
475             this.output = this.input.reverse();
476             this.input = [];
477             let x = this.output[l];
478             this.output.length--;
479             return x;
480         }
481     }
484 function frame_iterator (root_frame, start_with) {
485     var q = new queue, x;
486     if (start_with) {
487         x = start_with;
488         do {
489             yield x;
490             for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
491                 q.push(x.frames[i]);
492         } while ((x = q.pop()));
493     }
494     x = root_frame;
495     do {
496         if (x == start_with)
497             continue;
498         yield x;
499         for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
500             q.push(x.frames[i]);
501     } while ((x = q.pop()));
504 function xml_http_request () {
505     return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
506         .createInstance(Ci.nsIXMLHttpRequest)
507         .QueryInterface(Ci.nsIJSXMLHttpRequest)
508         .QueryInterface(Ci.nsIDOMEventTarget);
511 var xml_http_request_load_listener = {
512   // nsIBadCertListener2
513   notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
514     return true;
515   },
517   // nsISSLErrorListener
518   notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
519     return true;
520   },
522   // nsIInterfaceRequestor
523   getInterface: function SSLL_getInterface (iid) {
524     return this.QueryInterface(iid);
525   },
527   // nsISupports
528   //
529   // FIXME: array comprehension used here to hack around the lack of
530   // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
531   // make it a simple generateQI when xulrunner is more stable.
532   QueryInterface: XPCOMUtils.generateQI(
533       [i for each (i in [Ci.nsIBadCertListener2,
534                          Ci.nsISSLErrorListener,
535                          Ci.nsIInterfaceRequestor])
536        if (i)])
541  * Coroutine interface for sending an HTTP request and waiting for the
542  * response. (This includes so-called "AJAX" requests.)
544  * @param lspec (required) a load_spec object or URI string (see load-spec.js)
546  * The request URI is obtained from this argument. In addition, if the
547  * load spec specifies post data, a POST request is made instead of a
548  * GET request, and the post data included in the load spec is
549  * sent. Specifically, the request_mime_type and raw_post_data
550  * properties of the load spec are used.
552  * @param $user (optional) HTTP user name to include in the request headers
553  * @param $password (optional) HTTP password to include in the request headers
555  * @param $override_mime_type (optional) Force the response to be interpreted
556  *                            as having the specified MIME type.  This is only
557  *                            really useful for forcing the MIME type to be
558  *                            text/xml or something similar, such that it is
559  *                            automatically parsed into a DOM document.
560  * @param $headers (optional) an array of [name,value] pairs (each specified as
561  *                 a two-element array) specifying additional headers to add to
562  *                 the request.
564  * @returns After the request completes (either successfully or with an error),
565  *          the nsIXMLHttpRequest object is returned.  Its responseText (for any
566  *          arbitrary document) or responseXML (if the response type is an XML
567  *          content type) properties can be accessed to examine the response
568  *          document.
570  * If an exception is thrown to the continutation (which can be obtained by the
571  * caller by calling yield CONTINUATION prior to calling this function) while the
572  * request is in progress (i.e. before this coroutine returns), the request will
573  * be aborted, and the exception will be propagated to the caller.
575  **/
576 define_keywords("$user", "$password", "$override_mime_type", "$headers");
577 function send_http_request (lspec) {
578     // why do we get warnings in jsconsole unless we initialize the
579     // following keywords?
580     keywords(arguments, $user = undefined, $password = undefined,
581              $override_mime_type = undefined, $headers = undefined);
582     if (! (lspec instanceof load_spec))
583         lspec = load_spec(lspec);
584     var req = xml_http_request();
585     var cc = yield CONTINUATION;
586     var aborting = false;
587     req.onreadystatechange = function send_http_request__onreadystatechange () {
588         if (req.readyState != 4)
589             return;
590         if (aborting)
591             return;
592         cc();
593     };
595     if (arguments.$override_mime_type)
596         req.overrideMimeType(arguments.$override_mime_type);
598     var post_data = load_spec_raw_post_data(lspec);
600     var method = post_data ? "POST" : "GET";
602     req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
603     req.channel.notificationCallbacks = xml_http_request_load_listener;
605     for each (let [name,value] in arguments.$headers) {
606         req.setRequestHeader(name, value);
607     }
609     if (post_data) {
610         req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
611         req.send(post_data);
612     } else
613         req.send(null);
615     try {
616         yield SUSPEND;
617     } catch (e) {
618         aborting = true;
619         req.abort();
620         throw e;
621     }
623     // Let the caller access the status and reponse data
624     yield co_return(req);
629  * scroll_selection_into_view takes an editable element, and scrolls it so
630  * that the selection (or insertion point) are visible.
631  */
632 function scroll_selection_into_view (field) {
633     if (field.namespaceURI == XUL_NS)
634         field = field.inputField;
635     try {
636         field.QueryInterface(Ci.nsIDOMNSEditableElement)
637             .editor
638             .selectionController
639             .scrollSelectionIntoView(
640                 Ci.nsISelectionController.SELECTION_NORMAL,
641                 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
642                 true);
643     } catch (e) {
644         // we'll get here for richedit fields
645     }
650  * build_url_regex builds a regular expression to match URLs for a given
651  * web site.
653  * Both the $domain and $path arguments can be either regexes, in
654  * which case they will be matched as is, or strings, in which case
655  * they will be matched literally.
657  * $tlds specifies a list of valid top-level-domains to match, and
658  * defaults to .com. Useful for when e.g. foo.org and foo.com are the
659  * same.
661  * If $allow_www is true, www.domain.tld will also be allowed.
662  */
663 define_keywords("$domain", "$path", "$tlds", "$allow_www");
664 function build_url_regex () {
665     function regex_to_string (obj) {
666         if (typeof obj == "object" && "source" in obj)
667             return obj.source;
668         return quotemeta(obj);
669     }
671     keywords(arguments, $path = "", $tlds = ["com"], $allow_www = false);
672     var domain = regex_to_string(arguments.$domain);
673     if(arguments.$allow_www) {
674         domain = "(?:www\.)?" + domain;
675     }
676     var path   = regex_to_string(arguments.$path);
677     var tlds   = arguments.$tlds;
678     var regex = "^https?://" + domain + "\\." + choice_regex(tlds) + "/" + path;
679     return new RegExp(regex);
683 function compute_url_up_path (url) {
684     var new_url = Cc["@mozilla.org/network/standard-url;1"]
685         .createInstance (Ci.nsIURL);
686     new_url.spec = url;
687     var up;
688     if (new_url.param != "" || new_url.query != "")
689         up = new_url.filePath;
690     else if (new_url.fileName != "")
691         up = ".";
692     else
693         up = "..";
694     return up;
698 function url_path_trim (url) {
699     var uri = make_uri(url);
700     uri.spec = url;
701     uri.path = "";
702     return uri.spec;
705 /* possibly_valid_url returns true if the string might be a valid
706  * thing to pass to nsIWebNavigation.loadURI.  Currently just checks
707  * that there's no whitespace in the middle and that it's not entirely
708  * whitespace.
709  */
710 function possibly_valid_url (url) {
711     return !(/\S\s+\S/.test(url)) && !(/^\s*$/.test(url));
716  * Convenience function for making simple XPath lookups in a document.
718  * @param doc The document to look in.
719  * @param exp The XPath expression to search for.
720  * @return The XPathResult object representing the set of found nodes.
721  */
722 function xpath_lookup (doc, exp) {
723     return doc.evaluate(exp, doc, null, Ci.nsIDOMXPathResult.ANY_TYPE, null);
727 /* get_contents_synchronously returns the contents of the given
728  * url (string or nsIURI) as a string on success, or null on failure.
729  */
730 function get_contents_synchronously (url) {
731     var ioService=Cc["@mozilla.org/network/io-service;1"]
732         .getService(Ci.nsIIOService);
733     var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
734         .getService(Ci.nsIScriptableInputStream);
735     var channel;
736     var input;
737     try {
738         if (url instanceof Ci.nsIURI)
739             channel = ioService.newChannelFromURI(url);
740         else
741             channel = ioService.newChannel(url, null, null);
742         input=channel.open();
743     } catch (e) {
744         return null;
745     }
746     scriptableStream.init(input);
747     var str=scriptableStream.read(input.available());
748     scriptableStream.close();
749     input.close();
750     return str;
755  * dom_add_class adds a css class to the given dom node.
756  */
757 function dom_add_class (node, cssclass) {
758     if (node.className) {
759         var cs = node.className.split(" ");
760         if (cs.indexOf(cssclass) != -1)
761             return;
762         cs.push(cssclass);
763         node.className = cs.join(" ");
764     } else
765         node.className = cssclass;
769  * dom_remove_class removes the given css class from the given dom node.
770  */
771 function dom_remove_class (node, cssclass) {
772     if (! node.className)
773         return;
774     var classes = node.className.split(" ");
775     classes = classes.filter(function (x) { return x != cssclass; });
776     node.className = classes.join(" ");
781  * dom_node_flash adds the given cssclass to the node for a brief interval.
782  * this class can be styled, to create a flashing effect.
783  */
784 function dom_node_flash (node, cssclass) {
785     dom_add_class(node, cssclass);
786     call_after_timeout(
787         function () {
788             dom_remove_class(node, cssclass);
789         },
790         400);
795  * data is an an alist (array of 2 element arrays) where each pair is a key
796  * and a value.
798  * The return type is a mime input stream that can be passed as postData to
799  * nsIWebNavigation.loadURI.  In terms of Conkeror's API, the return value
800  * of this function is of the correct type for the `post_data' field of a
801  * load_spec.
802  */
803 function make_post_data (data) {
804     data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
805             for each (pair in data)].join('&');
806     data = string_input_stream(data);
807     return mime_input_stream(
808         data, [["Content-Type", "application/x-www-form-urlencoded"]]);
813  * Centers the viewport around a given element.
815  * @param win  The window to scroll.
816  * @param elem The element arund which we put the viewport.
817  */
818 function center_in_viewport (win, elem) {
819     let point = abs_point(elem);
821     point.x -= win.innerWidth / 2;
822     point.y -= win.innerHeight / 2;
824     win.scrollTo(point.x, point.y);
829  * Simple predicate returns true if elem is an nsIDOMNode or
830  * nsIDOMWindow.
831  */
832 function element_dom_node_or_window_p (elem) {
833     if (elem instanceof Ci.nsIDOMNode)
834         return true;
835     if (elem instanceof Ci.nsIDOMWindow)
836         return true;
837     return false;
841  * Given a hook name, a buffer and a function, waits until the buffer document
842  * has fully loaded, then calls the function with the buffer as its only
843  * argument.
845  * @param {String} The hook name.
846  * @param {buffer} The buffer.
847  * @param {function} The function to call with the buffer as its argument once
848  *                   the buffer has loaded.
849  */
850 function do_when (hook, buffer, fun) {
851     if (buffer.browser.webProgress.isLoadingDocument)
852         add_hook.call(buffer, hook, fun);
853     else
854         fun(buffer);
859  * evaluate string s as javascript in the 'this' scope in which evaluate
860  * is called.
861  */
862 function evaluate (s) {
863     try {
864         var temp = get_temporary_file("conkeror-evaluate.tmp.js");
865         write_text_file(temp, s);
866         var url = make_uri(temp).spec;
867         return load_url(url, this);
868     } finally {
869         if (temp && temp.exists())
870             temp.remove(false);
871     }
874 provide("utils");