Add facility to allow user functions to transform typed URLs.
[conkeror.git] / modules / element.js
blobe1bb37522a95c8fde0a200cb7d056b40143c6a72
1 /**
2  * (C) Copyright 2007-2008 John J. Foerch
3  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4  *
5  * Portions of this file are derived from Vimperator,
6  * (C) Copyright 2006-2007 Martin Stubenschrott.
7  *
8  * Use, modification, and distribution are subject to the terms specified in the
9  * COPYING file.
10 **/
12 require("hints.js");
13 require("save.js");
14 require("mime-type-override.js");
15 require("minibuffer-read-mime-type.js");
17 var browser_object_classes = {};
19 /**
20  * handler is a coroutine called as: handler(buffer, prompt)
21  */
22 function browser_object_class (name, label, doc, handler) {
23     this.name = name;
24     this.handler = handler;
25     if (doc) this.doc = doc;
26     if (label) this.label = label;
29 function define_browser_object_class (name, label, doc, handler) {
30     var varname = 'browser_object_'+name.replace('-','_','g');
31     var ob = conkeror[varname] =
32         new browser_object_class (name, label, doc, handler);
33     interactive(
34         "browser-object-"+name,
35         "A prefix command to specify that the following command operate "+
36             "on objects of type: "+name+".",
37         function (ctx) { ctx.browser_object = ob; },
38         $prefix = true);
39     return ob;
42 function xpath_browser_object_handler (xpath_expression) {
43     return function (I, prompt) {
44         var result = yield I.buffer.window.minibuffer.read_hinted_element(
45             $buffer = I.buffer,
46             $prompt = prompt,
47             $hint_xpath_expression = xpath_expression);
48         yield co_return(result);
49     };
52 define_browser_object_class(
53     "images", "image", null,
54     xpath_browser_object_handler ("//img | //xhtml:img"));
56 define_browser_object_class(
57     "frames","frame", null,
58     function (I, prompt) {
59         check_buffer(I.buffer, content_buffer);
60         var doc = I.buffer.document;
61         if (doc.getElementsByTagName("frame").length == 0 &&
62             doc.getElementsByTagName("iframe").length == 0)
63         {
64             // only one frame (the top-level one), no need to use the hints system
65             yield co_return(I.buffer.top_frame);
66         }
67         var result = yield I.buffer.window.minibuffer.read_hinted_element(
68             $buffer = I.buffer,
69             $prompt = prompt,
70             $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
71         yield co_return(result);
72     });
74 define_browser_object_class(
75     "links", "link", null,
76     xpath_browser_object_handler (
77         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
78         "@role='link'] | " +
79         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
80         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand] | " +
81         "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
82         "//xhtml:button | //xhtml:select"));
84 define_browser_object_class(
85     "mathml", "MathML element", null,
86     xpath_browser_object_handler ("//m:math"));
88 define_browser_object_class(
89     "top", null, null,
90     function (I, prompt) { yield co_return(I.buffer.top_frame); });
92 define_browser_object_class(
93     "url", null, null,
94     function (I, prompt) {
95         check_buffer (I.buffer, content_buffer);
96         var result = yield I.buffer.window.minibuffer.read_url ($prompt = prompt);
97         yield co_return (result);
98     });
100 define_browser_object_class(
101     "pasteurl", null, null,
102     function (I, url) {
103         check_buffer (I.buffer, content_buffer);
104         let url = read_from_x_primary_selection();
105         yield co_return (url);
106     });
108 define_browser_object_class(
109     "file", null, null,
110     function (I, prompt) {
111         var result = yield I.buffer.window.minibuffer.read_file(
112             $prompt = prompt,
113             $history = I.command.name+"/file",
114             $initial_value = I.buffer.cwd.path);
115         yield co_return (result);
116     });
118 define_browser_object_class(
119     "alt", "Image Alt-text", null,
120     function (I, prompt) {
121         var result = yield I.buffer.window.minibuffer.read_hinted_element(
122             $buffer = I.buffer,
123             $prompt = prompt,
124             $hint_xpath_expression = "//img[@alt]");
125         yield (co_return (result.alt));
126     });
128 define_browser_object_class(
129     "title", "Element Title", null,
130     function (I, prompt) {
131         var result = yield I.buffer.window.minibuffer.read_hinted_element(
132             $buffer = I.buffer,
133             $prompt = prompt,
134             $hint_xpath_expression = "//*[@title]");
135         yield (co_return (result.title));
136     });
138 define_browser_object_class(
139     "title-or-alt", "Element Title or Alt-text", null,
140     function (I, prompt) {
141         var result = yield I.buffer.window.minibuffer.read_hinted_element(
142             $buffer = I.buffer,
143             $prompt = prompt,
144             $hint_xpath_expression = "//img[@alt] | //*[@title]");
145         yield (co_return (result.title ? result.title : result.alt));
146     });
148 define_browser_object_class(
149     "scrape-url", "url",
150     "Scrapes urls from the source code of the top-level document of buffer.",
151     function (I, prompt) {
152         check_buffer (I.buffer, content_buffer);
153         var completions = I.buffer.document.documentElement.innerHTML
154             .match(/http:[^\s>"]*/g)
155             .filter(remove_duplicates_filter());
156         var completer = prefix_completer($completions = completions);
157         var result = yield I.buffer.window.minibuffer.read(
158             $prompt = prompt,
159             $completer = completer,
160             $initial_value = null,
161             $auto_complete = "url",
162             $select,
163             $match_required = false);
164         yield co_return (result);
165     });
168 function read_browser_object (I)
170     var browser_object = I.browser_object;
171     // literals cannot be overridden
172     if (browser_object instanceof Function)
173         yield co_return(browser_object());
174     if (! (browser_object instanceof browser_object_class))
175         yield co_return(browser_object);
177     var prompt = I.command.prompt;
178     if (! prompt) {
179         prompt = I.command.name.split(/-|_/).join(" ");
180         prompt = prompt[0].toUpperCase() + prompt.substring(1);
181     }
182     if (I.target != null)
183         prompt += TARGET_PROMPTS[I.target];
184     if (browser_object.label)
185         prompt += " (select " + browser_object.label + ")";
186     prompt += ":";
188     var result = yield browser_object.handler.call(null, I, prompt);
189     yield co_return(result);
193 function is_dom_node_or_window(elem) {
194     if (elem instanceof Ci.nsIDOMNode)
195         return true;
196     if (elem instanceof Ci.nsIDOMWindow)
197         return true;
198     return false;
202  * This is a simple wrapper function that sets focus to elem, and
203  * bypasses the automatic focus prevention system, which might
204  * otherwise prevent this from happening.
205  */
206 function browser_set_element_focus(buffer, elem, prevent_scroll) {
207     if (!is_dom_node_or_window(elem))
208         return;
210     buffer.last_user_input_received = Date.now();
211     if (prevent_scroll)
212         set_focus_no_scroll(buffer.window, elem);
213     else
214         elem.focus();
217 function browser_element_focus(buffer, elem)
219     if (!is_dom_node_or_window(elem))
220         return;
222     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
223         // Focus the input field instead
224         elem = elem.wrappedJSObject.inputField;
225     }
227     browser_set_element_focus(buffer, elem);
228     if (elem instanceof Ci.nsIDOMWindow) {
229         return;
230     }
231     // If it is not a window, it must be an HTML element
232     var x = 0;
233     var y = 0;
234     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
235         elem.contentWindow.focus();
236         return;
237     }
238     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
239         var coords = elem.getAttribute("coords").split(",");
240         x = Number(coords[0]);
241         y = Number(coords[1]);
242     }
244     var doc = elem.ownerDocument;
245     var evt = doc.createEvent("MouseEvents");
247     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
248     elem.dispatchEvent(evt);
251 function browser_object_follow(buffer, target, elem)
253     browser_set_element_focus(buffer, elem, true /* no scroll */);
255     // XXX: would be better to let nsILocalFile objects be load_specs
256     if (elem instanceof Ci.nsILocalFile)
257         elem = elem.path;
259     var no_click = (is_load_spec(elem) ||
260                     (elem instanceof Ci.nsIDOMWindow) ||
261                     (elem instanceof Ci.nsIDOMHTMLFrameElement) ||
262                     (elem instanceof Ci.nsIDOMHTMLIFrameElement) ||
263                     (elem instanceof Ci.nsIDOMHTMLLinkElement) ||
264                     (elem instanceof Ci.nsIDOMHTMLImageElement &&
265                      !elem.hasAttribute("onmousedown") && !elem.hasAttribute("onclick")));
267     if (target == FOLLOW_DEFAULT && !no_click) {
268         var x = 1, y = 1;
269         if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
270             var coords = elem.getAttribute("coords").split(",");
271             if (coords.length >= 2) {
272                 x = Number(coords[0]) + 1;
273                 y = Number(coords[1]) + 1;
274             }
275         }
276         browser_follow_link_with_click(buffer, elem, x, y);
277         return;
278     }
280     var spec = element_get_load_spec(elem);
281     if (spec == null) {
282         throw interactive_error("Element has no associated URL");
283         return;
284     }
286     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
287         // it is nonsensical to follow a javascript url in a different
288         // buffer or window
289         target = FOLLOW_DEFAULT;
290     } else if (!(buffer instanceof content_buffer) &&
291         (target == FOLLOW_CURRENT_FRAME ||
292          target == FOLLOW_DEFAULT ||
293          target == FOLLOW_TOP_FRAME ||
294          target == OPEN_CURRENT_BUFFER))
295     {
296         target = OPEN_NEW_BUFFER;
297     }
299     switch (target) {
300     case FOLLOW_CURRENT_FRAME:
301         var current_frame = load_spec_source_frame(spec);
302         if (current_frame && current_frame != buffer.top_frame) {
303             var target_obj = get_web_navigation_for_frame(current_frame);
304             apply_load_spec(target_obj, spec);
305             break;
306         }
307     case FOLLOW_DEFAULT:
308     case FOLLOW_TOP_FRAME:
309     case OPEN_CURRENT_BUFFER:
310         buffer.load(spec);
311         break;
312     case OPEN_NEW_WINDOW:
313     case OPEN_NEW_BUFFER:
314     case OPEN_NEW_BUFFER_BACKGROUND:
315         create_buffer(buffer.window,
316                       buffer_creator(content_buffer,
317                                      $load = spec,
318                                      $configuration = buffer.configuration),
319                       target);
320     }
324  * Follow a link-like element by generating fake mouse events.
325  */
326 function browser_follow_link_with_click(buffer, elem, x, y) {
327     var doc = elem.ownerDocument;
328     var view = doc.defaultView;
330     var evt = doc.createEvent("MouseEvents");
331     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
332                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
333     elem.dispatchEvent(evt);
335     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
336                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
337     elem.dispatchEvent(evt);
340 function element_get_load_spec(elem) {
342     if (is_load_spec(elem))
343         return elem;
345     var spec = null;
347     if (elem instanceof Ci.nsIDOMWindow)
348         spec = load_spec({document: elem.document});
350     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
351              elem instanceof Ci.nsIDOMHTMLIFrameElement)
352         spec = load_spec({document: elem.contentDocument});
354     else {
355         var url = null;
356         var title = null;
358         if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
359             elem instanceof Ci.nsIDOMHTMLAreaElement ||
360             elem instanceof Ci.nsIDOMHTMLLinkElement) {
361             if (!elem.hasAttribute("href"))
362                 return null; // nothing can be done, as no nesting within these elements is allowed
363             url = elem.href;
364             title = elem.title || elem.textContent;
365         }
366         else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
367             url = elem.src;
368             title = elem.title || elem.alt;
369         }
370         else {
371             var node = elem;
372             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
373                 node = node.parentNode;
374             if (node) {
375                 if (node.hasAttribute("href"))
376                     url = node.href;
377                 else
378                     node = null;
379             }
380             if (!node) {
381                 // Try simple XLink
382                 node = elem;
383                 while (node) {
384                     if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
385                         url = node.getAttributeNS(XLINK_NS, "href");
386                         break;
387                     }
388                     node = node.parentNode;
389                 }
390                 if (url)
391                     url = makeURLAbsolute(node.baseURI, url);
392                 title = node.title || node.textContent;
393             }
394         }
395         if (url && url.length > 0) {
396             if (title && title.length == 0)
397                 title = null;
398             spec = load_spec({uri: url, source_frame: elem.ownerDocument.defaultView, title: title});
399         }
400     }
401     return spec;
405 function follow (I, target) {
406     if (target == null)
407         target = FOLLOW_DEFAULT;
408     I.target = target;
409     var element = yield read_browser_object(I);
410     // XXX: to follow in the current buffer requires that the current
411     // buffer be a content_buffer.  this is perhaps not the best place
412     // for this check, because FOLLOW_DEFAULT could signify new buffer
413     // or new window.
414     check_buffer (I.buffer, content_buffer);
415     browser_object_follow(I.buffer, target, element);
418 function follow_new_buffer (I) {
419     yield follow(I, OPEN_NEW_BUFFER);
422 function follow_new_buffer_background (I) {
423     yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
426 function follow_new_window (I) {
427     yield follow(I, OPEN_NEW_WINDOW);
430 function follow_top (I) {
431     yield follow(I, FOLLOW_TOP_FRAME);
434 function follow_current_frame (I) {
435     yield follow(I, FOLLOW_CURRENT_FRAME);
438 function follow_current_buffer (I) {
439     yield follow(I, OPEN_CURRENT_BUFFER);
443 function element_get_load_target_label(element) {
444     if (element instanceof Ci.nsIDOMWindow)
445         return "page";
446     if (element instanceof Ci.nsIDOMHTMLFrameElement)
447         return "frame";
448     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
449         return "iframe";
450     return null;
453 function element_get_operation_label(element, op_name, suffix) {
454     var target_label = element_get_load_target_label(element);
455     if (target_label != null)
456         target_label = " " + target_label;
457     else
458         target_label = "";
460     if (suffix != null)
461         suffix = " " + suffix;
462     else
463         suffix = "";
465     return op_name + target_label + suffix + ":";
469 function browser_element_copy(buffer, elem)
471     var spec = element_get_load_spec(elem);
472     var text = null;
473     if (spec)
474         text = load_spec_uri_string(spec);
475     else  {
476         if (!(elem instanceof Ci.nsIDOMNode))
477             throw interactive_error("Element has no associated text to copy.");
478         switch (elem.localName) {
479         case "INPUT":
480         case "TEXTAREA":
481             text = elem.value;
482             break;
483         case "SELECT":
484             if (elem.selectedIndex >= 0)
485                 text = elem.item(elem.selectedIndex).text;
486             break;
487         default:
488             text = elem.textContent;
489             break;
490         }
491     }
492     browser_set_element_focus(buffer, elem);
493     writeToClipboard (text);
494     buffer.window.minibuffer.message ("Copied: " + text);
498 var view_source_use_external_editor = false, view_source_function = null;
499 function browser_object_view_source(buffer, target, elem)
501     if (view_source_use_external_editor || view_source_function)
502     {
503         var spec = element_get_load_spec(elem);
504         if (spec == null) {
505             throw interactive_error("Element has no associated URL");
506             return;
507         }
509         let [file, temp] = yield download_as_temporary(spec,
510                                                        $buffer = buffer,
511                                                        $action = "View source");
512         if (view_source_use_external_editor)
513             yield open_file_with_external_editor(file, $temporary = temp);
514         else
515             yield view_source_function(file, $temporary = temp);
516         return;
517     }
519     var win = null;
520     var window = buffer.window;
521     if (elem.localName) {
522         switch (elem.localName.toLowerCase()) {
523         case "frame": case "iframe":
524             win = elem.contentWindow;
525             break;
526         case "math":
527             view_mathml_source (window, charset, elem);
528             return;
529         default:
530             throw new Error("Invalid browser element");
531         }
532     } else
533         win = elem;
534     win.focus();
536     var url_s = win.location.href;
537     if (url_s.substring (0,12) != "view-source:") {
538         try {
539             browser_object_follow(buffer, target, "view-source:" + url_s);
540         } catch(e) { dump_error(e); }
541     } else {
542         window.minibuffer.message ("Already viewing source");
543     }
546 function view_source (I, target) {
547     I.target = target;
548     var element = yield read_browser_object(I);
549     yield browser_object_view_source(I.buffer, (target == null ? OPEN_CURRENT_BUFFER : target), element);
552 function view_source_new_buffer (I) {
553     yield view_source(I, OPEN_NEW_BUFFER);
556 function view_source_new_window (I) {
557     yield view_source(I, OPEN_NEW_WINDOW);
561 function browser_element_shell_command(buffer, elem, command) {
562     var spec = element_get_load_spec(elem);
563     if (spec == null) {
564         throw interactive_error("Element has no associated URL");
565         return;
566     }
567     yield download_as_temporary(spec,
568                                 $buffer = buffer,
569                                 $shell_command = command,
570                                 $shell_command_cwd = buffer.cwd);