2 * (C) Copyright 2004-2007 Shawn Betts
3 * (C) Copyright 2007-2009 John J. Foerch
4 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
6 * Use, modification, and distribution are subject to the terms specified in the
10 function string_hashset () {}
12 string_hashset.prototype = {
13 constructor : string_hashset,
19 contains : function (s) {
20 return (("-" + s) in this);
23 remove : function (s) {
27 for_each : function (f) {
34 iterator : function () {
42 function string_hashmap () {}
44 string_hashmap.prototype = {
45 constructor : string_hashmap,
47 put : function (s,value) {
48 this["-" + s] = value;
51 contains : function (s) {
52 return (("-" + s) in this);
55 get : function (s, default_value) {
61 get_put_default : function (s, default_value) {
64 return (this["-" + s] = default_value);
67 remove : function (s) {
71 for_each : function (f) {
74 f(i.slice(1), this[i]);
78 for_each_value : function (f) {
85 iterator: function (only_keys) {
87 for (let k in Iterator(this, true)) {
92 for (let [k,v] in Iterator(this, false)) {
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) {
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)
121 var f = Cc["@mozilla.org/file/local;1"]
122 .createInstance(Ci.nsILocalFile);
123 f.initWithPath(path);
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)
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;
148 content_disposition = document_o.defaultView
149 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
150 .getInterface(Components.interfaces.nsIDOMWindowUtils)
151 .getDocumentMetadata("content-disposition");
153 return content_disposition;
157 function set_focus_no_scroll (window, element) {
158 window.document.commandDispatcher.suppressFocusScroll = true;
160 window.document.commandDispatcher.suppressFocusScroll = false;
163 function do_repeatedly_positive (func, n) {
164 var args = Array.prototype.slice.call(arguments, 2);
166 func.apply(null, args);
169 function do_repeatedly (func, n, positive_args, negative_args) {
171 do func.apply(null, negative_args); while (++n < 0);
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.
184 function abs_point (node) {
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]);
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;
207 // while (node.tagName != "BODY") {
208 // alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
209 // node = node.offsetParent;
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) {
235 function method_caller (obj, func) {
237 func.apply(obj, arguments);
242 function get_window_from_frame (frame) {
244 var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
245 .getInterface(Ci.nsIWebNavigation)
246 .QueryInterface(Ci.nsIDocShellTreeItem)
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 */
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)
270 function dom_generator (document, ns) {
271 this.document = document;
274 dom_generator.prototype = {
275 element : function (tag, parent) {
276 var node = this.document.createElementNS(this.ns, tag);
278 if (parent != null && (parent instanceof Ci.nsIDOMNode)) {
279 parent.appendChild(node);
282 for (var nargs = arguments.length; i < nargs; i += 2)
283 node.setAttribute(arguments[i], arguments[i+1]);
287 text : function (str, parent) {
288 var node = this.document.createTextNode(str);
290 parent.appendChild(node);
295 stylesheet_link : function (href, parent) {
296 var node = this.element("link");
297 node.setAttribute("rel", "stylesheet");
298 node.setAttribute("type", "text/css");
299 node.setAttribute("href", href);
301 parent.appendChild(node);
306 add_stylesheet : function (url) {
307 var head = this.document.documentElement.firstChild;
308 this.stylesheet_link(url, head);
313 * Generates a QueryInterface function suitable for an implemenation
314 * of an XPCOM interface. Unlike XPCOMUtils, this uses the Function
315 * constructor to generate a slightly more efficient version. The
316 * arguments can be either Strings or elements of
317 * Components.interfaces.
319 function generate_QI () {
320 var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
322 Array.prototype.map.call(args, function (x) {
323 return "iid.equals(Components.interfaces." + x + ")";
326 ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
327 return new Function("iid", fstr);
331 const USER_AGENT_OVERRIDE_PREF = "general.useragent.override";
333 function set_user_agent (str) {
334 session_pref(USER_AGENT_OVERRIDE_PREF, str);
338 function abort (str) {
339 var e = new Error(str);
340 e.__proto__ = abort.prototype;
343 abort.prototype.__proto__ = Error.prototype;
346 function get_temporary_file (name) {
349 var file = file_locator_service.get("TmpD", Ci.nsIFile);
351 // Create the file now to ensure that no exploits are possible
352 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
357 /* FIXME: This should be moved somewhere else, perhaps. */
358 function create_info_panel (window, panel_class, row_arr) {
359 /* Show information panel above minibuffer */
361 var g = new dom_generator(window.document, XUL_NS);
363 var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
364 var grid = g.element("grid", p);
365 var cols = g.element("columns", grid);
366 g.element("column", cols, "flex", "0");
367 g.element("column", cols, "flex", "1");
369 var rows = g.element("rows", grid);
372 for each (let [row_class, row_label, row_value] in row_arr) {
373 row = g.element("row", rows, "class", row_class);
374 g.element("label", row,
376 "class", "panel-row-label");
377 g.element("label", row,
379 "class", "panel-row-value",
382 window.minibuffer.insert_before(p);
384 p.destroy = function () {
385 this.parentNode.removeChild(this);
393 * Paste from the X primary selection, unless the system doesn't support a
394 * primary selection, in which case fall back to the clipboard.
396 function read_from_x_primary_selection () {
398 let clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
399 .getService(Components.interfaces.nsIClipboard);
401 // Fall back to global clipboard if the system doesn't support a selection
402 let which_clipboard = clipboard.supportsSelectionClipboard() ?
403 clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
405 let flavors = ["text/unicode"];
407 // Don't barf if there's nothing on the clipboard
408 if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
411 // Create transferable that will transfer the text.
412 let trans = Components.classes["@mozilla.org/widget/transferable;1"]
413 .createInstance(Components.interfaces.nsITransferable);
415 for each (let flavor in flavors) {
416 trans.addDataFlavor(flavor);
418 clipboard.getData(trans, which_clipboard);
420 var data_flavor = {};
423 trans.getAnyTransferData(data_flavor, data, dataLen);
426 data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
427 let data_length = dataLen.value;
428 if (data_flavor.value == "text/unicode")
429 data_length = dataLen.value / 2;
430 return data.data.substring(0, data_length);
437 function predicate_alist_match (alist, key) {
438 for each (let i in alist) {
446 function get_meta_title (doc) {
447 var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
448 Ci.nsIDOMXPathResult.STRING_TYPE , null);
449 if (title && title.stringValue)
450 return title.stringValue;
461 return this.input.length + this.output.length;
464 this.input[this.input.length] = x;
467 let l = this.output.length;
469 l = this.input.length;
472 this.output = this.input.reverse();
474 let x = this.output[l];
475 this.output.length--;
481 function frame_iterator (root_frame, start_with) {
482 var q = new queue, x;
487 for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
489 } while ((x = q.pop()));
496 for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
498 } while ((x = q.pop()));
501 function xml_http_request () {
502 return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
503 .createInstance(Ci.nsIXMLHttpRequest)
504 .QueryInterface(Ci.nsIJSXMLHttpRequest)
505 .QueryInterface(Ci.nsIDOMEventTarget);
508 var xml_http_request_load_listener = {
509 // nsIBadCertListener2
510 notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
514 // nsISSLErrorListener
515 notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
519 // nsIInterfaceRequestor
520 getInterface: function SSLL_getInterface (iid) {
521 return this.QueryInterface(iid);
526 // FIXME: array comprehension used here to hack around the lack of
527 // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
528 // make it a simple generateQI when xulrunner is more stable.
529 QueryInterface: XPCOMUtils.generateQI(
530 [i for each (i in [Ci.nsIBadCertListener2,
531 Ci.nsISSLErrorListener,
532 Ci.nsIInterfaceRequestor])
538 * Coroutine interface for sending an HTTP request and waiting for the
539 * response. (This includes so-called "AJAX" requests.)
541 * @param lspec (required) a load_spec object or URI string (see load-spec.js)
543 * The request URI is obtained from this argument. In addition, if the
544 * load spec specifies post data, a POST request is made instead of a
545 * GET request, and the post data included in the load spec is
546 * sent. Specifically, the request_mime_type and raw_post_data
547 * properties of the load spec are used.
549 * @param $user (optional) HTTP user name to include in the request headers
550 * @param $password (optional) HTTP password to include in the request headers
552 * @param $override_mime_type (optional) Force the response to be interpreted
553 * as having the specified MIME type. This is only
554 * really useful for forcing the MIME type to be
555 * text/xml or something similar, such that it is
556 * automatically parsed into a DOM document.
557 * @param $headers (optional) an array of [name,value] pairs (each specified as
558 * a two-element array) specifying additional headers to add to
561 * @returns After the request completes (either successfully or with an error),
562 * the nsIXMLHttpRequest object is returned. Its responseText (for any
563 * arbitrary document) or responseXML (if the response type is an XML
564 * content type) properties can be accessed to examine the response
567 * If an exception is thrown to the continutation (which can be obtained by the
568 * caller by calling yield CONTINUATION prior to calling this function) while the
569 * request is in progress (i.e. before this coroutine returns), the request will
570 * be aborted, and the exception will be propagated to the caller.
573 define_keywords("$user", "$password", "$override_mime_type", "$headers");
574 function send_http_request (lspec) {
575 // why do we get warnings in jsconsole unless we initialize the
576 // following keywords?
577 keywords(arguments, $user = undefined, $password = undefined,
578 $override_mime_type = undefined, $headers = undefined);
579 if (! (lspec instanceof load_spec))
580 lspec = load_spec(lspec);
581 var req = xml_http_request();
582 var cc = yield CONTINUATION;
583 var aborting = false;
584 req.onreadystatechange = function send_http_request__onreadysatechange () {
585 if (req.readyState != 4)
592 if (arguments.$override_mime_type)
593 req.overrideMimeType(arguments.$override_mime_type);
595 var post_data = load_spec_raw_post_data(lspec);
597 var method = post_data ? "POST" : "GET";
599 req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
600 req.channel.notificationCallbacks = xml_http_request_load_listener;
602 for each (let [name,value] in arguments.$headers) {
603 req.setRequestHeader(name, value);
607 req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
620 // Let the caller access the status and reponse data
621 yield co_return(req);
626 * scroll_selection_into_view takes an editable element, and scrolls it so
627 * that the selection (or insertion point) are visible.
629 function scroll_selection_into_view (field) {
630 if (field.namespaceURI == XUL_NS)
631 field = field.inputField;
633 field.QueryInterface(Ci.nsIDOMNSEditableElement)
636 .scrollSelectionIntoView(
637 Ci.nsISelectionController.SELECTION_NORMAL,
638 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
641 // we'll get here for richedit fields
647 * build_url_regex builds a regular expression to match URLs for a given
650 * Both the $domain and $path arguments can be either regexes, in
651 * which case they will be matched as is, or strings, in which case
652 * they will be matched literally.
654 * $tlds specifies a list of valid top-level-domains to match, and
655 * defaults to .com. Useful for when e.g. foo.org and foo.com are the
658 * If $allow_www is true, www.domain.tld will also be allowed.
660 define_keywords("$domain", "$path", "$tlds", "$allow_www");
661 function build_url_regex () {
662 function regex_to_string (obj) {
663 if (obj instanceof RegExp)
665 return quotemeta(obj);
668 keywords(arguments, $path = "", $tlds = ["com"], $allow_www = false);
669 var domain = regex_to_string(arguments.$domain);
670 if(arguments.$allow_www) {
671 domain = "(?:www\.)?" + domain;
673 var path = regex_to_string(arguments.$path);
674 var tlds = arguments.$tlds;
675 var regex = "^https?://" + domain + "\\." + choice_regex(tlds) + "/" + path;
676 return new RegExp(regex);
680 function compute_url_up_path (url) {
681 var new_url = Cc["@mozilla.org/network/standard-url;1"]
682 .createInstance (Ci.nsIURL);
685 if (new_url.param != "" || new_url.query != "")
686 up = new_url.filePath;
687 else if (new_url.fileName != "")
695 function url_path_trim (url) {
696 var uri = make_uri(url);
702 /* possibly_valid_url returns true if the string might be a valid
703 * thing to pass to nsIWebNavigation.loadURI. Currently just checks
704 * that there's no whitespace in the middle and that it's not entirely
707 function possibly_valid_url (url) {
708 return !(/\S\s+\S/.test(url)) && !(/^\s*$/.test(url));
713 * Convenience function for making simple XPath lookups in a document.
715 * @param doc The document to look in.
716 * @param exp The XPath expression to search for.
717 * @return The XPathResult object representing the set of found nodes.
719 function xpath_lookup (doc, exp) {
720 return doc.evaluate(exp, doc, null, Ci.nsIDOMXPathResult.ANY_TYPE, null);
724 /* get_contents_synchronously returns the contents of the given
725 * url (string or nsIURI) as a string on success, or null on failure.
727 function get_contents_synchronously (url) {
728 var ioService=Cc["@mozilla.org/network/io-service;1"]
729 .getService(Ci.nsIIOService);
730 var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
731 .getService(Ci.nsIScriptableInputStream);
735 if (url instanceof Ci.nsIURI)
736 channel = ioService.newChannelFromURI(url);
738 channel = ioService.newChannel(url, null, null);
739 input=channel.open();
743 scriptableStream.init(input);
744 var str=scriptableStream.read(input.available());
745 scriptableStream.close();
752 * dom_add_class adds a css class to the given dom node.
754 function dom_add_class (node, cssclass) {
756 node.className += " "+cssclass;
758 node.className = cssclass;
762 * dom_remove_class removes the given css class from the given dom node.
764 function dom_remove_class (node, cssclass) {
765 if (! node.className)
767 var classes = node.className.split(" ");
768 classes = classes.filter(function (x) { return x != cssclass; });
769 node.className = classes.join(" ");
774 * dom_node_flash adds the given cssclass to the node for a brief interval.
775 * this class can be styled, to create a flashing effect.
777 function dom_node_flash (node, cssclass) {
778 dom_add_class(node, cssclass);
781 dom_remove_class(node, cssclass);
788 * data is an an alist (array of 2 element arrays) where each pair is a key
791 * The return type is a mime input stream that can be passed as postData to
792 * nsIWebNavigation.loadURI. In terms of Conkeror's API, the return value
793 * of this function is of the correct type for the `post_data' field of a
796 function make_post_data (data) {
797 data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
798 for each (pair in data)].join('&');
799 data = string_input_stream(data);
800 return mime_input_stream(
801 data, [["Content-Type", "application/x-www-form-urlencoded"]]);
806 * Centers the viewport around a given element.
808 * @param win The window to scroll.
809 * @param elem The element arund which we put the viewport.
811 function center_in_viewport (win, elem) {
812 let point = abs_point(elem);
814 point.x -= win.innerWidth / 2;
815 point.y -= win.innerHeight / 2;
817 win.scrollTo(point.x, point.y);
822 * Takes an interactive context and a function to call with the word
823 * at point as its sole argument, and which returns a modified word.
825 //XXX: this should be implemented in terms of modify_region,
826 // in order to work in richedit fields.
827 function modify_word_at_point (I, func) {
828 var focused = I.buffer.focused_element;
830 // Skip any whitespaces at point and move point to the right place.
831 var point = focused.selectionStart;
832 var rest = focused.value.substring(point);
834 // Skip any whitespaces.
835 for (var i = 0, rlen = rest.length; i < rlen; i++) {
836 if (" \n".indexOf(rest.charAt(i)) == -1) {
842 // Find the next whitespace, as it is the end of the word. If no next
843 // whitespace is found, we're at the end of input. TODO: Add "\n" support.
844 goal = focused.value.indexOf(" ", point);
846 goal = focused.value.length;
848 // Change the value of the text field.
849 var input = focused.value;
851 input.substring(0, point) +
852 func(input.substring(point, goal)) +
853 input.substring(goal);
856 focused.selectionStart = goal;
857 focused.selectionEnd = goal;
862 * Simple predicate returns true if elem is an nsIDOMNode or
865 function element_dom_node_or_window_p (elem) {
866 if (elem instanceof Ci.nsIDOMNode)
868 if (elem instanceof Ci.nsIDOMWindow)
874 * Given a hook name, a buffer and a function, waits until the buffer document
875 * has fully loaded, then calls the function with the buffer as its only
878 * @param {String} The hook name.
879 * @param {buffer} The buffer.
880 * @param {function} The function to call with the buffer as its argument once
881 * the buffer has loaded.
883 function do_when (hook, buffer, fun) {
884 if (buffer.browser.webProgress.isLoadingDocument)
885 add_hook.call(buffer, hook, fun);