added webjump_separator user variable
[conkeror/arlinius.git] / modules / content-buffer.js
blobea05f67ef3b2142977d9478232f37769080f145b
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009,2011-2012 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 require("buffer.js");
11 require("load-spec.js");
12 require("history.js");
13 require("webjump.js");
15 define_variable("homepage", "chrome://conkeror-help/content/help.html",
16                 "The url loaded by default for new content buffers.");
18 define_buffer_local_hook("content_buffer_finished_loading_hook");
19 define_buffer_local_hook("content_buffer_started_loading_hook");
20 define_buffer_local_hook("content_buffer_progress_change_hook");
21 define_buffer_local_hook("content_buffer_location_change_hook");
22 define_buffer_local_hook("content_buffer_status_change_hook");
23 define_buffer_local_hook("content_buffer_focus_change_hook");
24 define_buffer_local_hook("content_buffer_dom_link_added_hook");
25 define_buffer_local_hook("content_buffer_popup_blocked_hook");
27 define_current_buffer_hook("current_content_buffer_finished_loading_hook", "content_buffer_finished_loading_hook");
28 define_current_buffer_hook("current_content_buffer_progress_change_hook", "content_buffer_progress_change_hook");
29 define_current_buffer_hook("current_content_buffer_location_change_hook", "content_buffer_location_change_hook");
30 define_current_buffer_hook("current_content_buffer_status_change_hook", "content_buffer_status_change_hook");
31 define_current_buffer_hook("current_content_buffer_focus_change_hook", "content_buffer_focus_change_hook");
34 function content_buffer_modality (buffer) {
35     var elem = buffer.focused_element;
36     function push_keymaps (tag) {
37         buffer.content_modalities.map(
38             function (m) {
39                 if (m[tag])
40                     buffer.keymaps.push(m[tag]);
41             });
42         return null;
43     }
44     push_keymaps('normal');
45     if (elem) {
46         let p = elem.parentNode;
47         while (p && !(p instanceof Ci.nsIDOMHTMLFormElement))
48             p = p.parentNode;
49         if (p)
50             push_keymaps('form');
51     }
52     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
53         var type = (elem.getAttribute("type") || "").toLowerCase();
54         if ({checkbox:1, radio:1, submit:1, reset:1}[type])
55             return push_keymaps(type);
56         else
57             return push_keymaps('text');
58     }
59     if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
60         return push_keymaps('textarea');
61     if (elem instanceof Ci.nsIDOMHTMLSelectElement)
62         return push_keymaps('select');
63     if (elem instanceof Ci.nsIDOMHTMLAnchorElement)
64         return push_keymaps('anchor');
65     if (elem instanceof Ci.nsIDOMHTMLButtonElement)
66         return push_keymaps('button');
67     if (elem instanceof Ci.nsIDOMHTMLEmbedElement ||
68         elem instanceof Ci.nsIDOMHTMLObjectElement)
69     {
70         return push_keymaps('embed');
71     }
72     var frame = buffer.focused_frame;
73     if (frame && frame.document.designMode &&
74         frame.document.designMode == "on")
75     {
76         return push_keymaps('richedit');
77     }
78     while (elem) {
79         switch (elem.contentEditable) {
80         case "true":
81             return push_keymaps('richedit');
82         case "false":
83             return null;
84         default: // "inherit"
85             elem = elem.parentNode;
86         }
87     }
88     return null;
92 define_keywords("$load");
93 function content_buffer (window) {
94     keywords(arguments);
95     this.constructor_begin();
96     try {
97         conkeror.buffer.call(this, window, forward_keywords(arguments));
99         this.browser.addProgressListener(this);
100         var buffer = this;
101         this.browser.addEventListener("DOMTitleChanged", function (event) {
102             buffer_title_change_hook.run(buffer);
103         }, true /* capture */);
105         this.browser.addEventListener("scroll", function (event) {
106             buffer_scroll_hook.run(buffer);
107         }, true /* capture */);
109         this.browser.addEventListener("focus", function (event) {
110             content_buffer_focus_change_hook.run(buffer, event);
111         }, true /* capture */);
113         this.browser.addEventListener("DOMLinkAdded", function (event) {
114             content_buffer_dom_link_added_hook.run(buffer, event);
115         }, true /* capture */);
117         this.browser.addEventListener("DOMPopupBlocked", function (event) {
118             dumpln("Blocked popup: " + event.popupWindowURI.spec);
119             content_buffer_popup_blocked_hook.run(buffer, event);
120         }, true /* capture */);
122         this.page_modes = [];
124         this.ignore_initial_blank = true;
126         var lspec = arguments.$load;
127         if (lspec) {
128             if (load_spec_uri_string(lspec) == "about:blank")
129                 this.ignore_initial_blank = false;
130             else {
131                 /* Ensure that an existing load of about:blank is stopped */
132                 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
134                 this.load(lspec);
135             }
136         }
138         this.modalities.push(content_buffer_modality);
139         this.content_modalities = [{
140             normal: content_buffer_normal_keymap,
141             form: content_buffer_form_keymap,
142             checkbox: content_buffer_checkbox_keymap,
143             radio: content_buffer_checkbox_keymap,
144             submit: content_buffer_button_keymap,
145             reset: content_buffer_button_keymap,
146             text: content_buffer_text_keymap,
147             textarea: content_buffer_textarea_keymap,
148             select: content_buffer_select_keymap,
149             anchor: content_buffer_anchor_keymap,
150             button: content_buffer_button_keymap,
151             embed: content_buffer_embed_keymap,
152             richedit: content_buffer_richedit_keymap
153         }];
154     } finally {
155         this.constructor_end();
156     }
158 content_buffer.prototype = {
159     constructor: content_buffer,
160     toString: function () "#<content_buffer>",
162     destroy: function () {
163         this.browser.removeProgressListener(this);
164         buffer.prototype.destroy.call(this);
165     },
167     content_modalities: null,
169     get scrollX () { return this.top_frame.scrollX; },
170     get scrollY () { return this.top_frame.scrollY; },
171     get scrollMaxX () { return this.top_frame.scrollMaxX; },
172     get scrollMaxY () { return this.top_frame.scrollMaxY; },
174     /* Used to display the correct URI when the buffer opens initially
175      * even before loading has progressed far enough for currentURI to
176      * contain the correct URI. */
177     _display_uri: null,
179     get display_uri_string () {
180         if (this._display_uri)
181             return this._display_uri;
182         if (this.current_uri)
183             return this.current_uri.spec;
184         return "";
185     },
187     get title () { return this.browser.contentTitle; },
188     get description () { return this.display_uri_string; },
190     load: function (load_spec) {
191         apply_load_spec(this, load_spec);
192     },
194     _request_count: 0,
196     loading: false,
198     // nsIWebProgressListener interface
199     //
200     QueryInterface: generate_QI(Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference),
202     // This method is called to indicate state changes.
203     onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
204         if (!aRequest)
205             return;
206         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
207             this._request_count++;
208         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
209             const NS_ERROR_UNKNOWN_HOST = 2152398878;
210             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
211                 // to prevent bug 235825: wait for the request handled
212                 // by the automatic keyword resolver
213                 return;
214             }
215             // since we (try to) only handle STATE_STOP of the last request,
216             // the count of open requests should now be 0
217             this._request_count = 0;
218         }
220         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
221             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
222             // It's okay to clear what the user typed when we start
223             // loading a document. If the user types, this counter gets
224             // set to zero, if the document load ends without an
225             // onLocationChange, this counter gets decremented
226             // (so we keep it while switching tabs after failed loads)
227             this.loading = true;
228             content_buffer_started_loading_hook.run(this);
229         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
230                    aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
231             if (this.loading == true) {
232                 this.loading = false;
233                 content_buffer_finished_loading_hook.run(this);
234             }
235         }
237         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
238                            Ci.nsIWebProgressListener.STATE_START)) {
239             if (!this.loading)
240                 this.set_default_message("Done");
241         }
242     },
244     // This method is called to indicate progress changes for the
245     // currently loading page.
246     onProgressChange: function (webProgress, request, curSelf, maxSelf,
247                                 curTotal, maxTotal)
248     {
249         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
250     },
252     // This method is called to indicate a change to the current location.
253     // The url can be gotten as location.spec.
254     onLocationChange: function (webProgress, request, location) {
255         /* Attempt to ignore onLocationChange calls due to the initial
256          * loading of about:blank by all xul:browser elements. */
257         if (location.spec == "about:blank" && this.ignore_initial_blank)
258             return;
260         this.ignore_initial_blank = false;
262         //dumpln("spec: " + location.spec  +" ;;; " + this.display_uri_string);
263         /* Use the real location URI now */
264         this._display_uri = null;
265         this.set_input_mode();
266         this.page = {
267             local: { __proto__: this.local }
268         };
269         this.default_browser_object_classes = {};
270         content_buffer_location_change_hook.run(this, request, location);
271         buffer_description_change_hook.run(this);
272     },
274     // This method is called to indicate a status changes for the currently
275     // loading page.  The message is already formatted for display.
276     // Status messages could be displayed in the minibuffer output area.
277     onStatusChange: function (webProgress, request, status, msg) {
278         this.set_default_message(msg);
279         content_buffer_status_change_hook.run(this, request, status, msg);
280     },
282     // This method is called when the security state of the browser changes.
283     onSecurityChange: function (webProgress, request, state) {
284         //FIXME: implement this.
285         /*
286         const WPL = Components.interfaces.nsIWebProgressListener;
288         if (state & WPL.STATE_IS_INSECURE) {
289             // update visual indicator
290         } else {
291             var level = "unknown";
292             if (state & WPL.STATE_IS_SECURE) {
293                 if (state & WPL.STATE_SECURE_HIGH)
294                     level = "high";
295                 else if (state & WPL.STATE_SECURE_MED)
296                     level = "medium";
297                 else if (state & WPL.STATE_SECURE_LOW)
298                     level = "low";
299             } else if (state & WPL_STATE_IS_BROKEN) {
300                 level = "mixed";
301             }
302             // provide a visual indicator of the security state here.
303         }
304         */
305     },
307     /* Inherit from buffer */
308     __proto__: buffer.prototype
312 add_hook("current_content_buffer_finished_loading_hook",
313          function (buffer) {
314                  buffer.window.minibuffer.show("Done");
315          });
317 add_hook("current_content_buffer_status_change_hook",
318          function (buffer, request, status, msg) {
319              buffer.set_default_message(msg);
320          });
324 define_variable("read_url_handler_list", [],
325     "A list of handler functions which transform a typed url into a valid " +
326     "url or webjump.  If the typed input is not valid then each function " +
327     "on this list is tried in turn.  The handler function is called with " +
328     "a single string argument and it should return either a string or " +
329     "null.  The result of the first function on the list that returns a " +
330     "string is used in place of the input.");
333  * read_url_make_default_webjump_handler returns a function that
334  * transforms any input into the given webjump.  It should be the last
335  * handler on read_url_handler_list (because any input is
336  * accepted).
337  */
338 function read_url_make_default_webjump_handler (default_webjump) {
339     return function (input) {
340         return default_webjump + webjump_separator + input;
341     };
345  * read_url_make_blank_url_handler returns a function that replaces a
346  * blank (empty) input with the given url (or webjump).  The url may
347  * perform some function, eg. "javascript:location.reload()".
348  */
349 function read_url_make_blank_url_handler (url) {
350     return function (input) {
351         if (input.length == 0)
352             return url;
353         return null;
354     };
357 function try_read_url_handlers (input) {
358     var result;
359     for (var i = 0; i < read_url_handler_list.length; ++i) {
360         if ((result = read_url_handler_list[i](input)))
361             return result;
362     }
363     return input;
366 define_variable("url_completion_use_webjumps", true,
367     "Specifies whether URL completion should complete webjumps.");
369 define_variable("url_completion_use_bookmarks", true,
370     "Specifies whether URL completion should complete bookmarks.");
372 define_variable("url_completion_use_history", false,
373     "Specifies whether URL completion should complete using browser "+
374     "history.");
376 define_variable("url_completion_sort_order", "visitcount_descending",
377     "Gives the default sort order for history and bookmark completion.\n"+
378     "The value is given as a string, and the available options include: "+
379     "'none', 'title_ascending', 'date_ascending', 'uri_ascending', "+
380     "'visitcount_ascending', 'keyword_ascending', 'dateadded_ascending', "+
381     "'lastmodified_ascending', 'tags_ascending', and 'annotation_ascending'. "+
382     "For every 'ascending' option, there is a corresponding 'descending' "+
383     "option.  Additionally, with XULRunner 6 and later, the options "+
384     "'frecency_ascending' and 'frecency_descending' are available.  See also "+
385     "<https://developer.mozilla.org/en/NsINavHistoryQueryOptions#Sorting_methods>.");
387 define_variable("minibuffer_read_url_select_initial", true,
388     "Specifies whether a URL  presented in the minibuffer for editing "+
389     "should be selected.  This affects find-alternate-url.");
392 minibuffer_auto_complete_preferences["url"] = true;
393 minibuffer.prototype.read_url = function () {
394     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
395              $use_webjumps = url_completion_use_webjumps,
396              $use_history = url_completion_use_history,
397              $use_bookmarks = url_completion_use_bookmarks,
398              $sort_order = url_completion_sort_order);
399     var completer = url_completer($use_webjumps = arguments.$use_webjumps,
400         $use_bookmarks = arguments.$use_bookmarks,
401         $use_history = arguments.$use_history,
402         $sort_order = arguments.$sort_order);
403     var result = yield this.read(
404         $prompt = arguments.$prompt,
405         $history = arguments.$history,
406         $completer = completer,
407         $initial_value = arguments.$initial_value,
408         $auto_complete = "url",
409         $select = minibuffer_read_url_select_initial,
410         $match_required = false);
411     if (!possibly_valid_url(result) && !get_webjump(result))
412         result = try_read_url_handlers(result);
413     if (result == "") // well-formedness check. (could be better!)
414         throw ("invalid url or webjump (\""+ result +"\")");
415     yield co_return(load_spec(result));
420  * Overlink Mode
421  */
422 function overlink_update_status (buffer, node) {
423     if (node && node.href.length > 0)
424         buffer.window.minibuffer.show("Link: " + node.href);
425     else
426         buffer.window.minibuffer.clear();
429 function overlink_predicate (node) {
430     while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
431         node = node.parentNode;
432     return node;
435 function overlink_initialize (buffer) {
436     buffer.current_overlink = null;
437     buffer.overlink_mouseover = function (event) {
438         if (buffer != buffer.window.buffers.current ||
439             event.target == buffer.browser)
440         {
441             return;
442         }
443         var node = overlink_predicate(event.target);
444         if (node) {
445             buffer.current_overlink = event.target;
446             overlink_update_status(buffer, node);
447         }
448     };
449     buffer.overlink_mouseout = function (event) {
450         if (buffer != buffer.window.buffers.current)
451             return;
452         if (buffer.current_overlink == event.target) {
453             buffer.current_overlink = null;
454             overlink_update_status(buffer, null);
455         }
456     };
457     buffer.browser.addEventListener("mouseover", buffer.overlink_mouseover, true);
458     buffer.browser.addEventListener("mouseout", buffer.overlink_mouseout, true);
461 define_global_mode("overlink_mode",
462     function enable () {
463         add_hook("create_buffer_hook", overlink_initialize);
464         for_each_buffer(overlink_initialize);
465     },
466     function disable () {
467         remove_hook("create_buffer_hook", overlink_initialize);
468         for_each_buffer(function (b) {
469             b.browser.removeEventListener("mouseover", b.overlink_mouseover, true);
470             b.browser.removeEventListener("mouseout", b.overlink_mouseout, true);
471             delete b.current_overlink;
472             delete b.overlink_mouseover;
473             delete b.overlink_mouseout;
474         });
475     },
476     $doc = "Overlink-mode is a programmable mode for showing information "+
477            "about DOM nodes (such as link URLs) in the minibuffer when "+
478            "hovering with the mouse.");
480 overlink_mode(true);
484  * Navigation Commands
485  */
486 function go_back (b, prefix) {
487     if (prefix < 0)
488         go_forward(b, -prefix);
490     check_buffer(b, content_buffer);
492     if (b.web_navigation.canGoBack) {
493         var hist = b.web_navigation.sessionHistory;
494         var idx = hist.index - prefix;
495         if (idx < 0)
496             idx = 0;
497         b.web_navigation.gotoIndex(idx);
498     } else
499         throw interactive_error("Can't go back");
502 interactive("back",
503     "Go back in the session history for the current buffer.",
504     function (I) {go_back(I.buffer, I.p);});
507 function go_forward (b, prefix) {
508     if (prefix < 0)
509         go_back(b, -prefix);
511     check_buffer(b, content_buffer);
513     if (b.web_navigation.canGoForward) {
514         var hist = b.web_navigation.sessionHistory;
515         var idx = hist.index + prefix;
516         if (idx >= hist.count) idx = hist.count-1;
517         b.web_navigation.gotoIndex(idx);
518     } else
519         throw interactive_error("Can't go forward");
522 interactive("forward",
523             "Go forward in the session history for the current buffer.",
524             function (I) {go_forward(I.buffer, I.p);});
527 function stop_loading (b) {
528     check_buffer(b, content_buffer);
529     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
532 interactive("stop-loading",
533             "Stop loading the current document.",
534             function (I) {stop_loading(I.buffer);});
537 function reload (b, bypass_cache, element, forced_charset) {
538     check_buffer(b, content_buffer);
539     if (element) {
540         if (element instanceof Ci.nsIDOMHTMLImageElement) {
541             try {
542                 var cache = Cc['@mozilla.org/image/cache;1']
543                     .getService(Ci.imgICache);
544                 cache.removeEntry(make_uri(element.src));
545             } catch (e) {}
546         }
547         element.parentNode.replaceChild(element.cloneNode(true), element);
548     } else if (b.current_uri.spec != b.display_uri_string) {
549         apply_load_spec(b, load_spec({ uri: b.display_uri_string,
550                                        forced_charset: forced_charset }));
551     } else {
552         var flags = bypass_cache == null ?
553             Ci.nsIWebNavigation.LOAD_FLAGS_NONE :
554             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
556         if (! forced_charset && forced_charset_list)
557             forced_charset = predicate_alist_match(forced_charset_list,
558                                                    b.current_uri.spec);
560         if (forced_charset) {
561             try {
562                 var atomservice = Cc['@mozilla.org/atom-service;1']
563                     .getService(Ci.nsIAtomService);
564                 b.web_navigation.documentCharsetInfo.forcedCharset =
565                     atomservice.getAtom(forced_charset);
566             } catch (e) {}
567         }
568         b.web_navigation.reload(flags);
569     }
572 interactive("reload",
573     "Reload the current document.\n" +
574     "If a prefix argument is specified, the cache is bypassed.  If a "+
575     "DOM node is supplied via browser object, that node will be "+
576     "reloaded.",
577     function (I) {
578         check_buffer(I.buffer, content_buffer);
579         var element = yield read_browser_object(I);
580         reload(I.buffer, I.P, element, I.forced_charset);
581     },
582     $browser_object = null);
585  * browserDOMWindow: intercept window opening
586  */
587 function initialize_browser_dom_window (window) {
588     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
589         new browser_dom_window(window);
592 define_variable("browser_default_open_target", OPEN_NEW_BUFFER,
593     "Specifies how new window requests by content pages (e.g. by "+
594     "window.open from JavaScript or by using the target attribute of "+
595     "anchor and form elements) will be handled.  This will generally "+
596     "be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or "+
597     "`OPEN_NEW_WINDOW'.");
600 function browser_dom_window (window) {
601     this.window = window;
602     this.next_target = null;
604 browser_dom_window.prototype = {
605     constructor: browser_dom_window,
606     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
608     openURI: function (aURI, aOpener, aWhere, aContext) {
609         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
610         var target = this.next_target;
611         if (target == null || target == FOLLOW_DEFAULT)
612             target = browser_default_open_target;
613         this.next_target = null;
615         /* Determine the opener buffer */
616         var opener = get_buffer_from_frame(this.window, aOpener);
618         switch (browser_default_open_target) {
619         case OPEN_CURRENT_BUFFER:
620             return aOpener.top;
621         case FOLLOW_CURRENT_FRAME:
622             return aOpener;
623         case OPEN_NEW_BUFFER:
624             var buffer = new content_buffer(this.window, $opener = opener);
625             this.window.buffers.current = buffer;
626             return buffer.top_frame;
627         case OPEN_NEW_BUFFER_BACKGROUND:
628             var buffer = new content_buffer(this.window, $opener = opener);
629             return buffer.top_frame;
630         case OPEN_NEW_WINDOW:
631         default: /* shouldn't be needed */
633             /* We don't call make_window here, because that will result
634              * in the URL being loaded as the top-level document,
635              * instead of within a browser buffer.  Instead, we can
636              * rely on Mozilla using browser.chromeURL. */
637             window_set_extra_arguments(
638                 {initial_buffer_creator: buffer_creator(content_buffer, $opener = opener)}
639             );
640             return null;
641         }
642     }
645 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
649  * Page Modes
650  */
652 define_keywords("$test");
653 function page_mode (name, enable, disable) {
654     keywords(arguments);
655     buffer_mode.call(this, name, enable, disable,
656                      forward_keywords(arguments));
657     this.test = make_array(arguments.$test);
659 page_mode.prototype = {
660     constructor: page_mode,
661     __proto__: buffer_mode.prototype,
662     test: null,
663     enable: function (buffer) {
664         buffer_mode.prototype.enable.call(this, buffer);
665         buffer.page_modes.push(this.name);
666         buffer.set_input_mode();
667         return true;
668     },
669     disable: function (buffer) {
670         buffer_mode.prototype.disable.call(this, buffer);
671         var i = buffer.page_modes.indexOf(this.name);
672         if (i > -1)
673             buffer.page_modes.splice(i, 1);
674         buffer.set_input_mode();
675     }
678 function define_page_mode (name, test, enable, disable) {
679     keywords(arguments, $constructor = page_mode);
680     define_buffer_mode(name, enable, disable,
681                        $test = test,
682                        forward_keywords(arguments));
684 ignore_function_for_get_caller_source_code_reference("define_page_mode");
687 define_keywords("$modality");
688 function keymaps_page_mode (name, enable, disable) {
689     keywords(arguments);
690     page_mode.call(this, name, enable, disable,
691                    forward_keywords(arguments));
692     this.modality = arguments.$modality;
694 keymaps_page_mode.prototype = {
695     constructor: keymaps_page_mode,
696     __proto__: page_mode.prototype,
697     modality: null,
698     _enable: function (buffer) {
699         buffer.content_modalities.push(this.modality);
700     },
701     _disable: function (buffer) {
702         var i = buffer.content_modalities.indexOf(this.modality);
703         if (i > -1)
704             buffer.content_modalities.splice(i, 1);
705     }
707 function define_keymaps_page_mode (name, test, modality) {
708     keywords(arguments, $constructor = keymaps_page_mode);
709     define_buffer_mode(name, null, null,
710                        $test = test,
711                        $modality = modality,
712                        forward_keywords(arguments));
714 ignore_function_for_get_caller_source_code_reference("define_keymaps_page_mode");
717 var active_page_modes = [];
719 function page_mode_activate (page_mode) {
720     var i = active_page_modes.indexOf(page_mode.name);
721     if (i == -1)
722         active_page_modes.push(page_mode.name);
725 function page_mode_deactivate (page_mode) {
726     var i = active_page_modes.indexOf(page_mode.name);
727     if (i > -1)
728         active_page_modes.splice(i, 1);
732 function page_mode_update (buffer) {
733     for (var i = buffer.page_modes.length - 1; i >= 0; --i) {
734         var p = buffer.page_modes[i];
735         conkeror[p].disable(buffer);
736     }
737     var uri = buffer.current_uri;
738     for each (var name in active_page_modes) {
739         var m = conkeror[name];
740         m.test.some(
741             function (test) {
742                 if (test instanceof RegExp) {
743                     if (test.exec(uri.spec))
744                         return m.enable(buffer);
745                 } else if (test(uri))
746                     return m.enable(buffer);
747                 return false;
748             });
749     }
752 add_hook("content_buffer_location_change_hook", page_mode_update);
754 provide("content-buffer");